added project importer and grandtotal converter (#1468)
This commit is contained in:
@@ -1,6 +1,6 @@
|
||||
parameters:
|
||||
locale: en
|
||||
app_locales: ar|cs|de|de_CH|da|en|es|fr|hu|it|ja|ko|nl|pt_BR|ru|sk|sv|tr|zh_CN
|
||||
app_locales: ar|cs|de|de_CH|da|en|eo|es|eu|fr|he|hu|it|ja|ko|nl|pl|pt_BR|ro|ru|sk|sv|tr|vi|zh_CN
|
||||
|
||||
services:
|
||||
_defaults:
|
||||
@@ -20,3 +20,15 @@ services:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\Configuration']
|
||||
|
||||
# required for the importer command test
|
||||
App\Repository\UserRepository:
|
||||
class: Doctrine\ORM\EntityRepository
|
||||
factory: ['@doctrine.orm.entity_manager', getRepository]
|
||||
arguments: ['App\Entity\User']
|
||||
|
||||
# required for the importer command test
|
||||
App\Importer\ImporterService:
|
||||
public: true
|
||||
arguments:
|
||||
[ '@App\Customer\CustomerService', '@App\Project\ProjectService' ]
|
||||
|
||||
@@ -11,7 +11,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\API;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Form\API\UserApiCreateForm;
|
||||
use App\Form\API\UserApiEditForm;
|
||||
@@ -56,11 +56,11 @@ final class UserController extends BaseApiController
|
||||
*/
|
||||
private $encoder;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
|
||||
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository, UserPasswordEncoderInterface $encoder, FormConfiguration $config)
|
||||
public function __construct(ViewHandlerInterface $viewHandler, UserRepository $repository, UserPasswordEncoderInterface $encoder, SystemConfiguration $config)
|
||||
{
|
||||
$this->viewHandler = $viewHandler;
|
||||
$this->repository = $repository;
|
||||
|
||||
@@ -9,17 +9,10 @@
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Team;
|
||||
use App\Importer\InvalidFieldsException;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use League\Csv\Reader;
|
||||
use App\Importer\ImporterService;
|
||||
use App\Importer\ImportNotFoundException;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
@@ -28,72 +21,15 @@ use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* This command can change anytime, don't rely on its API for the future!
|
||||
*
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ImportCustomerCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'kimai:import:customer';
|
||||
private $importer;
|
||||
|
||||
private static $requiredHeader = [
|
||||
'Name',
|
||||
'Customer',
|
||||
];
|
||||
|
||||
private static $supportedHeader = [
|
||||
'Name',
|
||||
'Customer',
|
||||
'Comment',
|
||||
'OrderNumber',
|
||||
'OrderDate',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
private $customers;
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $projects;
|
||||
/**
|
||||
* @var TeamRepository
|
||||
*/
|
||||
private $teams;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
private $users;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var Customer[]
|
||||
*/
|
||||
private $customerCache = [];
|
||||
/**
|
||||
* The datetime of this import as formatted string.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $dateTime = '';
|
||||
/**
|
||||
* Comment that will be added to new customers, projects and activities.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $comment = '';
|
||||
|
||||
public function __construct(CustomerRepository $customers, ProjectRepository $projects, TeamRepository $teams, UserRepository $users, FormConfiguration $configuration)
|
||||
public function __construct(ImporterService $importer)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->customers = $customers;
|
||||
$this->projects = $projects;
|
||||
$this->teams = $teams;
|
||||
$this->users = $users;
|
||||
$this->configuration = $configuration;
|
||||
$this->importer = $importer;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -102,18 +38,16 @@ class ImportCustomerCommand extends Command
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName(self::$defaultName)
|
||||
->setDescription('Import projects from CSV file')
|
||||
->setName('kimai:import:customer')
|
||||
->setDescription('Import customer from CSV file')
|
||||
->setHelp(
|
||||
'This command allows to import projects from a CSV file, creating customers (if not existing) and optional empty teams for each project.' . PHP_EOL .
|
||||
'Imported customer will be matched by name and optionally created on the fly.' . PHP_EOL .
|
||||
'Required column names: ' . implode(', ', self::$requiredHeader) . PHP_EOL .
|
||||
'Supported column names: ' . implode(', ', self::$supportedHeader) . PHP_EOL
|
||||
'Import customers from a CSV file.' . PHP_EOL .
|
||||
'Customer will be matched by name or number, and if not found created on the fly.' . PHP_EOL
|
||||
)
|
||||
->addOption('teamlead', null, InputOption::VALUE_REQUIRED, 'If you want to create empty teams for each project, give the username of the teamlead to be assigned')
|
||||
->addOption('delimiter', null, InputOption::VALUE_OPTIONAL, 'The CSV field delimiter', ',')
|
||||
->addOption('comment', null, InputOption::VALUE_OPTIONAL, 'A description to be added to created customers and projects. %s will be replaced with the current datetime', 'Imported at %s')
|
||||
->addArgument('file', InputArgument::REQUIRED, 'The CSV file to be imported')
|
||||
->addOption('importer', null, InputOption::VALUE_REQUIRED, 'The importer to use (supported: default, grandtotal)', 'default')
|
||||
->addOption('reader', null, InputOption::VALUE_REQUIRED, 'The reader to use (supported: csv, csv-semicolon)', 'csv')
|
||||
->addOption('no-update', null, InputOption::VALUE_NONE, 'If you want to create new customers, but not update existing ones')
|
||||
;
|
||||
}
|
||||
|
||||
@@ -126,213 +60,108 @@ class ImportCustomerCommand extends Command
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Kimai importer: Projects');
|
||||
$io->title('Kimai importer: Customers');
|
||||
|
||||
$csvFile = $input->getArgument('file');
|
||||
if (!file_exists($csvFile)) {
|
||||
$io->error('File not existing: ' . $csvFile);
|
||||
$skipUpdate = $input->getOption('no-update');
|
||||
$doImport = true;
|
||||
$row = 1;
|
||||
$errors = 0;
|
||||
$customers = [];
|
||||
$importer = null;
|
||||
|
||||
try {
|
||||
$importer = $this->importer->getCustomerImporter($input->getOption('importer'));
|
||||
$reader = $this->importer->getReader($input->getOption('reader'));
|
||||
} catch (\Exception $ex) {
|
||||
$io->error($ex->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!is_readable($csvFile)) {
|
||||
$io->error('File cannot be read: ' . $csvFile);
|
||||
$importerFile = $input->getArgument('file');
|
||||
|
||||
try {
|
||||
$records = $reader->read($importerFile);
|
||||
} catch (ImportNotFoundException $ex) {
|
||||
$io->error('File not existing or not readable: ' . $importerFile);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
$this->dateTime = (new \DateTime())->format('Y.m.d H:i');
|
||||
$this->comment = sprintf($input->getOption('comment'), $this->dateTime);
|
||||
$amount = iterator_count($records);
|
||||
$records->rewind();
|
||||
$io->text(sprintf('Found %s rows to process, converting now ...', $amount));
|
||||
|
||||
$csv = Reader::createFromPath($csvFile, 'r');
|
||||
$csv->setDelimiter($input->getOption('delimiter'));
|
||||
$csv->setHeaderOffset(0);
|
||||
$header = $csv->getHeader();
|
||||
|
||||
// validate teamlead
|
||||
$teamlead = $input->getOption('teamlead');
|
||||
if (null !== $teamlead) {
|
||||
$tmpUser = $this->users->findOneBy(['username' => $teamlead]);
|
||||
if (null === $tmpUser) {
|
||||
$tmpUser = $this->users->findOneBy(['email' => $teamlead]);
|
||||
if (null === $tmpUser) {
|
||||
$io->error(
|
||||
sprintf(
|
||||
'You requested to create empty teams for each project, but the given teamlead cannot be found.' . PHP_EOL .
|
||||
'Please create a user with the name (or email) %s first, before continuing.' . PHP_EOL,
|
||||
$teamlead
|
||||
)
|
||||
);
|
||||
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
$teamlead = $tmpUser;
|
||||
}
|
||||
|
||||
if (!$this->validateHeader($header)) {
|
||||
$io->error(
|
||||
sprintf(
|
||||
'Found invalid CSV header: %s' . PHP_EOL .
|
||||
'Required fields: %s' . PHP_EOL .
|
||||
'All supported fields: %s' . PHP_EOL,
|
||||
implode(', ', $header),
|
||||
implode(', ', self::$requiredHeader),
|
||||
implode(', ', self::$supportedHeader)
|
||||
)
|
||||
);
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
$records = $csv->getRecords();
|
||||
|
||||
$doImport = true;
|
||||
$row = 1;
|
||||
$errors = 0;
|
||||
$progressBar = new ProgressBar($output, $amount);
|
||||
|
||||
foreach ($records as $record) {
|
||||
try {
|
||||
$this->validateRow($record);
|
||||
} catch (InvalidFieldsException $ex) {
|
||||
$io->error(sprintf('Invalid row %s, invalid fields: %s', $row, implode(', ', $ex->getFields())));
|
||||
$customers[] = $importer->convertEntryToCustomer($record);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(sprintf('Invalid row %s: %s', $row, $ex->getMessage()));
|
||||
$doImport = false;
|
||||
$errors++;
|
||||
}
|
||||
$progressBar->advance();
|
||||
|
||||
$row++;
|
||||
}
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
|
||||
if (!$doImport) {
|
||||
$io->caution(sprintf('Not importing, previous %s errors need to be fixed first.', $errors));
|
||||
|
||||
return 5;
|
||||
return 3;
|
||||
}
|
||||
|
||||
$row = 0;
|
||||
foreach ($records as $record) {
|
||||
$row++;
|
||||
$amount = \count($customers);
|
||||
$io->text(sprintf('Converted %s customers, importing into Kimai now ...', $amount));
|
||||
|
||||
$progressBar = new ProgressBar($output, $amount);
|
||||
|
||||
$created = 0;
|
||||
$updated = 0;
|
||||
$noUpdatedCustomers = 0;
|
||||
|
||||
foreach ($customers as $customer) {
|
||||
try {
|
||||
$customer = $this->getCustomer($record['Customer']);
|
||||
$projectName = $record['Name'];
|
||||
$progressBar->advance();
|
||||
|
||||
$project = new Project();
|
||||
$project->setName($projectName);
|
||||
$project->setCustomer($customer);
|
||||
|
||||
$comment = $this->comment;
|
||||
if (isset($record['Comment']) && !empty($record['Comment'])) {
|
||||
$comment = $record['Comment'];
|
||||
}
|
||||
$project->setComment($comment);
|
||||
|
||||
if (isset($record['OrderNumber']) && !empty($record['OrderNumber'])) {
|
||||
$project->setOrderNumber($record['OrderNumber']);
|
||||
}
|
||||
if (isset($record['OrderDate']) && !empty($record['OrderDate'])) {
|
||||
$project->setOrderDate($record['OrderDate']);
|
||||
}
|
||||
|
||||
$team = null;
|
||||
if (null !== $teamlead) {
|
||||
$team = new Team();
|
||||
$team->setName($projectName);
|
||||
$team->setTeamLead($teamlead);
|
||||
|
||||
$this->teams->saveTeam($team);
|
||||
}
|
||||
|
||||
$this->projects->saveProject($project);
|
||||
|
||||
if (null !== $team) {
|
||||
$project->addTeam($team);
|
||||
$team->addProject($project);
|
||||
|
||||
$this->teams->saveTeam($team);
|
||||
$this->projects->saveProject($project);
|
||||
if ($customer->getId() === null) {
|
||||
$this->importer->importCustomer($customer);
|
||||
$created++;
|
||||
} elseif ($skipUpdate === false) {
|
||||
$this->importer->importCustomer($customer);
|
||||
$updated++;
|
||||
} else {
|
||||
$noUpdatedCustomers++;
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(sprintf('Failed importing project row %s with: %s', $row, $ex->getMessage()));
|
||||
$io->error(sprintf('Failed importing customer "%s" with: %s', $customer->getName(), $ex->getMessage()));
|
||||
|
||||
return 6;
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
$io->success(sprintf('Imported %s rows', $row));
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
$io->writeln('');
|
||||
|
||||
if ($created > 0) {
|
||||
$io->success(sprintf('Imported %s customer', $created));
|
||||
}
|
||||
if ($updated > 0) {
|
||||
$io->success(sprintf('Updated %s customer', $updated));
|
||||
}
|
||||
if ($noUpdatedCustomers > 0) {
|
||||
$io->success(sprintf('Skipped %s existing customer', $noUpdatedCustomers));
|
||||
}
|
||||
|
||||
if ($updated === 0 && $created === 0) {
|
||||
$io->text('Nothing was imported');
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getCustomer(string $customerName): Customer
|
||||
{
|
||||
if (!\array_key_exists($customerName, $this->customerCache)) {
|
||||
$tmpCustomer = $this->customers->findBy(['name' => $customerName]);
|
||||
|
||||
if (\count($tmpCustomer) > 1) {
|
||||
throw new \Exception(sprintf('Found multiple customers with the name: %s', $customerName));
|
||||
} elseif (\count($tmpCustomer) === 1) {
|
||||
$tmpCustomer = $tmpCustomer[0];
|
||||
}
|
||||
|
||||
if ($tmpCustomer instanceof Customer) {
|
||||
$this->customerCache[$customerName] = $tmpCustomer;
|
||||
}
|
||||
}
|
||||
|
||||
if (\array_key_exists($customerName, $this->customerCache)) {
|
||||
return $this->customerCache[$customerName];
|
||||
}
|
||||
|
||||
$customer = new Customer();
|
||||
$customer->setName(sprintf($customerName, $this->dateTime));
|
||||
$customer->setComment($this->comment);
|
||||
$customer->setCountry($this->configuration->getCustomerDefaultCountry());
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $this->configuration->getCustomerDefaultTimezone()) {
|
||||
$timezone = $this->configuration->getCustomerDefaultTimezone();
|
||||
}
|
||||
$customer->setTimezone($timezone);
|
||||
|
||||
$this->customers->saveCustomer($customer);
|
||||
|
||||
$this->customerCache[$customerName] = $customer;
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
* @return bool
|
||||
* @throws InvalidFieldsException
|
||||
*/
|
||||
private function validateRow(array $row)
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
foreach (self::$requiredHeader as $headerName) {
|
||||
if (!isset($row[$headerName]) || empty($row[$headerName])) {
|
||||
$fields[] = $headerName;
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($fields)) {
|
||||
throw new InvalidFieldsException($fields);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateHeader(array $header)
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
foreach (self::$requiredHeader as $headerName) {
|
||||
if (!\in_array($headerName, $header)) {
|
||||
$fields[] = $headerName;
|
||||
}
|
||||
}
|
||||
|
||||
return empty($fields);
|
||||
}
|
||||
}
|
||||
|
||||
234
src/Command/ImportProjectCommand.php
Normal file
234
src/Command/ImportProjectCommand.php
Normal file
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Command;
|
||||
|
||||
use App\Entity\Team;
|
||||
use App\Importer\ImporterService;
|
||||
use App\Importer\ImportNotFoundException;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
use Symfony\Component\Console\Helper\ProgressBar;
|
||||
use Symfony\Component\Console\Input\InputArgument;
|
||||
use Symfony\Component\Console\Input\InputInterface;
|
||||
use Symfony\Component\Console\Input\InputOption;
|
||||
use Symfony\Component\Console\Output\OutputInterface;
|
||||
use Symfony\Component\Console\Style\SymfonyStyle;
|
||||
|
||||
/**
|
||||
* This command can change anytime, don't rely on its API for the future!
|
||||
*/
|
||||
class ImportProjectCommand extends Command
|
||||
{
|
||||
private $importerService;
|
||||
private $teams;
|
||||
private $users;
|
||||
|
||||
public function __construct(ImporterService $importerService, TeamRepository $teams, UserRepository $users)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->importerService = $importerService;
|
||||
$this->teams = $teams;
|
||||
$this->users = $users;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName('kimai:import:project')
|
||||
->setDescription('Import projects from CSV file')
|
||||
->setHelp(
|
||||
'Import projects from a CSV file, creating customers (if not existing) and optional empty teams for each project.' . PHP_EOL .
|
||||
'Imported customer will be matched by name and optionally created on the fly.' . PHP_EOL
|
||||
)
|
||||
->addArgument('file', InputArgument::REQUIRED, 'The CSV file to be imported')
|
||||
->addOption('importer', null, InputOption::VALUE_REQUIRED, 'The importer to use (supported: default)', 'default')
|
||||
->addOption('reader', null, InputOption::VALUE_REQUIRED, 'The reader to use (supported: csv, csv-semicolon)', 'csv')
|
||||
->addOption('teamlead', null, InputOption::VALUE_REQUIRED, 'If you want to create empty teams for each project, give the username of the teamlead to be assigned')
|
||||
->addOption('no-update', null, InputOption::VALUE_NONE, 'If you want to create new project, but not update existing ones')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param InputInterface $input
|
||||
* @param OutputInterface $output
|
||||
* @return int|null
|
||||
*/
|
||||
protected function execute(InputInterface $input, OutputInterface $output)
|
||||
{
|
||||
$io = new SymfonyStyle($input, $output);
|
||||
|
||||
$io->title('Kimai importer: Projects');
|
||||
|
||||
// validate teamlead
|
||||
$teamlead = $input->getOption('teamlead');
|
||||
if (null !== $teamlead) {
|
||||
$tmpUser = $this->users->findOneBy(['username' => $teamlead]);
|
||||
if ($tmpUser === null) {
|
||||
$tmpUser = $this->users->findOneBy(['email' => $teamlead]);
|
||||
if ($tmpUser === null) {
|
||||
$io->error(
|
||||
sprintf(
|
||||
'You requested to create empty teams for each project, but the given teamlead cannot be found.' . PHP_EOL .
|
||||
'Please create a user with the name (or email) %s first, before continuing.' . PHP_EOL,
|
||||
$teamlead
|
||||
)
|
||||
);
|
||||
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
$teamlead = $tmpUser;
|
||||
}
|
||||
|
||||
$skipUpdate = $input->getOption('no-update');
|
||||
$doImport = true;
|
||||
$row = 1;
|
||||
$errors = 0;
|
||||
$projects = [];
|
||||
|
||||
try {
|
||||
$importer = $this->importerService->getProjectImporter($input->getOption('importer'));
|
||||
$reader = $this->importerService->getReader($input->getOption('reader'));
|
||||
} catch (\Exception $ex) {
|
||||
$io->error($ex->getMessage());
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
$importerFile = $input->getArgument('file');
|
||||
|
||||
$io->text('Reading import file ...');
|
||||
|
||||
try {
|
||||
$records = $reader->read($importerFile);
|
||||
} catch (ImportNotFoundException $ex) {
|
||||
$io->error('File not existing or not readable: ' . $importerFile);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
$amount = iterator_count($records);
|
||||
$records->rewind();
|
||||
$io->text(sprintf('Found %s rows to process, converting now ...', $amount));
|
||||
|
||||
$progressBar = new ProgressBar($output, $amount);
|
||||
|
||||
foreach ($records as $record) {
|
||||
try {
|
||||
$projects[] = $importer->convertEntryToProject($record);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(sprintf('Invalid row %s: %s', $row, $ex->getMessage()));
|
||||
$doImport = false;
|
||||
$errors++;
|
||||
}
|
||||
$progressBar->advance();
|
||||
|
||||
$row++;
|
||||
}
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
|
||||
if (!$doImport) {
|
||||
$io->caution(sprintf('Not importing, previous %s errors need to be fixed first.', $errors));
|
||||
|
||||
return 3;
|
||||
}
|
||||
|
||||
$createdProjects = 0;
|
||||
$updatedProjects = 0;
|
||||
$noUpdatedProjects = 0;
|
||||
$createdCustomers = 0;
|
||||
$createdTeams = 0;
|
||||
|
||||
$amount = \count($projects);
|
||||
$io->text(sprintf('Converted %s projects, importing into Kimai now ...', $amount));
|
||||
|
||||
$progressBar = new ProgressBar($output, $amount);
|
||||
|
||||
foreach ($projects as $project) {
|
||||
$row++;
|
||||
$progressBar->advance();
|
||||
try {
|
||||
if ($project->getCustomer()->getId() === null) {
|
||||
$this->importerService->importCustomer($project->getCustomer());
|
||||
$createdCustomers++;
|
||||
}
|
||||
|
||||
$createTeam = false;
|
||||
|
||||
if ($project->getId() === null) {
|
||||
$this->importerService->importProject($project);
|
||||
$createdProjects++;
|
||||
$createTeam = (null !== $teamlead);
|
||||
} elseif ($skipUpdate === false) {
|
||||
$this->importerService->importProject($project);
|
||||
$updatedProjects++;
|
||||
} else {
|
||||
$noUpdatedProjects++;
|
||||
}
|
||||
|
||||
if (!$createTeam) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$team = new Team();
|
||||
$team->setName($project->getName());
|
||||
$team->setTeamLead($teamlead);
|
||||
|
||||
$this->teams->saveTeam($team);
|
||||
|
||||
$project->addTeam($team);
|
||||
$team->addProject($project);
|
||||
|
||||
$this->teams->saveTeam($team);
|
||||
$createdTeams++;
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(sprintf('Failed importing project row %s with: %s', $row, $ex->getMessage()));
|
||||
|
||||
return 4;
|
||||
}
|
||||
}
|
||||
|
||||
$progressBar->finish();
|
||||
$io->writeln('');
|
||||
$io->writeln('');
|
||||
|
||||
if ($createdCustomers === 0 && $updatedProjects === 0 && $createdProjects === 0) {
|
||||
if ($noUpdatedProjects > 0) {
|
||||
$io->success(sprintf('Skipped %s existing projects', $noUpdatedProjects));
|
||||
} else {
|
||||
$io->text('Nothing was imported');
|
||||
}
|
||||
} else {
|
||||
if ($createdCustomers > 0) {
|
||||
$io->success(sprintf('Imported %s customers', $createdCustomers));
|
||||
}
|
||||
if ($updatedProjects > 0) {
|
||||
$io->success(sprintf('Updated %s projects', $updatedProjects));
|
||||
}
|
||||
if ($noUpdatedProjects > 0) {
|
||||
$io->success(sprintf('Skipped %s existing projects', $noUpdatedProjects));
|
||||
}
|
||||
if ($createdProjects > 0) {
|
||||
$io->success(sprintf('Imported %s projects', $createdProjects));
|
||||
}
|
||||
if ($createdTeams > 0) {
|
||||
$io->success(sprintf('Created %s teams', $createdTeams));
|
||||
}
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Command;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
@@ -89,7 +89,7 @@ class ImportTimesheetCommand extends Command
|
||||
*/
|
||||
private $timesheets;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
@@ -121,7 +121,7 @@ class ImportTimesheetCommand extends Command
|
||||
*/
|
||||
private $begin = self::DEFAULT_BEGIN;
|
||||
|
||||
public function __construct(CustomerRepository $customers, ProjectRepository $projects, ActivityRepository $activities, UserRepository $users, TagRepository $tagRepository, TimesheetRepository $timesheets, FormConfiguration $configuration)
|
||||
public function __construct(CustomerRepository $customers, ProjectRepository $projects, ActivityRepository $activities, UserRepository $users, TagRepository $tagRepository, TimesheetRepository $timesheets, SystemConfiguration $configuration)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->customers = $customers;
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Configuration;
|
||||
|
||||
/**
|
||||
* @internal will be deprecated soon, use SystemConfiguration instead
|
||||
* @deprecated will be removed with 2.0, use SystemConfiguration instead
|
||||
*/
|
||||
class FormConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
|
||||
@@ -82,4 +82,39 @@ class SystemConfiguration implements SystemBundleConfiguration
|
||||
{
|
||||
return (string) $this->find('calendar.slot_duration');
|
||||
}
|
||||
|
||||
public function getCustomerDefaultTimezone(): ?string
|
||||
{
|
||||
return $this->find('defaults.customer.timezone');
|
||||
}
|
||||
|
||||
public function getCustomerDefaultCurrency(): string
|
||||
{
|
||||
return $this->find('defaults.customer.currency');
|
||||
}
|
||||
|
||||
public function getCustomerDefaultCountry(): string
|
||||
{
|
||||
return $this->find('defaults.customer.country');
|
||||
}
|
||||
|
||||
public function getUserDefaultTimezone(): ?string
|
||||
{
|
||||
return $this->find('defaults.user.timezone');
|
||||
}
|
||||
|
||||
public function getUserDefaultTheme(): ?string
|
||||
{
|
||||
return $this->find('defaults.user.theme');
|
||||
}
|
||||
|
||||
public function getUserDefaultLanguage(): string
|
||||
{
|
||||
return $this->find('defaults.user.language');
|
||||
}
|
||||
|
||||
public function getUserDefaultCurrency(): string
|
||||
{
|
||||
return $this->find('defaults.user.currency');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\ActivityRate;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
@@ -51,7 +51,7 @@ final class ActivityController extends AbstractController
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
@@ -59,7 +59,7 @@ final class ActivityController extends AbstractController
|
||||
*/
|
||||
private $dispatcher;
|
||||
|
||||
public function __construct(ActivityRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
public function __construct(ActivityRepository $repository, SystemConfiguration $configuration, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerComment;
|
||||
use App\Entity\CustomerRate;
|
||||
@@ -109,7 +109,7 @@ final class CustomerController extends AbstractController
|
||||
* @Route(path="/create", name="admin_customer_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_customer')")
|
||||
*/
|
||||
public function createAction(Request $request, FormConfiguration $configuration)
|
||||
public function createAction(Request $request, SystemConfiguration $configuration)
|
||||
{
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $configuration->getCustomerDefaultTimezone()) {
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\MetaTableTypeInterface;
|
||||
use App\Entity\Project;
|
||||
@@ -57,7 +57,7 @@ final class ProjectController extends AbstractController
|
||||
*/
|
||||
private $repository;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
@@ -69,7 +69,7 @@ final class ProjectController extends AbstractController
|
||||
*/
|
||||
private $projectService;
|
||||
|
||||
public function __construct(ProjectRepository $repository, FormConfiguration $configuration, EventDispatcherInterface $dispatcher, ProjectService $projectService)
|
||||
public function __construct(ProjectRepository $repository, SystemConfiguration $configuration, EventDispatcherInterface $dispatcher, ProjectService $projectService)
|
||||
{
|
||||
$this->repository = $repository;
|
||||
$this->configuration = $configuration;
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Event\UserPreferenceDisplayEvent;
|
||||
use App\Export\Spreadsheet\UserExporter;
|
||||
@@ -97,7 +97,7 @@ final class UserController extends AbstractController
|
||||
]);
|
||||
}
|
||||
|
||||
private function createNewDefaultUser(FormConfiguration $config): User
|
||||
private function createNewDefaultUser(SystemConfiguration $config): User
|
||||
{
|
||||
$user = new User();
|
||||
$user->setEnabled(true);
|
||||
@@ -112,7 +112,7 @@ final class UserController extends AbstractController
|
||||
* @Route(path="/create", name="admin_user_create", methods={"GET", "POST"})
|
||||
* @Security("is_granted('create_user')")
|
||||
*/
|
||||
public function createAction(Request $request, FormConfiguration $config): Response
|
||||
public function createAction(Request $request, SystemConfiguration $config): Response
|
||||
{
|
||||
$user = $this->createNewDefaultUser($config);
|
||||
$editForm = $this->getCreateUserForm($user);
|
||||
|
||||
112
src/Customer/CustomerService.php
Normal file
112
src/Customer/CustomerService.php
Normal file
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Customer;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\Customer;
|
||||
use App\Event\CustomerCreateEvent;
|
||||
use App\Event\CustomerCreatePostEvent;
|
||||
use App\Event\CustomerCreatePreEvent;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Event\CustomerUpdatePostEvent;
|
||||
use App\Event\CustomerUpdatePreEvent;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
class CustomerService
|
||||
{
|
||||
private $repository;
|
||||
private $dispatcher;
|
||||
private $validator;
|
||||
private $configuration;
|
||||
|
||||
public function __construct(CustomerRepository $customerRepository, SystemConfiguration $configuration, ValidatorInterface $validator, EventDispatcherInterface $dispatcher)
|
||||
{
|
||||
$this->repository = $customerRepository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->validator = $validator;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
private function getDefaultTimezone(): string
|
||||
{
|
||||
if (null === ($timezone = $this->configuration->getCustomerDefaultTimezone())) {
|
||||
$timezone = date_default_timezone_get();
|
||||
}
|
||||
|
||||
return $timezone;
|
||||
}
|
||||
|
||||
public function createNewCustomer(): Customer
|
||||
{
|
||||
$customer = new Customer();
|
||||
$customer->setTimezone($this->getDefaultTimezone());
|
||||
$customer->setCountry($this->configuration->getCustomerDefaultCountry());
|
||||
$customer->setCurrency($this->configuration->getCustomerDefaultCurrency());
|
||||
|
||||
$this->dispatcher->dispatch(new CustomerMetaDefinitionEvent($customer));
|
||||
$this->dispatcher->dispatch(new CustomerCreateEvent($customer));
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
public function saveNewCustomer(Customer $customer): Customer
|
||||
{
|
||||
if (null !== $customer->getId()) {
|
||||
throw new InvalidArgumentException('Cannot create customer, already persisted');
|
||||
}
|
||||
|
||||
$this->validateCustomer($customer);
|
||||
|
||||
$this->dispatcher->dispatch(new CustomerCreatePreEvent($customer));
|
||||
$this->repository->saveCustomer($customer);
|
||||
$this->dispatcher->dispatch(new CustomerCreatePostEvent($customer));
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param Customer $customer
|
||||
* @param string[] $groups
|
||||
* @throws ValidationFailedException
|
||||
*/
|
||||
private function validateCustomer(Customer $customer, array $groups = []): void
|
||||
{
|
||||
$errors = $this->validator->validate($customer, null, $groups);
|
||||
|
||||
if ($errors->count() > 0) {
|
||||
throw new ValidationFailedException($errors, 'Validation Failed');
|
||||
}
|
||||
}
|
||||
|
||||
public function updateCustomer(Customer $customer): Customer
|
||||
{
|
||||
$this->validateCustomer($customer);
|
||||
|
||||
$this->dispatcher->dispatch(new CustomerUpdatePreEvent($customer));
|
||||
$this->repository->saveCustomer($customer);
|
||||
$this->dispatcher->dispatch(new CustomerUpdatePostEvent($customer));
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
public function findCustomerByName(string $name): ?Customer
|
||||
{
|
||||
return $this->repository->findOneBy(['name' => $name]);
|
||||
}
|
||||
|
||||
public function findCustomerByNumber(string $number): ?Customer
|
||||
{
|
||||
return $this->repository->findOneBy(['number' => $number]);
|
||||
}
|
||||
}
|
||||
@@ -69,7 +69,7 @@ class CustomerFixtures extends Fixture
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
$manager->clear(Activity::class);
|
||||
}
|
||||
|
||||
$amountGlobalActivities = rand(self::MIN_GLOBAL_ACTIVITIES, self::MAX_GLOBAL_ACTIVITIES);
|
||||
@@ -80,7 +80,9 @@ class CustomerFixtures extends Fixture
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
$manager->clear(Activity::class);
|
||||
$manager->clear(Project::class);
|
||||
$manager->clear(Customer::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -32,9 +32,6 @@ class TeamFixtures extends Fixture implements DependentFixtureInterface
|
||||
public const MAX_USERS_PER_TEAM = 15;
|
||||
public const MAX_PROJECTS_PER_TEAM = 5;
|
||||
|
||||
// lower batch size, as user preferences are added in the same run
|
||||
public const BATCH_SIZE = 50;
|
||||
|
||||
/**
|
||||
* @return class-string[]
|
||||
*/
|
||||
@@ -127,11 +124,6 @@ class TeamFixtures extends Fixture implements DependentFixtureInterface
|
||||
}
|
||||
|
||||
$manager->persist($team);
|
||||
|
||||
if ($i % self::BATCH_SIZE === 0) {
|
||||
$manager->flush();
|
||||
$manager->clear(Team::class);
|
||||
}
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
|
||||
@@ -40,9 +40,11 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
public const TIMERANGE_RUNNING = 1047; // in minutes = 17:45 hours
|
||||
public const MIN_MINUTES_PER_ENTRY = 15;
|
||||
public const MAX_MINUTES_PER_ENTRY = 840; // 14h
|
||||
public const MAX_TAG_PER_ENTRY = 3;
|
||||
public const MAX_DESCRIPTION_LENGTH = 500;
|
||||
|
||||
public const ADD_TAGS_MAX_ENTRIES = 10000;
|
||||
public const MAX_TAG_PER_ENTRY = 3;
|
||||
|
||||
public const BATCH_SIZE = 100;
|
||||
|
||||
/**
|
||||
@@ -117,6 +119,8 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
null,
|
||||
false
|
||||
);
|
||||
|
||||
$all++;
|
||||
$manager->persist($entry);
|
||||
}
|
||||
|
||||
@@ -125,13 +129,16 @@ class TimesheetFixtures extends Fixture implements DependentFixtureInterface
|
||||
}
|
||||
$manager->flush();
|
||||
|
||||
$entries = $manager->getRepository(Timesheet::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$tagAmount = rand(0, self::MAX_TAG_PER_ENTRY);
|
||||
for ($iTag = 0; $iTag < $tagAmount; $iTag++) {
|
||||
$tagId = rand(1, TagFixtures::MAX_TAGS);
|
||||
if (isset($allTags[$tagId])) {
|
||||
$temp->addTag($allTags[$tagId]);
|
||||
// TODO this breaks if too many records need to be loaded: find a better way of adding tags
|
||||
if ($all < self::ADD_TAGS_MAX_ENTRIES) {
|
||||
$entries = $manager->getRepository(Timesheet::class)->findAll();
|
||||
foreach ($entries as $temp) {
|
||||
$tagAmount = rand(0, self::MAX_TAG_PER_ENTRY);
|
||||
for ($iTag = 0; $iTag < $tagAmount; $iTag++) {
|
||||
$tagId = rand(1, TagFixtures::MAX_TAGS);
|
||||
if (isset($allTags[$tagId])) {
|
||||
$temp->addTag($allTags[$tagId]);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -41,9 +41,6 @@ class UserFixtures extends Fixture
|
||||
public const MIN_RATE = 30;
|
||||
public const MAX_RATE = 120;
|
||||
|
||||
// lower batch size, as user preferences are added in the same run
|
||||
public const BATCH_SIZE = 50;
|
||||
|
||||
/**
|
||||
* @var UserPasswordEncoderInterface
|
||||
*/
|
||||
@@ -88,14 +85,18 @@ class UserFixtures extends Fixture
|
||||
->setEnabled($userData[6])
|
||||
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
|
||||
->setApiToken($passwordEncoder->encodePassword($user, self::DEFAULT_API_TOKEN))
|
||||
->setPreferences($this->getUserPreferences($user, $userData[7]))
|
||||
;
|
||||
|
||||
$manager->persist($user);
|
||||
|
||||
$prefs = $this->getUserPreferences($user, $userData[7]);
|
||||
$user->setPreferences($prefs);
|
||||
$manager->persist($prefs[0]);
|
||||
$manager->persist($prefs[1]);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
$manager->clear(User::class);
|
||||
$manager->clear(UserPreference::class);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -160,19 +161,17 @@ class UserFixtures extends Fixture
|
||||
->setRoles([User::ROLE_USER])
|
||||
->setEnabled(true)
|
||||
->setPassword($passwordEncoder->encodePassword($user, self::DEFAULT_PASSWORD))
|
||||
->setPreferences($this->getUserPreferences($user))
|
||||
;
|
||||
|
||||
$manager->persist($user);
|
||||
|
||||
if ($i % self::BATCH_SIZE === 0) {
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
}
|
||||
$prefs = $this->getUserPreferences($user);
|
||||
$user->setPreferences($prefs);
|
||||
$manager->persist($prefs[0]);
|
||||
}
|
||||
|
||||
$manager->flush();
|
||||
$manager->clear();
|
||||
$manager->clear(User::class);
|
||||
$manager->clear(UserPreference::class);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -214,15 +214,21 @@ trait MetaTableTypeTrait
|
||||
public function merge(MetaTableTypeInterface $meta): MetaTableTypeInterface
|
||||
{
|
||||
$this
|
||||
->setType($meta->getType())
|
||||
->setConstraints($meta->getConstraints())
|
||||
->setIsRequired($meta->isRequired())
|
||||
->setIsVisible($meta->isVisible())
|
||||
->setLabel($meta->getLabel())
|
||||
->setOptions($meta->getOptions())
|
||||
->setOrder($meta->getOrder())
|
||||
;
|
||||
|
||||
if ($meta->getLabel() !== null) {
|
||||
$this->setLabel($meta->getLabel());
|
||||
}
|
||||
|
||||
if ($meta->getType() !== null) {
|
||||
$this->setType($meta->getType());
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
|
||||
34
src/Event/AbstractCustomerEvent.php
Normal file
34
src/Event/AbstractCustomerEvent.php
Normal file
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
/**
|
||||
* Base event class to used with customer manipulations.
|
||||
*/
|
||||
abstract class AbstractCustomerEvent extends Event
|
||||
{
|
||||
/**
|
||||
* @var Customer
|
||||
*/
|
||||
private $customer;
|
||||
|
||||
public function __construct(Customer $customer)
|
||||
{
|
||||
$this->customer = $customer;
|
||||
}
|
||||
|
||||
public function getCustomer(): Customer
|
||||
{
|
||||
return $this->customer;
|
||||
}
|
||||
}
|
||||
17
src/Event/CustomerCreateEvent.php
Normal file
17
src/Event/CustomerCreateEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for new customer instances, which might or might not be saved.
|
||||
*/
|
||||
final class CustomerCreateEvent extends AbstractCustomerEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/CustomerCreatePostEvent.php
Normal file
17
src/Event/CustomerCreatePostEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for customer instances, which were just saved.
|
||||
*/
|
||||
final class CustomerCreatePostEvent extends AbstractCustomerEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/CustomerCreatePreEvent.php
Normal file
17
src/Event/CustomerCreatePreEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for customer instances, which are just about to being saved.
|
||||
*/
|
||||
final class CustomerCreatePreEvent extends AbstractCustomerEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/CustomerUpdatePostEvent.php
Normal file
17
src/Event/CustomerUpdatePostEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for customer instances, which were just updated.
|
||||
*/
|
||||
final class CustomerUpdatePostEvent extends AbstractCustomerEvent
|
||||
{
|
||||
}
|
||||
17
src/Event/CustomerUpdatePreEvent.php
Normal file
17
src/Event/CustomerUpdatePreEvent.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Event;
|
||||
|
||||
/**
|
||||
* Triggered for customer instances, which are just about to being updated.
|
||||
*/
|
||||
final class CustomerUpdatePreEvent extends AbstractCustomerEvent
|
||||
{
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\EventSubscriber;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
@@ -39,15 +39,15 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
*/
|
||||
protected $voter;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
* @var SystemConfiguration
|
||||
*/
|
||||
protected $formConfig;
|
||||
protected $configuration;
|
||||
|
||||
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter, FormConfiguration $formConfig)
|
||||
public function __construct(EventDispatcherInterface $dispatcher, AuthorizationCheckerInterface $voter, SystemConfiguration $formConfig)
|
||||
{
|
||||
$this->eventDispatcher = $dispatcher;
|
||||
$this->voter = $voter;
|
||||
$this->formConfig = $formConfig;
|
||||
$this->configuration = $formConfig;
|
||||
}
|
||||
|
||||
public static function getSubscribedEvents(): array
|
||||
@@ -59,22 +59,22 @@ class UserPreferenceSubscriber implements EventSubscriberInterface
|
||||
|
||||
private function getDefaultTheme(): ?string
|
||||
{
|
||||
return $this->formConfig->getUserDefaultTheme();
|
||||
return $this->configuration->getUserDefaultTheme();
|
||||
}
|
||||
|
||||
private function getDefaultCurrency(): string
|
||||
{
|
||||
return $this->formConfig->getUserDefaultCurrency();
|
||||
return $this->configuration->getUserDefaultCurrency();
|
||||
}
|
||||
|
||||
private function getDefaultLanguage(): string
|
||||
{
|
||||
return $this->formConfig->getUserDefaultLanguage();
|
||||
return $this->configuration->getUserDefaultLanguage();
|
||||
}
|
||||
|
||||
private function getDefaultTimezone(): string
|
||||
{
|
||||
$timezone = $this->formConfig->getUserDefaultTimezone();
|
||||
$timezone = $this->configuration->getUserDefaultTimezone();
|
||||
if (null === $timezone) {
|
||||
$timezone = date_default_timezone_get();
|
||||
}
|
||||
|
||||
121
src/Importer/AbstractCustomerImporter.php
Normal file
121
src/Importer/AbstractCustomerImporter.php
Normal file
@@ -0,0 +1,121 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use App\Customer\CustomerService;
|
||||
use App\Entity\Customer;
|
||||
|
||||
abstract class AbstractCustomerImporter implements CustomerImporterInterface
|
||||
{
|
||||
private $customerService;
|
||||
|
||||
public function __construct(CustomerService $repository)
|
||||
{
|
||||
$this->customerService = $repository;
|
||||
}
|
||||
|
||||
protected function findCustomerByName(string $name): ?Customer
|
||||
{
|
||||
return $this->customerService->findCustomerByName($name);
|
||||
}
|
||||
|
||||
protected function findCustomerByNumber(string $number): ?Customer
|
||||
{
|
||||
return $this->customerService->findCustomerByNumber($number);
|
||||
}
|
||||
|
||||
public function convertEntryToCustomer(array $entry): Customer
|
||||
{
|
||||
$customer = $this->findCustomer($entry);
|
||||
|
||||
$this->mapEntryToCustomer($customer, $entry);
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
protected function createNewCustomer(string $name): Customer
|
||||
{
|
||||
$customer = $this->customerService->createNewCustomer();
|
||||
$customer->setName(substr($name, 0, 149));
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
protected function findCustomer(array $entry): ?Customer
|
||||
{
|
||||
$name = $this->findCustomerName($entry);
|
||||
$customer = $this->findCustomerByName($name);
|
||||
|
||||
if ($customer === null) {
|
||||
$number = $this->findCustomerNumber($entry);
|
||||
if ($number !== null) {
|
||||
$customer = $this->findCustomerByNumber($number);
|
||||
}
|
||||
}
|
||||
|
||||
if ($customer === null) {
|
||||
$customer = $this->createNewCustomer($name);
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the unique customer name inside $entry.
|
||||
*
|
||||
* @param array $entry
|
||||
* @return string
|
||||
* @throws UnsupportedFormatException
|
||||
*/
|
||||
protected function findCustomerName(array $entry): string
|
||||
{
|
||||
foreach ($entry as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'name':
|
||||
if (!empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedFormatException('Missing customer name, expected in column: "Name"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the unique customer number inside $entry.
|
||||
*
|
||||
* @param array $entry
|
||||
* @return string
|
||||
* @throws UnsupportedFormatException
|
||||
*/
|
||||
protected function findCustomerNumber(array $entry): ?string
|
||||
{
|
||||
foreach ($entry as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'number':
|
||||
case 'account':
|
||||
if (!empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all supported values from $entry to $customer.
|
||||
*
|
||||
* @param Customer $customer
|
||||
* @param array $entry
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function mapEntryToCustomer(Customer $customer, array $entry);
|
||||
}
|
||||
158
src/Importer/AbstractProjectImporter.php
Normal file
158
src/Importer/AbstractProjectImporter.php
Normal file
@@ -0,0 +1,158 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use App\Customer\CustomerService;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Project\ProjectService;
|
||||
|
||||
abstract class AbstractProjectImporter implements ProjectImporterInterface
|
||||
{
|
||||
private $projectService;
|
||||
private $customerService;
|
||||
/**
|
||||
* @var Customer[]
|
||||
*/
|
||||
private $customerCache = [];
|
||||
|
||||
public function __construct(ProjectService $projectService, CustomerService $customerService)
|
||||
{
|
||||
$this->projectService = $projectService;
|
||||
$this->customerService = $customerService;
|
||||
}
|
||||
|
||||
protected function findProjectByName(string $name): ?Project
|
||||
{
|
||||
return $this->projectService->findProjectByName($name);
|
||||
}
|
||||
|
||||
protected function findCustomerByName(string $name): ?Customer
|
||||
{
|
||||
return $this->customerService->findCustomerByName($name);
|
||||
}
|
||||
|
||||
public function convertEntryToProject(array $entry): Project
|
||||
{
|
||||
$project = $this->findProject($entry);
|
||||
|
||||
$this->convertEntry($project, $entry);
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
protected function createNewProject(Customer $customer, string $name): Project
|
||||
{
|
||||
$project = $this->projectService->createNewProject($customer);
|
||||
$project->setName($name);
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
protected function findProject(array $entry): ?Project
|
||||
{
|
||||
$name = $this->findCustomerName($entry);
|
||||
$customer = $this->findCustomer($name);
|
||||
|
||||
$name = $this->findProjectName($entry);
|
||||
$project = $this->findProjectByName($name);
|
||||
|
||||
if ($project === null) {
|
||||
$project = $this->createNewProject($customer, $name);
|
||||
}
|
||||
|
||||
if ($customer->getId() !== $project->getCustomer()->getId()) {
|
||||
throw new \InvalidArgumentException(
|
||||
sprintf(
|
||||
'Customer mismatch for project %s with attached customer %s and new customer %s',
|
||||
$project->getName(),
|
||||
$project->getCustomer()->getName(),
|
||||
$customer->getName()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
private function findCustomer(string $customerName): Customer
|
||||
{
|
||||
if (!\array_key_exists($customerName, $this->customerCache)) {
|
||||
$customer = $this->findCustomerByName($customerName);
|
||||
|
||||
if ($customer === null) {
|
||||
$customer = $this->customerService->createNewCustomer();
|
||||
$customer->setName($customerName);
|
||||
}
|
||||
|
||||
$this->customerCache[$customerName] = $customer;
|
||||
}
|
||||
|
||||
return $this->customerCache[$customerName];
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the unique project name inside $entry.
|
||||
*
|
||||
* @param array $entry
|
||||
* @return string
|
||||
* @throws UnsupportedFormatException
|
||||
*/
|
||||
protected function findProjectName(array $entry): string
|
||||
{
|
||||
foreach ($entry as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'project':
|
||||
case 'projectname':
|
||||
case 'project name':
|
||||
case 'project-name':
|
||||
case 'name':
|
||||
if (!empty($value)) {
|
||||
return substr($value, 0, 149);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedFormatException('Missing project name, expected in one of the columns: "Name", "Project , "Project Name", "Project-Name", "ProjectName"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Find the unique project name inside $entry.
|
||||
*
|
||||
* @param array $entry
|
||||
* @return string
|
||||
* @throws UnsupportedFormatException
|
||||
*/
|
||||
protected function findCustomerName(array $entry): string
|
||||
{
|
||||
foreach ($entry as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'customer':
|
||||
case 'customername':
|
||||
case 'customer-name':
|
||||
case 'customer name':
|
||||
if (!empty($value)) {
|
||||
return substr($value, 0, 149);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedFormatException('Missing customer name, expected in one of the columns: "Customer", "Customer Name", "Customer-Name" or "CustomerName"');
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies all supported values from $entry to $project.
|
||||
*
|
||||
* @param Project $project
|
||||
* @param array $entry
|
||||
* @return mixed
|
||||
*/
|
||||
abstract protected function convertEntry(Project $project, array $entry);
|
||||
}
|
||||
35
src/Importer/CsvReader.php
Normal file
35
src/Importer/CsvReader.php
Normal file
@@ -0,0 +1,35 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use League\Csv\Reader;
|
||||
|
||||
final class CsvReader implements ImportReaderInterface
|
||||
{
|
||||
private $delimiter;
|
||||
|
||||
public function __construct(string $delimiter)
|
||||
{
|
||||
$this->delimiter = $delimiter;
|
||||
}
|
||||
|
||||
public function read(string $input): \Iterator
|
||||
{
|
||||
if (!is_readable($input)) {
|
||||
throw new ImportNotFoundException();
|
||||
}
|
||||
|
||||
$csv = Reader::createFromPath($input, 'r');
|
||||
$csv->setDelimiter($this->delimiter);
|
||||
$csv->setHeaderOffset(0);
|
||||
|
||||
return $csv->getRecords();
|
||||
}
|
||||
}
|
||||
17
src/Importer/CustomerImporterInterface.php
Normal file
17
src/Importer/CustomerImporterInterface.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use App\Entity\Customer;
|
||||
|
||||
interface CustomerImporterInterface
|
||||
{
|
||||
public function convertEntryToCustomer(array $entry): Customer;
|
||||
}
|
||||
162
src/Importer/DefaultCustomerImporter.php
Normal file
162
src/Importer/DefaultCustomerImporter.php
Normal file
@@ -0,0 +1,162 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\CustomerMeta;
|
||||
|
||||
final class DefaultCustomerImporter extends AbstractCustomerImporter
|
||||
{
|
||||
protected function mapEntryToCustomer(Customer $customer, array $row)
|
||||
{
|
||||
foreach ($row as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'name':
|
||||
$customer->setName(substr($value, 0, 149));
|
||||
if (empty($customer->getCompany())) {
|
||||
$customer->setCompany($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'company':
|
||||
case 'company-name':
|
||||
case 'company name':
|
||||
$customer->setCompany($value);
|
||||
break;
|
||||
|
||||
case 'email':
|
||||
case 'e-mail':
|
||||
case 'e mail':
|
||||
if (!empty($value)) {
|
||||
$customer->setEmail($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'country':
|
||||
if (!empty($value)) {
|
||||
$customer->setCountry($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'number':
|
||||
case 'account':
|
||||
case 'customer number':
|
||||
case 'customer account':
|
||||
if (!empty($value)) {
|
||||
$customer->setNumber($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'vat':
|
||||
case 'vat-id':
|
||||
case 'vat id':
|
||||
case 'tax-id':
|
||||
case 'tax id':
|
||||
if (!empty($value)) {
|
||||
$customer->setVatId($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'comment':
|
||||
case 'description':
|
||||
if (!empty($value)) {
|
||||
$customer->setComment($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'address':
|
||||
if (!empty($value)) {
|
||||
$customer->setAddress($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'contact':
|
||||
if (!empty($value)) {
|
||||
$customer->setContact($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'currency':
|
||||
if (!empty($value)) {
|
||||
$customer->setCurrency($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'timezone':
|
||||
if (!empty($value)) {
|
||||
$customer->setTimezone($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'phone':
|
||||
if (!empty($value)) {
|
||||
$customer->setPhone($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'mobile':
|
||||
if (!empty($value)) {
|
||||
$customer->setMobile($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'fax':
|
||||
if (!empty($value)) {
|
||||
$customer->setFax($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'homepage':
|
||||
if (!empty($value)) {
|
||||
$customer->setHomepage($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'color':
|
||||
if (!empty($value)) {
|
||||
$customer->setColor($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'visible':
|
||||
if ($value !== '') {
|
||||
$customer->setVisible((bool) $value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'budget':
|
||||
if (!empty($value)) {
|
||||
$customer->setBudget($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'time budget':
|
||||
case 'time-budget':
|
||||
if (!empty($value)) {
|
||||
$customer->setTimeBudget($value);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (stripos($name, 'meta.') === 0) {
|
||||
$tmpName = str_replace('meta.', '', $name);
|
||||
$meta = new CustomerMeta();
|
||||
$meta->setIsVisible(true);
|
||||
$meta->setName($tmpName);
|
||||
$meta->setValue($value);
|
||||
$customer->setMetaField($meta);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
}
|
||||
90
src/Importer/DefaultProjectImporter.php
Normal file
90
src/Importer/DefaultProjectImporter.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use App\Entity\Project;
|
||||
use App\Entity\ProjectMeta;
|
||||
|
||||
final class DefaultProjectImporter extends AbstractProjectImporter
|
||||
{
|
||||
protected function convertEntry(Project $project, array $row)
|
||||
{
|
||||
foreach ($row as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'name':
|
||||
$project->setName(substr($value, 0, 149));
|
||||
break;
|
||||
|
||||
case 'comment':
|
||||
case 'description':
|
||||
if (!empty($value)) {
|
||||
$project->setComment($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'ordernumber':
|
||||
case 'order-number':
|
||||
case 'order number':
|
||||
if (!empty($value)) {
|
||||
$project->setOrderNumber($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'orderdate':
|
||||
case 'order-date':
|
||||
case 'order date':
|
||||
if (!empty($value)) {
|
||||
$timezone = $project->getCustomer()->getTimezone();
|
||||
$timezone = new \DateTimeZone($timezone ?? date_default_timezone_get());
|
||||
$project->setOrderDate(new \DateTime($value, $timezone));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'color':
|
||||
if (!empty($value)) {
|
||||
$project->setColor($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'budget':
|
||||
if (!empty($value)) {
|
||||
$project->setBudget($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'time budget':
|
||||
case 'time-budget':
|
||||
if (!empty($value)) {
|
||||
$project->setTimeBudget($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'visible':
|
||||
if ($value !== '') {
|
||||
$project->setVisible((bool) $value);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
if (stripos($name, 'meta.') === 0) {
|
||||
$tmpName = str_replace('meta.', '', $name);
|
||||
$meta = new ProjectMeta();
|
||||
$meta->setIsVisible(true);
|
||||
$meta->setName($tmpName);
|
||||
$meta->setValue($value);
|
||||
$project->setMetaField($meta);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $project;
|
||||
}
|
||||
}
|
||||
159
src/Importer/GrandtotalCustomerImporter.php
Normal file
159
src/Importer/GrandtotalCustomerImporter.php
Normal file
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use App\Entity\Customer;
|
||||
|
||||
final class GrandtotalCustomerImporter extends AbstractCustomerImporter
|
||||
{
|
||||
protected function findCustomerName(array $row): string
|
||||
{
|
||||
foreach ($row as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'organization':
|
||||
case 'firma':
|
||||
if (!empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new UnsupportedFormatException('Missing customer name, expected in one of the columns: "Organization", "Firma"');
|
||||
}
|
||||
|
||||
protected function findCustomerNumber(array $row): ?string
|
||||
{
|
||||
foreach ($row as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'customer number':
|
||||
case 'kundennummer':
|
||||
if (!empty($value)) {
|
||||
return $value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
protected function mapEntryToCustomer(Customer $customer, array $row)
|
||||
{
|
||||
$names = ['first' => '', 'middle' => '', 'last' => '', 'title' => ''];
|
||||
$address = ['street' => '', 'city' => '', 'code' => ''];
|
||||
|
||||
foreach ($row as $name => $value) {
|
||||
switch (strtolower($name)) {
|
||||
case 'department':
|
||||
case 'abteilung':
|
||||
|
||||
case 'salutation':
|
||||
case 'briefanrede':
|
||||
|
||||
case 'state':
|
||||
case 'bundesland':
|
||||
|
||||
case 'iban':
|
||||
case 'bic':
|
||||
|
||||
case 'sepa mandate id':
|
||||
case 'sepa mandat':
|
||||
// not supported in Kimai
|
||||
break;
|
||||
|
||||
case 'organization':
|
||||
case 'firma':
|
||||
$customer->setCompany($value);
|
||||
break;
|
||||
|
||||
case 'e-mail':
|
||||
if (!empty($value)) {
|
||||
$customer->setEmail($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'country':
|
||||
case 'land':
|
||||
if (!empty($value)) {
|
||||
$customer->setCountry($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'customer number':
|
||||
case 'kundennummer':
|
||||
if (!empty($value)) {
|
||||
$customer->setNumber($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'tax-id':
|
||||
case 'umsatzsteuer':
|
||||
if (!empty($value)) {
|
||||
$customer->setVatId($value);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'note':
|
||||
case 'notiz':
|
||||
if (!empty($value)) {
|
||||
$customer->setComment(strip_tags($value));
|
||||
}
|
||||
break;
|
||||
|
||||
case 'title':
|
||||
case 'titel':
|
||||
$names['title'] = $value;
|
||||
break;
|
||||
|
||||
case 'first name':
|
||||
case 'vorname':
|
||||
$names['first'] = $value;
|
||||
break;
|
||||
|
||||
case 'middle name':
|
||||
case 'zweiter vorname':
|
||||
$names['middle'] = $value;
|
||||
break;
|
||||
|
||||
case 'last name':
|
||||
case 'nachname':
|
||||
$names['last'] = $value;
|
||||
break;
|
||||
|
||||
case 'street':
|
||||
case 'straße':
|
||||
$address['street'] = $value;
|
||||
break;
|
||||
|
||||
case 'zip':
|
||||
case 'plz':
|
||||
$address['code'] = $value;
|
||||
break;
|
||||
|
||||
case 'city':
|
||||
case 'ort':
|
||||
$address['city'] = $value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$calculatedAddress = trim($address['street'] . PHP_EOL . $address['code'] . ' ' . $address['city']);
|
||||
$calculatedContact = trim(str_replace(' ', ' ', $names['title'] . ' ' . $names['first'] . ' ' . $names['middle'] . ' ' . $names['last']));
|
||||
|
||||
if (!empty($calculatedAddress)) {
|
||||
$customer->setAddress($calculatedAddress);
|
||||
}
|
||||
|
||||
if (!empty($calculatedContact)) {
|
||||
$customer->setContact($calculatedContact);
|
||||
}
|
||||
|
||||
return $customer;
|
||||
}
|
||||
}
|
||||
14
src/Importer/ImportNotFoundException.php
Normal file
14
src/Importer/ImportNotFoundException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
final class ImportNotFoundException extends \Exception
|
||||
{
|
||||
}
|
||||
14
src/Importer/ImportNotReadableException.php
Normal file
14
src/Importer/ImportNotReadableException.php
Normal file
@@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
final class ImportNotReadableException extends \Exception
|
||||
{
|
||||
}
|
||||
20
src/Importer/ImportReaderInterface.php
Normal file
20
src/Importer/ImportReaderInterface.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
interface ImportReaderInterface
|
||||
{
|
||||
/**
|
||||
* @param string $input
|
||||
* @return \Iterator
|
||||
* @throws ImportNotFoundException
|
||||
*/
|
||||
public function read(string $input): \Iterator;
|
||||
}
|
||||
102
src/Importer/ImporterService.php
Normal file
102
src/Importer/ImporterService.php
Normal file
@@ -0,0 +1,102 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use App\Customer\CustomerService;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Project\ProjectService;
|
||||
|
||||
class ImporterService
|
||||
{
|
||||
private $customers;
|
||||
private $projects;
|
||||
/**
|
||||
* @var ProjectImporterInterface[]
|
||||
*/
|
||||
private $projectImporter = [];
|
||||
/**
|
||||
* @var CustomerImporterInterface[]
|
||||
*/
|
||||
private $customerImporter = [];
|
||||
|
||||
public function __construct(CustomerService $customers, ProjectService $projects)
|
||||
{
|
||||
$this->customers = $customers;
|
||||
$this->projects = $projects;
|
||||
}
|
||||
|
||||
public function registerProjectImporter(string $name, ProjectImporterInterface $importer): void
|
||||
{
|
||||
$this->projectImporter[$name] = $importer;
|
||||
}
|
||||
|
||||
public function registerCustomerImporter(string $name, CustomerImporterInterface $importer): void
|
||||
{
|
||||
$this->customerImporter[$name] = $importer;
|
||||
}
|
||||
|
||||
public function getProjectImporter(string $name): ProjectImporterInterface
|
||||
{
|
||||
if (!\array_key_exists('default', $this->projectImporter)) {
|
||||
$this->registerProjectImporter('default', new DefaultProjectImporter($this->projects, $this->customers));
|
||||
}
|
||||
|
||||
if (!\array_key_exists($name, $this->projectImporter)) {
|
||||
throw new \InvalidArgumentException('Unknown project importer: ' . $name);
|
||||
}
|
||||
|
||||
return $this->projectImporter[$name];
|
||||
}
|
||||
|
||||
public function getCustomerImporter(string $name): CustomerImporterInterface
|
||||
{
|
||||
if (!\array_key_exists('default', $this->customerImporter)) {
|
||||
$this->registerCustomerImporter('default', new DefaultCustomerImporter($this->customers));
|
||||
$this->registerCustomerImporter('grandtotal', new GrandtotalCustomerImporter($this->customers));
|
||||
}
|
||||
|
||||
if (!\array_key_exists($name, $this->customerImporter)) {
|
||||
throw new \InvalidArgumentException('Unknown customer importer: ' . $name);
|
||||
}
|
||||
|
||||
return $this->customerImporter[$name];
|
||||
}
|
||||
|
||||
public function importProject(Project $project): void
|
||||
{
|
||||
if ($project->getId() === null) {
|
||||
$this->projects->saveNewProject($project);
|
||||
} else {
|
||||
$this->projects->updateProject($project);
|
||||
}
|
||||
}
|
||||
|
||||
public function getReader(string $name): ImportReaderInterface
|
||||
{
|
||||
switch ($name) {
|
||||
case 'csv':
|
||||
return new CsvReader(',');
|
||||
case 'csv-semicolon':
|
||||
return new CsvReader(';');
|
||||
}
|
||||
|
||||
throw new \InvalidArgumentException('Unknown import reader: ' . $name);
|
||||
}
|
||||
|
||||
public function importCustomer(Customer $customer): void
|
||||
{
|
||||
if ($customer->getId() === null) {
|
||||
$this->customers->saveNewCustomer($customer);
|
||||
} else {
|
||||
$this->customers->updateCustomer($customer);
|
||||
}
|
||||
}
|
||||
}
|
||||
17
src/Importer/ProjectImporterInterface.php
Normal file
17
src/Importer/ProjectImporterInterface.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use App\Entity\Project;
|
||||
|
||||
interface ProjectImporterInterface
|
||||
{
|
||||
public function convertEntryToProject(array $entry): Project;
|
||||
}
|
||||
20
src/Importer/UnsupportedFormatException.php
Normal file
20
src/Importer/UnsupportedFormatException.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Importer;
|
||||
|
||||
use Throwable;
|
||||
|
||||
class UnsupportedFormatException extends \Exception
|
||||
{
|
||||
public function __construct(string $message, int $code = 0, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct($message, $code, $previous);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ use InvalidArgumentException;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
final class ProjectService
|
||||
class ProjectService
|
||||
{
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
@@ -38,11 +38,8 @@ final class ProjectService
|
||||
*/
|
||||
private $validator;
|
||||
|
||||
public function __construct(
|
||||
ProjectRepository $projectRepository,
|
||||
EventDispatcherInterface $dispatcher,
|
||||
ValidatorInterface $validator
|
||||
) {
|
||||
public function __construct(ProjectRepository $projectRepository, EventDispatcherInterface $dispatcher, ValidatorInterface $validator)
|
||||
{
|
||||
$this->repository = $projectRepository;
|
||||
$this->dispatcher = $dispatcher;
|
||||
$this->validator = $validator;
|
||||
@@ -101,4 +98,9 @@ final class ProjectService
|
||||
|
||||
return $project;
|
||||
}
|
||||
|
||||
public function findProjectByName(string $name): ?Project
|
||||
{
|
||||
return $this->repository->findOneBy(['name' => $name]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,24 +20,25 @@ use Doctrine\ORM\ORMException;
|
||||
*/
|
||||
class ConfigurationRepository extends EntityRepository implements ConfigLoaderInterface
|
||||
{
|
||||
private static $cacheByPrefix = null;
|
||||
private static $cacheByPrefix = [];
|
||||
private static $cacheAll = [];
|
||||
private static $initialized = false;
|
||||
|
||||
public function clearCache()
|
||||
{
|
||||
static::$cacheByPrefix = null;
|
||||
static::$cacheAll = null;
|
||||
static::$cacheByPrefix = [];
|
||||
static::$cacheAll = [];
|
||||
static::$initialized = false;
|
||||
}
|
||||
|
||||
private function prefillCache()
|
||||
{
|
||||
if (null !== static::$cacheByPrefix) {
|
||||
if (static::$initialized === true) {
|
||||
return;
|
||||
}
|
||||
|
||||
/** @var Configuration[] $configs */
|
||||
$configs = $this->findAll();
|
||||
static::$cacheByPrefix = [];
|
||||
foreach ($configs as $config) {
|
||||
$key = substr($config->getName(), 0, strpos($config->getName(), '.'));
|
||||
if (!\array_key_exists($key, static::$cacheByPrefix)) {
|
||||
@@ -46,6 +47,7 @@ class ConfigurationRepository extends EntityRepository implements ConfigLoaderIn
|
||||
static::$cacheByPrefix[$key][] = $config;
|
||||
static::$cacheAll[] = $config;
|
||||
}
|
||||
static::$initialized = true;
|
||||
}
|
||||
|
||||
public function saveConfiguration(Configuration $configuration)
|
||||
|
||||
@@ -10,16 +10,12 @@
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\ImportCustomerCommand;
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Importer\ImporterService;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
/**
|
||||
* @covers \App\Command\ImportCustomerCommand
|
||||
* @group integration
|
||||
*/
|
||||
class ImportCustomerCommandTest extends KernelTestCase
|
||||
@@ -33,14 +29,11 @@ class ImportCustomerCommandTest extends KernelTestCase
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
||||
$customers = $this->createMock(CustomerRepository::class);
|
||||
$projects = $this->createMock(ProjectRepository::class);
|
||||
$teams = $this->createMock(TeamRepository::class);
|
||||
$users = $this->createMock(UserRepository::class);
|
||||
$configuration = $this->createMock(FormConfiguration::class);
|
||||
$importer = $container->get(ImporterService::class);
|
||||
|
||||
$this->application->add(new ImportCustomerCommand($customers, $projects, $teams, $users, $configuration));
|
||||
$this->application->add(new ImportCustomerCommand($importer));
|
||||
}
|
||||
|
||||
public function testCommandName()
|
||||
@@ -48,4 +41,184 @@ class ImportCustomerCommandTest extends KernelTestCase
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
self::assertInstanceOf(ImportCustomerCommand::class, $command);
|
||||
}
|
||||
|
||||
public function testImportWithMissingFile()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/foo_bar.csv1'
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Kimai importer: Customers', $result);
|
||||
self::assertStringContainsString('[ERROR] File not existing or not readable', $result);
|
||||
self::assertStringContainsString('_data/foo_bar', $result);
|
||||
|
||||
self::assertEquals(2, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportWithUnknownReader()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/customers2.csv',
|
||||
'--reader' => 'fooo',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('[ERROR] Unknown import reader: fooo', $result);
|
||||
|
||||
self::assertEquals(1, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testImportWithUnknownImporter()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/customers2.csv',
|
||||
'--importer' => 'fooo',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('[ERROR] Unknown customer importer: fooo', $result);
|
||||
|
||||
self::assertEquals(1, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImport()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/customers.csv'
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 10 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 10 customers, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 9 customer', $result);
|
||||
self::assertStringContainsString('[OK] Updated 1 customer', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportSkipUpdate()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/customers.csv',
|
||||
'--no-update' => true,
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 10 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 10 customers, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 9 customer', $result);
|
||||
self::assertStringContainsString('[OK] Skipped 1 existing customer', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportWithSemicolon()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/customers2.csv',
|
||||
'--importer' => 'default',
|
||||
'--reader' => 'csv-semicolon',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 10 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 10 customers, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 10 customer', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testGrandtotalImportWithInvalidCsvFile()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/customers2.csv',
|
||||
'--importer' => 'grandtotal',
|
||||
'--reader' => 'csv-semicolon',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Invalid row 1: Missing customer name', $result);
|
||||
self::assertStringContainsString('! [CAUTION] Not importing, previous 10 errors need to be fixed first.', $result);
|
||||
|
||||
self::assertEquals(3, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testGrandtotalImport()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/grandtotal_en.csv',
|
||||
'--importer' => 'grandtotal',
|
||||
'--reader' => 'csv-semicolon',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 1 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 1 customers, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 1 customer', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testGrandtotalImportGerman()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:customer');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/grandtotal_de.csv',
|
||||
'--importer' => 'grandtotal',
|
||||
'--reader' => 'csv-semicolon',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 2 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 2 customers, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 1 customer', $result);
|
||||
self::assertStringContainsString('[OK] Updated 1 customer', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
}
|
||||
|
||||
250
tests/Command/ImportProjectCommandTest.php
Normal file
250
tests/Command/ImportProjectCommandTest.php
Normal file
@@ -0,0 +1,250 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Command;
|
||||
|
||||
use App\Command\ImportProjectCommand;
|
||||
use App\Importer\ImporterService;
|
||||
use App\Repository\TeamRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use Symfony\Bundle\FrameworkBundle\Console\Application;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\Console\Tester\CommandTester;
|
||||
|
||||
/**
|
||||
* @group integration
|
||||
*/
|
||||
class ImportProjectCommandTest extends KernelTestCase
|
||||
{
|
||||
/**
|
||||
* @var Application
|
||||
*/
|
||||
protected $application;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
$this->application = new Application($kernel);
|
||||
$container = self::$kernel->getContainer();
|
||||
|
||||
$importer = $container->get(ImporterService::class);
|
||||
$teams = $this->createMock(TeamRepository::class);
|
||||
/** @var UserRepository $users */
|
||||
$users = $container->get(UserRepository::class);
|
||||
|
||||
$this->application->add(new ImportProjectCommand($importer, $teams, $users));
|
||||
}
|
||||
|
||||
public function testCommandName()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
self::assertInstanceOf(ImportProjectCommand::class, $command);
|
||||
}
|
||||
|
||||
public function testImportWithMissingFile()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/foo_bar.csv1'
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Kimai importer: Projects', $result);
|
||||
self::assertStringContainsString('[ERROR] File not existing or not readable', $result);
|
||||
self::assertStringContainsString('_data/foo_bar', $result);
|
||||
|
||||
self::assertEquals(2, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportWithUnknownReader()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/customers2.csv',
|
||||
'--reader' => 'fooo',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('[ERROR] Unknown import reader: fooo', $result);
|
||||
|
||||
self::assertEquals(1, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testImportWithUnknownImporter()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/customers2.csv',
|
||||
'--importer' => 'grandtotal',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('[ERROR] Unknown project importer: grandtotal', $result);
|
||||
|
||||
self::assertEquals(1, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImport()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/projects.csv',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 3 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 3 projects, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 2 projects', $result);
|
||||
self::assertStringContainsString('[OK] Updated 1 projects', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportSkipUpdate()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/projects.csv',
|
||||
'--no-update' => true,
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 3 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 3 projects, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 2 projects', $result);
|
||||
self::assertStringContainsString('[OK] Skipped 1 existing projects', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportWithInvalidCustomerMapping()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/projects_invalid.csv',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 3 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('[ERROR] Invalid row 2: Customer mismatch for project', $result);
|
||||
self::assertStringContainsString('[CAUTION] Not importing, previous 1 errors need to be fixed first.', $result);
|
||||
|
||||
self::assertEquals(3, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportWithSemicolon()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/projects2.csv',
|
||||
'--importer' => 'default',
|
||||
'--reader' => 'csv-semicolon',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 39 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 39 projects, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 39 projects', $result);
|
||||
self::assertStringContainsString('[OK] Imported 10 customers', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testGrandtotalImportWithInvalidCsvFile()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/projects.csv',
|
||||
'--reader' => 'csv-semicolon',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Invalid row 1: Missing customer name', $result);
|
||||
self::assertStringContainsString('! [CAUTION] Not importing, previous 3 errors need to be fixed first.', $result);
|
||||
|
||||
self::assertEquals(3, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportWithSemicolonAndTeamlead()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/projects2.csv',
|
||||
'--importer' => 'default',
|
||||
'--reader' => 'csv-semicolon',
|
||||
'--teamlead' => 'clara_customer',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('Found 39 rows to process, converting now ...', $result);
|
||||
self::assertStringContainsString('Converted 39 projects, importing into Kimai now ...', $result);
|
||||
self::assertStringContainsString('[OK] Imported 39 projects', $result);
|
||||
self::assertStringContainsString('[OK] Imported 10 customers', $result);
|
||||
self::assertStringContainsString('[OK] Created 39 teams', $result);
|
||||
|
||||
self::assertEquals(0, $commandTester->getStatusCode());
|
||||
}
|
||||
|
||||
public function testDefaultImportWithSemicolonAndMissingTeamlead()
|
||||
{
|
||||
$command = $this->application->find('kimai:import:project');
|
||||
$commandTester = new CommandTester($command);
|
||||
$commandTester->setInputs(['no']);
|
||||
$commandTester->execute([
|
||||
'command' => $command->getName(),
|
||||
'file' => __DIR__ . '/../Importer/_data/projects2.csv',
|
||||
'--importer' => 'default',
|
||||
'--reader' => 'csv-semicolon',
|
||||
'--teamlead' => 'foobar',
|
||||
]);
|
||||
|
||||
$result = $commandTester->getDisplay();
|
||||
|
||||
self::assertStringContainsString('You requested to create empty teams for each project', $result);
|
||||
self::assertStringContainsString('Please create a user with the name (or email) foobar', $result);
|
||||
|
||||
self::assertEquals(3, $commandTester->getStatusCode());
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
namespace App\Tests\Command;
|
||||
|
||||
use App\Command\ImportTimesheetCommand;
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
@@ -42,7 +42,7 @@ class ImportTimesheetCommandTest extends KernelTestCase
|
||||
$users = $this->createMock(UserRepository::class);
|
||||
$tagRepository = $this->createMock(TagRepository::class);
|
||||
$timesheets = $this->createMock(TimesheetRepository::class);
|
||||
$configuration = $this->createMock(FormConfiguration::class);
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
$this->application->add(new ImportTimesheetCommand($customers, $projects, $activities, $users, $tagRepository, $timesheets, $configuration));
|
||||
}
|
||||
|
||||
@@ -16,6 +16,7 @@ use PHPUnit\Framework\TestCase;
|
||||
/**
|
||||
* @covers \App\Configuration\FormConfiguration
|
||||
* @covers \App\Configuration\StringAccessibleConfigTrait
|
||||
* @group legacy
|
||||
*/
|
||||
class FormConfigurationTest extends TestCase
|
||||
{
|
||||
@@ -34,6 +35,13 @@ class FormConfigurationTest extends TestCase
|
||||
'currency' => 'GBP',
|
||||
'country' => 'FR',
|
||||
],
|
||||
'user' => [
|
||||
'timezone' => 'Europe/London',
|
||||
'currency' => 'GBP',
|
||||
'country' => 'FR',
|
||||
'language' => 'it',
|
||||
'theme' => 'blue',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
@@ -43,6 +51,11 @@ class FormConfigurationTest extends TestCase
|
||||
(new Configuration())->setName('defaults.customer.timezone')->setValue('Russia/Moscov'),
|
||||
(new Configuration())->setName('defaults.customer.currency')->setValue('USD'),
|
||||
(new Configuration())->setName('defaults.customer.country')->setValue('RU'),
|
||||
(new Configuration())->setName('defaults.user.timezone')->setValue('Russia/Moscov'),
|
||||
(new Configuration())->setName('defaults.user.currency')->setValue('USD'),
|
||||
(new Configuration())->setName('defaults.user.language')->setValue('RU'),
|
||||
(new Configuration())->setName('defaults.user.country')->setValue('RU'),
|
||||
(new Configuration())->setName('defaults.user.theme')->setValue('black'),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -66,6 +79,11 @@ class FormConfigurationTest extends TestCase
|
||||
$this->assertEquals('Russia/Moscov', $sut->getCustomerDefaultTimezone());
|
||||
$this->assertEquals('USD', $sut->getCustomerDefaultCurrency());
|
||||
$this->assertEquals('RU', $sut->getCustomerDefaultCountry());
|
||||
$this->assertEquals('USD', $sut->getUserDefaultCurrency());
|
||||
$this->assertEquals('RU', $sut->getUserDefaultLanguage());
|
||||
$this->assertEquals('black', $sut->getUserDefaultTheme());
|
||||
$this->assertEquals('Russia/Moscov', $sut->getUserDefaultTimezone());
|
||||
$this->assertEquals('Russia/Moscov', $sut->offsetGet('defaults.user.timezone'));
|
||||
}
|
||||
|
||||
public function testDefaultWithMixedConfigs()
|
||||
|
||||
@@ -51,6 +51,12 @@ class SystemConfigurationTest extends TestCase
|
||||
'currency' => 'GBP',
|
||||
'country' => 'FR',
|
||||
],
|
||||
'user' => [
|
||||
'timezone' => 'foo/bar',
|
||||
'theme' => 'blue',
|
||||
'language' => 'IT',
|
||||
'currency' => 'USD',
|
||||
],
|
||||
],
|
||||
'calendar' => [
|
||||
'businessHours' => [
|
||||
@@ -164,4 +170,28 @@ class SystemConfigurationTest extends TestCase
|
||||
$sources = $sut->getCalendarGoogleSources();
|
||||
$this->assertEquals(2, \count($sources));
|
||||
}
|
||||
|
||||
public function testFormDefaultWithoutLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), []);
|
||||
$this->assertEquals('Europe/London', $sut->getCustomerDefaultTimezone());
|
||||
$this->assertEquals('GBP', $sut->getCustomerDefaultCurrency());
|
||||
$this->assertEquals('FR', $sut->getCustomerDefaultCountry());
|
||||
$this->assertEquals('foo/bar', $sut->getUserDefaultTimezone());
|
||||
$this->assertEquals('blue', $sut->getUserDefaultTheme());
|
||||
$this->assertEquals('IT', $sut->getUserDefaultLanguage());
|
||||
$this->assertEquals('USD', $sut->getUserDefaultCurrency());
|
||||
}
|
||||
|
||||
public function testFormDefaultWithLoader()
|
||||
{
|
||||
$sut = $this->getSut($this->getDefaultSettings(), $this->getDefaultLoaderSettings());
|
||||
$this->assertEquals('Russia/Moscov', $sut->getCustomerDefaultTimezone());
|
||||
$this->assertEquals('RUB', $sut->getCustomerDefaultCurrency());
|
||||
$this->assertEquals('FR', $sut->getCustomerDefaultCountry());
|
||||
$this->assertEquals('foo/bar', $sut->getUserDefaultTimezone());
|
||||
$this->assertEquals('blue', $sut->getUserDefaultTheme());
|
||||
$this->assertEquals('IT', $sut->getUserDefaultLanguage());
|
||||
$this->assertEquals('USD', $sut->getUserDefaultCurrency());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use App\Configuration\ConfigLoaderInterface;
|
||||
use App\Entity\Configuration;
|
||||
|
||||
/**
|
||||
* @covers \App\Configuration\FormConfiguration
|
||||
* @covers \App\Configuration\SystemConfiguration
|
||||
*/
|
||||
class TestConfigLoader implements ConfigLoaderInterface
|
||||
{
|
||||
|
||||
@@ -64,6 +64,11 @@ class CalendarControllerTest extends ControllerBaseTest
|
||||
protected function getDefaultSettings()
|
||||
{
|
||||
return [
|
||||
'defaults' => [
|
||||
'user' => [
|
||||
'language' => 'en'
|
||||
],
|
||||
],
|
||||
'timesheet' => [
|
||||
'default_begin' => '08:30:00',
|
||||
],
|
||||
|
||||
@@ -11,6 +11,7 @@ namespace App\Tests\Controller;
|
||||
|
||||
use App\DataFixtures\UserFixtures;
|
||||
use App\Entity\User;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use App\Tests\KernelTestTrait;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;
|
||||
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||
@@ -26,6 +27,15 @@ abstract class ControllerBaseTest extends WebTestCase
|
||||
|
||||
public const DEFAULT_LANGUAGE = 'en';
|
||||
|
||||
protected function tearDown(): void
|
||||
{
|
||||
/** @var ConfigurationRepository $repository */
|
||||
$repository = static::$kernel->getContainer()->get(ConfigurationRepository::class);
|
||||
$repository->clearCache();
|
||||
|
||||
parent::tearDown();
|
||||
}
|
||||
|
||||
protected function getClientForAuthenticatedUser(string $role = User::ROLE_USER): HttpKernelBrowser
|
||||
{
|
||||
switch ($role) {
|
||||
|
||||
@@ -9,12 +9,10 @@
|
||||
|
||||
namespace App\Tests\Controller;
|
||||
|
||||
use App\Entity\Configuration;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\TimesheetMeta;
|
||||
use App\Entity\User;
|
||||
use App\Form\Type\DateRangeType;
|
||||
use App\Repository\ConfigurationRepository;
|
||||
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||
use App\Tests\Mocks\TimesheetTestMetaFieldSubscriberMock;
|
||||
|
||||
@@ -53,7 +51,6 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$start = new \DateTime('first day of this month');
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(5);
|
||||
$fixture->setAmountRunning(2);
|
||||
@@ -90,7 +87,6 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
$start = new \DateTime('first day of this month');
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(5);
|
||||
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
|
||||
@@ -127,7 +123,6 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(5);
|
||||
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
|
||||
@@ -305,60 +300,54 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
|
||||
public function testCreateActionWithFromAndToValuesTwiceFailsOnOverlappingRecord()
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||
$this->request($client, '/timesheet/create?from=2018-08-02T20%3A00%3A00&to=2018-08-02T20%3A30%3A00');
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
$this->assertAccessIsGranted($client, '/admin/system-config/');
|
||||
|
||||
$form = $client->getCrawler()->filter('form[name=timesheet_edit_form]')->form();
|
||||
$form = $client->getCrawler()->filter('form[name=system_configuration_form_timesheet]')->form();
|
||||
$client->submit($form, [
|
||||
'timesheet_edit_form' => [
|
||||
'hourlyRate' => 100,
|
||||
'project' => 1,
|
||||
'activity' => 1,
|
||||
'system_configuration_form_timesheet' => [
|
||||
'configuration' => [
|
||||
['name' => 'timesheet.mode', 'value' => 'default'],
|
||||
['name' => 'timesheet.active_entries.default_begin', 'value' => '08:00'],
|
||||
['name' => 'timesheet.rules.allow_future_times', 'value' => true],
|
||||
['name' => 'timesheet.rules.allow_overlapping_records', 'value' => false],
|
||||
['name' => 'timesheet.rules.lockdown_period_start', 'value' => null],
|
||||
['name' => 'timesheet.rules.lockdown_period_end', 'value' => null],
|
||||
['name' => 'timesheet.rules.lockdown_grace_period', 'value' => null],
|
||||
['name' => 'timesheet.active_entries.hard_limit', 'value' => 1],
|
||||
['name' => 'timesheet.active_entries.soft_limit', 'value' => 1],
|
||||
]
|
||||
]
|
||||
]);
|
||||
|
||||
$this->assertIsRedirect($client, $this->createUrl('/timesheet/'));
|
||||
$client->followRedirect();
|
||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||
$this->assertHasFlashSuccess($client);
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
/** @var Timesheet $timesheet */
|
||||
$timesheet = $em->getRepository(Timesheet::class)->find(1);
|
||||
$this->assertInstanceOf(\DateTime::class, $timesheet->getBegin());
|
||||
$this->assertInstanceOf(\DateTime::class, $timesheet->getEnd());
|
||||
$this->assertEquals(50, $timesheet->getRate());
|
||||
$begin = new \DateTime('2018-08-02T20:00:00');
|
||||
$end = new \DateTime('2018-08-02T20:30:00');
|
||||
|
||||
$expected = new \DateTime('2018-08-02T20:00:00');
|
||||
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getBegin()->format(\DateTime::ATOM));
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setCallback(function (Timesheet $timesheet) use ($begin, $end) {
|
||||
$timesheet->setBegin($begin);
|
||||
$timesheet->setEnd($end);
|
||||
});
|
||||
$fixture->setAmount(1);
|
||||
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
|
||||
$this->importFixture($fixture);
|
||||
|
||||
$expected = new \DateTime('2018-08-02T20:30:00');
|
||||
$this->assertEquals($expected->format(\DateTime::ATOM), $timesheet->getEnd()->format(\DateTime::ATOM));
|
||||
|
||||
/** @var ConfigurationRepository $configurations */
|
||||
$configurations = $em->getRepository(Configuration::class);
|
||||
$config = new Configuration();
|
||||
$config->setName('timesheet.rules.allow_overlapping_records');
|
||||
$config->setValue(false);
|
||||
$configurations->saveConfiguration($config);
|
||||
|
||||
// create a second entry that is overlapping fails due to the changed config above
|
||||
// create a second entry that is overlapping - should fail due to the changed config above
|
||||
$this->assertHasValidationError(
|
||||
$client,
|
||||
'/timesheet/create?from=2018-08-02T20%3A02%3A00&to=2018-08-02T20%3A20%3A00',
|
||||
'form[name=timesheet_edit_form]',
|
||||
[
|
||||
'timesheet_edit_form' => [
|
||||
'hourlyRate' => 100,
|
||||
//'hourlyRate' => 100,
|
||||
'project' => 1,
|
||||
'activity' => 1,
|
||||
]
|
||||
],
|
||||
['#timesheet_edit_form_begin']
|
||||
);
|
||||
|
||||
$configurations->clearCache();
|
||||
}
|
||||
|
||||
public function testCreateActionWithBeginAndEndAndTagValues()
|
||||
@@ -401,7 +390,6 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser();
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
$fixture->setUser($this->getUserByRole(User::ROLE_USER));
|
||||
@@ -442,7 +430,6 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$user = $this->getUserByRole(User::ROLE_USER);
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
@@ -481,7 +468,6 @@ class TimesheetControllerTest extends ControllerBaseTest
|
||||
{
|
||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_SUPER_ADMIN);
|
||||
|
||||
$em = $this->getEntityManager();
|
||||
$user = $this->getUserByRole(User::ROLE_SUPER_ADMIN);
|
||||
$fixture = new TimesheetFixtures();
|
||||
$fixture->setAmount(10);
|
||||
|
||||
156
tests/Customer/CustomerServiceTest.php
Normal file
156
tests/Customer/CustomerServiceTest.php
Normal file
@@ -0,0 +1,156 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Customer;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Customer\CustomerService;
|
||||
use App\Entity\Customer;
|
||||
use App\Event\CustomerCreateEvent;
|
||||
use App\Event\CustomerCreatePostEvent;
|
||||
use App\Event\CustomerCreatePreEvent;
|
||||
use App\Event\CustomerMetaDefinitionEvent;
|
||||
use App\Event\CustomerUpdatePostEvent;
|
||||
use App\Event\CustomerUpdatePreEvent;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Validator\ValidationFailedException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Component\EventDispatcher\EventDispatcherInterface;
|
||||
use Symfony\Component\Validator\ConstraintViolation;
|
||||
use Symfony\Component\Validator\ConstraintViolationList;
|
||||
use Symfony\Component\Validator\Validator\ValidatorInterface;
|
||||
|
||||
/**
|
||||
* @covers \App\Customer\CustomerService
|
||||
*/
|
||||
class CustomerServiceTest extends TestCase
|
||||
{
|
||||
private function getSut(
|
||||
?EventDispatcherInterface $dispatcher = null,
|
||||
?ValidatorInterface $validator = null,
|
||||
?CustomerRepository $repository = null,
|
||||
?SystemConfiguration $configuration = null
|
||||
): CustomerService {
|
||||
if ($repository === null) {
|
||||
$repository = $this->createMock(CustomerRepository::class);
|
||||
}
|
||||
|
||||
if ($dispatcher === null) {
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
}
|
||||
|
||||
if ($validator === null) {
|
||||
$validator = $this->createMock(ValidatorInterface::class);
|
||||
$validator->method('validate')->willReturn(new ConstraintViolationList());
|
||||
}
|
||||
|
||||
if ($configuration === null) {
|
||||
$configuration = $this->createMock(SystemConfiguration::class);
|
||||
$configuration->method('getCustomerDefaultTimezone')->willReturn('Europe/Vienna');
|
||||
$configuration->method('getCustomerDefaultCountry')->willReturn('IN');
|
||||
$configuration->method('getCustomerDefaultCurrency')->willReturn('RUB');
|
||||
}
|
||||
|
||||
$service = new CustomerService($repository, $configuration, $validator, $dispatcher);
|
||||
|
||||
return $service;
|
||||
}
|
||||
|
||||
public function testCannotSavePersistedCustomerAsNew()
|
||||
{
|
||||
$Customer = $this->createMock(Customer::class);
|
||||
$Customer->expects($this->once())->method('getId')->willReturn(1);
|
||||
|
||||
$sut = $this->getSut();
|
||||
|
||||
$this->expectException(\InvalidArgumentException::class);
|
||||
$this->expectExceptionMessage('Cannot create customer, already persisted');
|
||||
|
||||
$sut->saveNewCustomer($Customer);
|
||||
}
|
||||
|
||||
public function testSaveNewCustomerHasValidationError()
|
||||
{
|
||||
$constraints = new ConstraintViolationList();
|
||||
$constraints->add(new ConstraintViolation('toooo many tests', 'abc.def', [], '$root', 'begin', 4, null, null, null, '$cause'));
|
||||
|
||||
$validator = $this->createMock(ValidatorInterface::class);
|
||||
$validator->method('validate')->willReturn($constraints);
|
||||
|
||||
$sut = $this->getSut(null, $validator);
|
||||
|
||||
$this->expectException(ValidationFailedException::class);
|
||||
$this->expectExceptionMessage('Validation Failed');
|
||||
|
||||
$sut->saveNewCustomer(new Customer());
|
||||
}
|
||||
|
||||
public function testUpdateDispatchesEvents()
|
||||
{
|
||||
$Customer = $this->createMock(Customer::class);
|
||||
$Customer->method('getId')->willReturn(1);
|
||||
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) use ($Customer) {
|
||||
if ($event instanceof CustomerUpdatePostEvent) {
|
||||
self::assertSame($Customer, $event->getCustomer());
|
||||
} elseif ($event instanceof CustomerUpdatePreEvent) {
|
||||
self::assertSame($Customer, $event->getCustomer());
|
||||
} else {
|
||||
$this->fail('Invalid event received');
|
||||
}
|
||||
});
|
||||
|
||||
$sut = $this->getSut($dispatcher);
|
||||
|
||||
$sut->updateCustomer($Customer);
|
||||
}
|
||||
|
||||
public function testCreateNewCustomerDispatchesEvents()
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) {
|
||||
if ($event instanceof CustomerMetaDefinitionEvent) {
|
||||
self::assertInstanceOf(Customer::class, $event->getEntity());
|
||||
} elseif ($event instanceof CustomerCreateEvent) {
|
||||
self::assertInstanceOf(Customer::class, $event->getCustomer());
|
||||
} else {
|
||||
$this->fail('Invalid event received');
|
||||
}
|
||||
});
|
||||
|
||||
$sut = $this->getSut($dispatcher);
|
||||
|
||||
$customer = $sut->createNewCustomer();
|
||||
|
||||
self::assertInstanceOf(Customer::class, $customer);
|
||||
self::assertEquals('Europe/Vienna', $customer->getTimezone());
|
||||
self::assertEquals('IN', $customer->getCountry());
|
||||
self::assertEquals('RUB', $customer->getCurrency());
|
||||
}
|
||||
|
||||
public function testSaveNewCustomerDispatchesEvents()
|
||||
{
|
||||
$dispatcher = $this->createMock(EventDispatcherInterface::class);
|
||||
$dispatcher->expects($this->exactly(2))->method('dispatch')->willReturnCallback(function ($event) {
|
||||
if ($event instanceof CustomerCreatePreEvent) {
|
||||
self::assertInstanceOf(Customer::class, $event->getCustomer());
|
||||
} elseif ($event instanceof CustomerCreatePostEvent) {
|
||||
self::assertInstanceOf(Customer::class, $event->getCustomer());
|
||||
} else {
|
||||
$this->fail('Invalid event received');
|
||||
}
|
||||
});
|
||||
|
||||
$sut = $this->getSut($dispatcher);
|
||||
|
||||
$Customer = new Customer();
|
||||
$sut->saveNewCustomer($Customer);
|
||||
}
|
||||
}
|
||||
@@ -253,7 +253,6 @@ class AppExtensionTest extends TestCase
|
||||
'kimai.i18n_domains' => []
|
||||
];
|
||||
|
||||
// nasty parameter, should be removed!!!
|
||||
$this->assertTrue($container->hasParameter('kimai.config'));
|
||||
|
||||
foreach ($expected as $key => $value) {
|
||||
|
||||
29
tests/Event/AbstractCustomerEventTest.php
Normal file
29
tests/Event/AbstractCustomerEventTest.php
Normal file
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Event\AbstractCustomerEvent;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Symfony\Contracts\EventDispatcher\Event;
|
||||
|
||||
abstract class AbstractCustomerEventTest extends TestCase
|
||||
{
|
||||
abstract protected function createCustomerEvent(Customer $customer): AbstractCustomerEvent;
|
||||
|
||||
public function testGetterAndSetter()
|
||||
{
|
||||
$customer = new Customer();
|
||||
$sut = $this->createCustomerEvent($customer);
|
||||
|
||||
self::assertInstanceOf(Event::class, $sut);
|
||||
self::assertSame($customer, $sut->getCustomer());
|
||||
}
|
||||
}
|
||||
26
tests/Event/CustomerCreateEventTest.php
Normal file
26
tests/Event/CustomerCreateEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Event\AbstractCustomerEvent;
|
||||
use App\Event\CustomerCreateEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractCustomerEvent
|
||||
* @covers \App\Event\CustomerCreateEvent
|
||||
*/
|
||||
class CustomerCreateEventTest extends AbstractCustomerEventTest
|
||||
{
|
||||
protected function createCustomerEvent(Customer $customer): AbstractCustomerEvent
|
||||
{
|
||||
return new CustomerCreateEvent($customer);
|
||||
}
|
||||
}
|
||||
26
tests/Event/CustomerCreatePostEventTest.php
Normal file
26
tests/Event/CustomerCreatePostEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Event\AbstractCustomerEvent;
|
||||
use App\Event\CustomerCreatePostEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractCustomerEvent
|
||||
* @covers \App\Event\CustomerCreatePostEvent
|
||||
*/
|
||||
class CustomerCreatePostEventTest extends AbstractCustomerEventTest
|
||||
{
|
||||
protected function createCustomerEvent(Customer $customer): AbstractCustomerEvent
|
||||
{
|
||||
return new CustomerCreatePostEvent($customer);
|
||||
}
|
||||
}
|
||||
26
tests/Event/CustomerCreatePreEventTest.php
Normal file
26
tests/Event/CustomerCreatePreEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Event\AbstractCustomerEvent;
|
||||
use App\Event\CustomerCreatePreEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractCustomerEvent
|
||||
* @covers \App\Event\CustomerCreatePreEvent
|
||||
*/
|
||||
class CustomerCreatePreEventTest extends AbstractCustomerEventTest
|
||||
{
|
||||
protected function createCustomerEvent(Customer $customer): AbstractCustomerEvent
|
||||
{
|
||||
return new CustomerCreatePreEvent($customer);
|
||||
}
|
||||
}
|
||||
26
tests/Event/CustomerUpdatePostEventTest.php
Normal file
26
tests/Event/CustomerUpdatePostEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Event\AbstractCustomerEvent;
|
||||
use App\Event\CustomerUpdatePostEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractCustomerEvent
|
||||
* @covers \App\Event\CustomerUpdatePostEvent
|
||||
*/
|
||||
class CustomerUpdatePostEventTest extends AbstractCustomerEventTest
|
||||
{
|
||||
protected function createCustomerEvent(Customer $customer): AbstractCustomerEvent
|
||||
{
|
||||
return new CustomerUpdatePostEvent($customer);
|
||||
}
|
||||
}
|
||||
26
tests/Event/CustomerUpdatePreEventTest.php
Normal file
26
tests/Event/CustomerUpdatePreEventTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Event;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Event\AbstractCustomerEvent;
|
||||
use App\Event\CustomerUpdatePreEvent;
|
||||
|
||||
/**
|
||||
* @covers \App\Event\AbstractCustomerEvent
|
||||
* @covers \App\Event\CustomerUpdatePreEvent
|
||||
*/
|
||||
class CustomerUpdatePreEventTest extends AbstractCustomerEventTest
|
||||
{
|
||||
protected function createCustomerEvent(Customer $customer): AbstractCustomerEvent
|
||||
{
|
||||
return new CustomerUpdatePreEvent($customer);
|
||||
}
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
|
||||
namespace App\Tests\EventSubscriber;
|
||||
|
||||
use App\Configuration\FormConfiguration;
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Entity\User;
|
||||
use App\Entity\UserPreference;
|
||||
use App\Event\PrepareUserEvent;
|
||||
@@ -91,7 +91,7 @@ class UserPreferenceSubscriberTest extends TestCase
|
||||
$authMock->expects($this->once())->method('isGranted')->willReturn($seeHourlyRate);
|
||||
|
||||
$eventMock = $this->createMock(EventDispatcherInterface::class);
|
||||
$formConfigMock = $this->createMock(FormConfiguration::class);
|
||||
$formConfigMock = $this->createMock(SystemConfiguration::class);
|
||||
|
||||
return new UserPreferenceSubscriber($eventMock, $authMock, $formConfigMock);
|
||||
}
|
||||
|
||||
62
tests/Importer/CsvReaderTest.php
Normal file
62
tests/Importer/CsvReaderTest.php
Normal file
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Importer;
|
||||
|
||||
use App\Importer\CsvReader;
|
||||
use App\Importer\ImportNotFoundException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Importer\CsvReader
|
||||
*/
|
||||
class CsvReaderTest extends TestCase
|
||||
{
|
||||
public function testRead()
|
||||
{
|
||||
$sut = new CsvReader(';');
|
||||
$result = $sut->read(__DIR__ . '/_data/grandtotal_en.csv');
|
||||
$result = iterator_to_array($result);
|
||||
self::assertEquals([1 => [
|
||||
'Organization' => 'Keleo',
|
||||
'Department' => 'IT Abteilung',
|
||||
'Title' => 'Bc',
|
||||
'First name' => 'Kevin',
|
||||
'Middle name' => '',
|
||||
'Last name' => 'Papst',
|
||||
'E-Mail' => 'unknown@kimai.org',
|
||||
'Street' => 'Acme Street
|
||||
Downtown',
|
||||
'ZIP' => '1022',
|
||||
'City' => 'Vienna',
|
||||
'State' => '',
|
||||
'Salutation' => 'Sehr geehrter Herr',
|
||||
'Country' => 'DE',
|
||||
'Customer number' => '00001',
|
||||
'Tax-ID' => 'DE1234567890',
|
||||
'Note' => 'sakdjhfg laksjhdfasd f#asd<br>
|
||||
fas<br>
|
||||
dfasdfasdf<br>
|
||||
asdfasdfasdf',
|
||||
'IBAN' => '0987654321',
|
||||
'BIC' => '12345678',
|
||||
'SEPA Mandate ID' => '',
|
||||
'zusatz 1' => 'blub',
|
||||
'zusatz 2' => 'foo',
|
||||
]], $result);
|
||||
}
|
||||
|
||||
public function testReadNotFound()
|
||||
{
|
||||
$this->expectException(ImportNotFoundException::class);
|
||||
|
||||
$sut = new CsvReader(';');
|
||||
$sut->read(__DIR__ . '/_data/fffffoooooooooooo');
|
||||
}
|
||||
}
|
||||
90
tests/Importer/DefaultCustomerImporterTest.php
Normal file
90
tests/Importer/DefaultCustomerImporterTest.php
Normal file
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Importer;
|
||||
|
||||
use App\Customer\CustomerService;
|
||||
use App\Entity\Customer;
|
||||
use App\Importer\DefaultCustomerImporter;
|
||||
use App\Importer\UnsupportedFormatException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Importer\DefaultCustomerImporter
|
||||
*/
|
||||
class DefaultCustomerImporterTest extends TestCase
|
||||
{
|
||||
private function getSut(int $count = 1): DefaultCustomerImporter
|
||||
{
|
||||
$customerService = $this->createMock(CustomerService::class);
|
||||
$customerService->expects($this->exactly($count))->method('createNewCustomer')->willReturnCallback(
|
||||
function () {
|
||||
return new Customer();
|
||||
}
|
||||
);
|
||||
|
||||
$sut = new DefaultCustomerImporter($customerService);
|
||||
|
||||
return $sut;
|
||||
}
|
||||
|
||||
private function getDefaultImport(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Test customer',
|
||||
];
|
||||
}
|
||||
|
||||
private function prepareCustomer(array $values = []): Customer
|
||||
{
|
||||
$sut = $this->getSut();
|
||||
|
||||
$import = array_merge($this->getDefaultImport(), $values);
|
||||
|
||||
return $sut->convertEntryToCustomer($import);
|
||||
}
|
||||
|
||||
public function testImportMissingName()
|
||||
{
|
||||
$this->expectException(UnsupportedFormatException::class);
|
||||
$this->expectExceptionMessage('Missing customer name, expected in column: "Name"');
|
||||
|
||||
return $this->getSut(0)->convertEntryToCustomer(['sdfgsdfgsdfg' => 'sdfgsdfg']);
|
||||
}
|
||||
|
||||
public function testImport()
|
||||
{
|
||||
$customer = $this->prepareCustomer([]);
|
||||
self::assertEquals('Test customer', $customer->getName());
|
||||
}
|
||||
|
||||
public function testImportWithMultipleValues()
|
||||
{
|
||||
$customer = $this->prepareCustomer([
|
||||
'e mail' => 'test@example.com',
|
||||
'contact' => 'Foo Bar',
|
||||
'phone' => '0123 4567890',
|
||||
'mobile' => '111 354687',
|
||||
'fax' => '999 112233445566778899',
|
||||
'homepage' => 'www.example.com',
|
||||
'budget' => 1000.17,
|
||||
'time budget' => 3600,
|
||||
'meta.qwertz' => 'uztiuzgubhöklji7gl',
|
||||
]);
|
||||
self::assertEquals('Test customer', $customer->getName());
|
||||
self::assertEquals(1000.17, $customer->getBudget());
|
||||
self::assertEquals(3600, $customer->getTimeBudget());
|
||||
self::assertEquals('0123 4567890', $customer->getPhone());
|
||||
self::assertEquals('111 354687', $customer->getMobile());
|
||||
self::assertEquals('999 112233445566778899', $customer->getFax());
|
||||
self::assertEquals('www.example.com', $customer->getHomepage());
|
||||
self::assertEquals('www.example.com', $customer->getHomepage());
|
||||
self::assertEquals('uztiuzgubhöklji7gl', $customer->getMetaField('qwertz')->getValue());
|
||||
}
|
||||
}
|
||||
92
tests/Importer/DefaultProjectImporterTest.php
Normal file
92
tests/Importer/DefaultProjectImporterTest.php
Normal file
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Importer;
|
||||
|
||||
use App\Customer\CustomerService;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Importer\DefaultProjectImporter;
|
||||
use App\Importer\UnsupportedFormatException;
|
||||
use App\Project\ProjectService;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Importer\DefaultProjectImporter
|
||||
*/
|
||||
class DefaultProjectImporterTest extends TestCase
|
||||
{
|
||||
private function getSut(int $count = 1): DefaultProjectImporter
|
||||
{
|
||||
$projectService = $this->createMock(ProjectService::class);
|
||||
$projectService->expects($this->exactly($count))->method('createNewProject')->willReturnCallback(
|
||||
function (Customer $customer) {
|
||||
$customer->setTimezone('Europe/Paris');
|
||||
$project = new Project();
|
||||
$project->setCustomer($customer);
|
||||
|
||||
return $project;
|
||||
}
|
||||
);
|
||||
$customerService = $this->createMock(CustomerService::class);
|
||||
$customerService->expects($this->once())->method('createNewCustomer')->willReturn(new Customer());
|
||||
|
||||
$sut = new DefaultProjectImporter($projectService, $customerService);
|
||||
|
||||
return $sut;
|
||||
}
|
||||
|
||||
private function getDefaultImport(): array
|
||||
{
|
||||
return [
|
||||
'name' => 'Test project',
|
||||
];
|
||||
}
|
||||
|
||||
private function prepareProject(array $values = []): Project
|
||||
{
|
||||
$sut = $this->getSut();
|
||||
|
||||
$import = array_merge($this->getDefaultImport(), $values);
|
||||
|
||||
return $sut->convertEntryToProject($import);
|
||||
}
|
||||
|
||||
public function testImportMissingName()
|
||||
{
|
||||
$this->expectException(UnsupportedFormatException::class);
|
||||
$this->expectExceptionMessage('Missing project name, expected in one of the columns: "Name", "Project , "Project Name", "Project-Name", "ProjectName"');
|
||||
|
||||
return $this->getSut(0)->convertEntryToProject(['CuStOMer-name' => 'Test CUSTOMER!']);
|
||||
}
|
||||
|
||||
public function testImport()
|
||||
{
|
||||
$project = $this->prepareProject(['CuStOMer-name' => 'Test CUSTOMER!']);
|
||||
self::assertEquals('Test project', $project->getName());
|
||||
self::assertEquals('Test CUSTOMER!', $project->getCustomer()->getName());
|
||||
}
|
||||
|
||||
public function testImportWithMultipleValues()
|
||||
{
|
||||
$project = $this->prepareProject([
|
||||
'CuStOMer-name' => 'Test CUSTOMER!',
|
||||
'order date' => '2020-07-21 17:28:54',
|
||||
'budget' => 1000.17,
|
||||
'time budget' => 3600,
|
||||
'meta.abcd' => 'uztiuzgubhöklji7gl',
|
||||
]);
|
||||
self::assertEquals('Test project', $project->getName());
|
||||
self::assertEquals('Test CUSTOMER!', $project->getCustomer()->getName());
|
||||
self::assertEquals(1000.17, $project->getBudget());
|
||||
self::assertEquals(3600, $project->getTimeBudget());
|
||||
self::assertEquals('2020-07-21T17:28:54+0200', $project->getOrderDate()->format(DATE_ISO8601));
|
||||
self::assertEquals('uztiuzgubhöklji7gl', $project->getMetaField('abcd')->getValue());
|
||||
}
|
||||
}
|
||||
26
tests/Importer/UnsupportedFormatExceptionTest.php
Normal file
26
tests/Importer/UnsupportedFormatExceptionTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* 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 App\Tests\Importer;
|
||||
|
||||
use App\Importer\UnsupportedFormatException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Importer\UnsupportedFormatException
|
||||
*/
|
||||
class UnsupportedFormatExceptionTest extends TestCase
|
||||
{
|
||||
public function testException()
|
||||
{
|
||||
$sut = new UnsupportedFormatException('test');
|
||||
self::assertEquals('test', $sut->getMessage());
|
||||
self::assertEquals(0, $sut->getCode());
|
||||
}
|
||||
}
|
||||
21
tests/Importer/_data/customers.csv
Normal file
21
tests/Importer/_data/customers.csv
Normal file
@@ -0,0 +1,21 @@
|
||||
ID,Name,Company name,Account,VAT-ID,Address,Contact,Email,Phone,Mobile,Fax,Homepage,Country,Currency,Timezone,Color,Visible,Comment,meta.some-id,meta.aaaaa
|
||||
8,Cassin LLC,Cassin Limited,C-63671629,AT U33445071,"7170 Scottie Motorway Suite 828
|
||||
Bridgettestad, MO 04855-1492",,,,,,,YE,EUR,Pacific/Pitcairn,#0d0403,1,Et placeat error esse rerum. Vitae optio sed et consequuntur est molestias. Sunt accusantium error qui deleniti nisi esse ab.,4,
|
||||
12,Gaylord-Kling,,C-81069873,AT U41003217,"999 Stracke Underpass Apt. 278
|
||||
West Celestine, NC 28716",,,,,,,HN,NPR,Europe/Kiev,#17dd05,1,Quam itaque illo aut omnis et. Explicabo molestias ut consequatur ad voluptatem illum ut. Voluptas enim ut vitae explicabo quis.,5,
|
||||
7,"Kub, Lowe and Daugherty",,C-08406910,AT U34327342,"3410 Ferry Ferry
|
||||
New Raphaelstad, MA 86907-3325",,,,,,,AS,HRK,America/Indiana/Knox,,1,"Architecto explicabo in iure debitis sunt sed. Eum animi ducimus adipisci quod voluptas reprehenderit molestias laudantium. Eos voluptatem non est officia molestias minus.",,
|
||||
6,"Nitzsche, Pfeffer and Dickens",,C-73111252,AT U92452965,"563 Alvis Plains Apt. 816
|
||||
Schowalterberg, MT 54542",,,,,,,MU,JMD,America/Port_of_Spain,#d14add,1,Itaque ipsa et expedita. Porro aut ab quo. Cumque quia blanditiis aut et vero in neque.,,
|
||||
2,"Smitham, Balistreri and Ondricka",,C-29186648,AT U94223297,"685 Waelchi Mountains
|
||||
Vanceport, AK 24274",,,,,,,BR,RUB,Atlantic/Faroe,#dd0c08,0,Architecto eum quaerat minus in sed quia impedit. Amet qui aliquam rerum provident optio. Voluptatem pariatur qui ad vitae corporis.,,
|
||||
13,Stanton PLC,,C-65852132,AT U66535044,"1927 O'Keefe Island
|
||||
North Carmella, MI 02061-1971",,,,,,,GD,GNF,America/Denver,#dd8a11,1,Iusto dignissimos quibusdam neque fuga ut et quam nobis. Non atque natus esse consequatur quod et est.,,
|
||||
4,"Toy, Larson and Schaefer",,C-07787515,AT U10587666,"760 Dare Highway Suite 023
|
||||
Port Vernice, IA 78379-6315",,,,,,,GL,USD,America/Grand_Turk,#191add,1,Perferendis enim nihil fugiat explicabo animi. Molestiae odit nemo enim quo id. Odio dolores aperiam repudiandae accusantium. Consequuntur illo doloremque veritatis minima animi maiores impedit.,,
|
||||
14,"Ullrich, Cruickshank and Emard",,C-89509630,AT U88402013,"4532 Hahn Squares Suite 090
|
||||
Port Eliezer, MI 45157",,,,,,,HT,GHS,Australia/Perth,#d2d6de,1,Culpa debitis repellendus quisquam dolor non consequuntur. Omnis doloribus dolore recusandae dolorem quia quidem. Odio veniam expedita quam. Et ut aut voluptatem veniam et necessitatibus.,1,
|
||||
11,"Ullrich, Satterfield and Homenick",,C-91464415,AT U72675057,"46915 Hartmann Crossroad Suite 125
|
||||
Antoniahaven, VT 39060-5021",,,,,,,MU,ZMW,Asia/Taipei,#00dddd,1,Temporibus aut numquam et fugiat. Laboriosam itaque quibusdam dolore sunt delectus est. Iusto reprehenderit mollitia iure tempora blanditiis praesentium quia quisquam.,3,
|
||||
9,"Test",,C-76507878,AT U18747846,"95027 Murray Walks Apt. 283
|
||||
New Kendrick, UT 09361-3329",,,,,,,SZ,LKR,Asia/Riyadh,,1,Nulla velit voluptatem atque molestias consequuntur sed. Ut et sunt quia quidem ea.,,
|
||||
|
21
tests/Importer/_data/customers2.csv
Normal file
21
tests/Importer/_data/customers2.csv
Normal file
@@ -0,0 +1,21 @@
|
||||
ID;Name;Company name;Account;VAT-ID;Address;Contact;Email;Phone;Mobile;Fax;Homepage;Country;Currency;Timezone;Color;Visible;Comment;meta.some-id;meta.aaaaa
|
||||
8;Cassin LLC;Cassin Limited;C-63671629;AT U33445071;"7170 Scottie Motorway Suite 828
|
||||
Bridgettestad, MO 04855-1492";;;;;;;YE;EUR;Pacific/Pitcairn;#0d0403;1;Et placeat error esse rerum. Vitae optio sed et consequuntur est molestias. Sunt accusantium error qui deleniti nisi esse ab.;4;
|
||||
12;Gaylord-Kling;;C-81069873;AT U41003217;"999 Stracke Underpass Apt. 278
|
||||
West Celestine, NC 28716";;;;;;;HN;NPR;Europe/Kiev;#17dd05;1;Quam itaque illo aut omnis et. Explicabo molestias ut consequatur ad voluptatem illum ut. Voluptas enim ut vitae explicabo quis.;5;
|
||||
7;Kub, Lowe and Daugherty;;C-08406910;AT U34327342;"3410 Ferry Ferry
|
||||
New Raphaelstad, MA 86907-3325";;;;;;;AS;HRK;America/Indiana/Knox;;1;Architecto explicabo in iure debitis sunt sed. Eum animi ducimus adipisci quod voluptas reprehenderit molestias laudantium. Eos voluptatem non est officia molestias minus.;;
|
||||
6;Nitzsche, Pfeffer and Dickens;;C-73111252;AT U92452965;"563 Alvis Plains Apt. 816
|
||||
Schowalterberg, MT 54542";;;;;;;MU;JMD;America/Port_of_Spain;#d14add;1;Itaque ipsa et expedita. Porro aut ab quo. Cumque quia blanditiis aut et vero in neque.;;
|
||||
2;Smitham, Balistreri and Ondricka;;C-29186648;AT U94223297;"685 Waelchi Mountains
|
||||
Vanceport, AK 24274";;;;;;;BR;RUB;Atlantic/Faroe;#dd0c08;0;Architecto eum quaerat minus in sed quia impedit. Amet qui aliquam rerum provident optio. Voluptatem pariatur qui ad vitae corporis.;;
|
||||
13;Stanton PLC;;C-65852132;AT U66535044;"1927 O'Keefe Island
|
||||
North Carmella, MI 02061-1971";;;;;;;GD;GNF;America/Denver;#dd8a11;1;Iusto dignissimos quibusdam neque fuga ut et quam nobis. Non atque natus esse consequatur quod et est.;;
|
||||
4;Toy, Larson and Schaefer;;C-07787515;AT U10587666;"760 Dare Highway Suite 023
|
||||
Port Vernice, IA 78379-6315";;;;;;;GL;USD;America/Grand_Turk;#191add;1;Perferendis enim nihil fugiat explicabo animi. Molestiae odit nemo enim quo id. Odio dolores aperiam repudiandae accusantium. Consequuntur illo doloremque veritatis minima animi maiores impedit.;;
|
||||
14;Ullrich, Cruickshank and Emard;;C-89509630;AT U88402013;"4532 Hahn Squares Suite 090
|
||||
Port Eliezer, MI 45157";;;;;;;HT;GHS;Australia/Perth;#d2d6de;1;Culpa debitis repellendus quisquam dolor non consequuntur. Omnis doloribus dolore recusandae dolorem quia quidem. Odio veniam expedita quam. Et ut aut voluptatem veniam et necessitatibus.;1;
|
||||
11;Ullrich, Satterfield and Homenick;;C-91464415;AT U72675057;"46915 Hartmann Crossroad Suite 125
|
||||
Antoniahaven, VT 39060-5021";;;;;;;MU;ZMW;Asia/Taipei;#00dddd;1;Temporibus aut numquam et fugiat. Laboriosam itaque quibusdam dolore sunt delectus est. Iusto reprehenderit mollitia iure tempora blanditiis praesentium quia quisquam.;3;
|
||||
9;Weber, Weber and Mosciski;;C-76507878;AT U18747846;"95027 Murray Walks Apt. 283
|
||||
New Kendrick, UT 09361-3329";;;;;;;SZ;LKR;Asia/Riyadh;;1;Nulla velit voluptatem atque molestias consequuntur sed. Ut et sunt quia quidem ea.;;
|
||||
|
11
tests/Importer/_data/grandtotal_de.csv
Normal file
11
tests/Importer/_data/grandtotal_de.csv
Normal file
@@ -0,0 +1,11 @@
|
||||
Firma;Abteilung;Titel;Vorname;Zweiter Vorname;Nachname;E-Mail;Straße;PLZ;Ort;Bundesland;Briefanrede;Land;Kundennummer;Umsatzsteuer-ID;Notiz;IBAN;BIC;SEPA Mandat ID;zusatz 1;zusatz 2
|
||||
Keleo;IT Abteilung;Bc;Kevin;;Papst;unknown@kimai.org;"Acme Street
|
||||
Downtown";1022;Vienna;;Sehr geehrter Herr;DE;00001;DE1234567890;"sakdjhfg laksjhdfasd f#asd<br>
|
||||
fas<br>
|
||||
dfasdfasdf<br>
|
||||
asdfasdfasdf";0987654321;12345678;;blub;foo
|
||||
Test;IT Abteilung;Bc;Kevin;;Tspap;unknown@kimai.org;"Acme Street 2
|
||||
Downtown";1022;Vienna;;Sehr geehrter Herr;DE;1;DE0987654321;"sakdjhfg laksjhdfasd f#asd<br>
|
||||
fas<br>
|
||||
dfasdfasdf<br>
|
||||
asdfasdfasdf";0987654321;12345678;;blub;foo
|
||||
|
6
tests/Importer/_data/grandtotal_en.csv
Normal file
6
tests/Importer/_data/grandtotal_en.csv
Normal file
@@ -0,0 +1,6 @@
|
||||
Organization;Department;Title;First name;Middle name;Last name;E-Mail;Street;ZIP;City;State;Salutation;Country;Customer number;Tax-ID;Note;IBAN;BIC;SEPA Mandate ID;zusatz 1;zusatz 2
|
||||
Keleo;IT Abteilung;Bc;Kevin;;Papst;unknown@kimai.org;"Acme Street
|
||||
Downtown";1022;Vienna;;Sehr geehrter Herr;DE;00001;DE1234567890;"sakdjhfg laksjhdfasd f#asd<br>
|
||||
fas<br>
|
||||
dfasdfasdf<br>
|
||||
asdfasdfasdf";0987654321;12345678;;blub;foo
|
||||
|
4
tests/Importer/_data/projects.csv
Normal file
4
tests/Importer/_data/projects.csv
Normal file
@@ -0,0 +1,4 @@
|
||||
ID,Name,Customer,Order number,Order date,Project Start,Project End,Color,Visible,Comment,Meta.zzzzz
|
||||
147,Multi-channelled 4thgeneration strategy,"Ullrich, Satterfield and Homenick",P-53400468,,,,,1,Amet non magni est. Cupiditate in magni adipisci nisi voluptatem sequi id. Accusamus sed alias enim et perferendis quo ut. Veniam voluptatem dicta dolores ut tenetur.,
|
||||
188,Test,Test,P-58944387,,,,,1,Facere architecto doloremque quae. Exercitationem ut velit molestiae alias dolore provident. Unde facere iste harum doloribus odio.,
|
||||
177,Multi-channelled transitional data-warehouse,Gaylord-Kling,P-17219648,,,,,1,Voluptatibus ipsa eos et maiores. Autem dolorem quia sit dolores. Qui adipisci aut aut eligendi voluptatibus. Libero voluptatem dignissimos rerum dignissimos aliquid consectetur.,
|
||||
|
40
tests/Importer/_data/projects2.csv
Normal file
40
tests/Importer/_data/projects2.csv
Normal file
@@ -0,0 +1,40 @@
|
||||
ID;Name;Customer;Order number;Order date;Project Start;Project End;Color;Visible;Comment;Meta.zzzzz
|
||||
147;Multi-channelled 4thgeneration strategy;Ullrich, Satterfield and Homenick;P-53400468;;;;;1;Amet non magni est. Cupiditate in magni adipisci nisi voluptatem sequi id. Accusamus sed alias enim et perferendis quo ut. Veniam voluptatem dicta dolores ut tenetur.;
|
||||
188;Multi-channelled background workforce;Stanton PLC;P-58944387;;;;;1;Facere architecto doloremque quae. Exercitationem ut velit molestiae alias dolore provident. Unde facere iste harum doloribus odio.;
|
||||
177;Multi-channelled transitional data-warehouse;Gaylord-Kling;P-17219648;;;;;1;Voluptatibus ipsa eos et maiores. Autem dolorem quia sit dolores. Qui adipisci aut aut eligendi voluptatibus. Libero voluptatem dignissimos rerum dignissimos aliquid consectetur.;
|
||||
154;Multi-channelled uniform focusgroup;Ullrich, Satterfield and Homenick;P-36133086;;;;;0;Similique eaque ea aut provident. Explicabo maiores consectetur est voluptatum expedita. Fugit cumque non iusto recusandae aspernatur.;
|
||||
165;Multi-channelled web-enabled adapter;Gaylord-Kling;P-27569344;;;;;1;Odit beatae nostrum quia laboriosam nam dolores. Rem similique voluptate facilis rerum omnis quia. Officiis a culpa vel aliquam sunt perspiciatis.;
|
||||
201;Multi-layered empowering neural-net;Ullrich, Cruickshank and Emard;P-96679708;;;;;1;Minus vero praesentium mollitia earum repellat quas eveniet eius. Et velit aut quia. Placeat non aperiam in iure eligendi harum porro.;
|
||||
27;Multi-layered uniform project;Smitham, Balistreri and Ondricka;P-86389174;;;;;1;Id placeat ut ut eius fugiat voluptate qui. Unde deleniti velit voluptatem quia. Neque harum eum est.;#d2d6de
|
||||
181;Multi-tiered impactful paradigm;Stanton PLC;P-78069558;;;;;1;Cupiditate nemo ut omnis consectetur est ipsam voluptates. A minus quod sit vel adipisci rerum. Voluptatum ut nihil et rerum. Recusandae voluptatem porro sed omnis fuga ut ea.;
|
||||
115;Networked contextually-based blockchain;Weber, Weber and Mosciski;P-14827181;;;;;1;Animi maiores dolorem impedit molestias. Et est quia et aut. Eligendi et quis quia occaecati exercitationem. Autem vel voluptas maxime modi reprehenderit earum.;
|
||||
18;Networked mission-critical functionalities;A customer name;P-06598815;;;;;1;Et illo labore sit sint. Similique quis natus voluptates aut ut. Ipsum distinctio laboriosam veritatis nesciunt consequatur laborum odit autem. Recusandae impedit veritatis sunt quasi.;
|
||||
138;Open-architected human-resource instructionset;Ullrich, Satterfield and Homenick;P-87900477;;;;;0;At commodi quis non eveniet recusandae voluptas. Assumenda earum esse et est odio ut dolorem. Enim nihil ipsa quo et. Rerum provident facilis non deleniti dolore aut. Culpa sed rerum sequi.;
|
||||
183;Open-source web-enabled projection;Stanton PLC;P-44548292;;;;;1;Distinctio eum earum debitis facere velit numquam exercitationem ipsum. Et et iusto possimus facere. Unde autem et recusandae sed quae. Sed vero eligendi nihil corporis harum fugiat.;
|
||||
37;Optimized content-based array;Smitham, Balistreri and Ondricka;P-74951659;;;;;1;Est aperiam est similique sit velit et. Rerum commodi veniam ullam et consequuntur.;
|
||||
178;Optional bandwidth-monitored moratorium;Stanton PLC;P-72979020;;;;;1;Voluptas suscipit voluptatibus dolorum ullam eos consequuntur aut. Nam voluptatem magni enim quas ad tempore voluptatum. Cum incidunt est qui sit.;Sdfgsdf
|
||||
53;Organic context-sensitive neural-net;Toy, Larson and Schaefer;P-53133083;;;;#d2d6de;1;Vitae sint sed ea nulla facere qui cum. Eligendi velit repellat non sit sit atque aut. Est minus nobis id accusamus repellat et. Quo in et tempora expedita.;
|
||||
83;Organic incremental interface;Nitzsche, Pfeffer and Dickens;P-10570715;;;;;1;Iure est impedit explicabo dolores qui. Quo amet est iste nulla et voluptatum. Sed magni id aliquam aut velit voluptas omnis dolores.;
|
||||
84;Organic needs-based methodology;Nitzsche, Pfeffer and Dickens;P-16957282;;;;;1;Dignissimos dolores et nam ipsa. Aut ipsam fugiat vel. Neque vel consequatur corporis quibusdam eaque.;
|
||||
26;Organic nextgeneration array;Smitham, Balistreri and Ondricka;P-90074059;;;;;1;Rerum at animi sint ipsum. Expedita earum omnis in voluptatem voluptatem porro libero. Perspiciatis possimus ratione voluptatem nobis.;Sfgh0zu90
|
||||
100;Organic responsive artificialintelligence;Cassin LLC;P-47808997;;;;#d2d6de;1;Enim molestias molestiae error ab quisquam qui. Nihil tempore voluptatum vel id et. Sint nisi consequatur iste ipsa vitae veniam neque.;
|
||||
99;Organized composite capability;Cassin LLC;P-88143507;;;;;1;Ut quia recusandae occaecati voluptates dolores. Consectetur quasi et et. Nostrum error modi quam ea a quam eveniet.;
|
||||
167;Organized interactive hardware;Gaylord-Kling;P-80225447;;;;;1;Doloribus eveniet repellendus ut. Dolor quaerat vel eos velit et aspernatur. Consequatur rerum provident quia voluptatem.;
|
||||
119;Organized reciprocal groupware;Weber, Weber and Mosciski;P-96414873;;;;;1;Et reprehenderit reiciendis magnam corrupti. Laudantium fugit fugit nobis unde quod magnam. Sit rerum earum nostrum repellendus.;
|
||||
150;Persistent asynchronous migration;Ullrich, Satterfield and Homenick;P-72771556;;;;;0;Dolores quia nisi aliquam vitae rem. Dolorem ipsum velit culpa quo. Voluptatem iste aliquam maiores consequatur. Et quisquam quae cupiditate ea labore dolor qui.;
|
||||
193;Persistent dedicated extranet;Stanton PLC;P-83020315;;;;;1;Eligendi harum in consequatur enim aut quidem. Debitis est autem et amet quia. Recusandae enim cumque quidem sequi suscipit sunt.;#d2d6de
|
||||
96;Phased assymetric contingency;Cassin LLC;P-63450040;;;;;1;Earum odit non ad cum nihil voluptas et. Totam ea corrupti dicta est. Odit minima est repudiandae sed aperiam sed dolores.;
|
||||
44;Polarised regional capacity;Toy, Larson and Schaefer;P-86899055;;;;;1;Accusamus a sit nisi voluptatem doloremque ipsa totam. Autem sit laudantium magnam perferendis sapiente. Assumenda aperiam quos nobis qui cumque accusantium.;
|
||||
55;Pre-emptive client-driven neural-net;Toy, Larson and Schaefer;P-93689915;;;;;1;Eos labore exercitationem rerum modi tenetur. Occaecati labore sit in rerum quas. Ullam quos illum iure.;
|
||||
51;Pre-emptive directional application;Toy, Larson and Schaefer;P-82374365;;;;;1;Voluptas consequatur consequuntur deserunt dolores placeat. Velit dolores accusamus commodi quae sit. Ut optio in ipsam rerum doloribus.;
|
||||
45;Pre-emptive grid-enabled migration;Toy, Larson and Schaefer;P-44070519;;;;;1;Et nihil alias occaecati nostrum. Doloribus neque nemo repellat nihil. Nihil voluptas est non fugit. Hic nulla omnis alias incidunt asperiores asperiores aut.;
|
||||
90;Pre-emptive homogeneous migration;Cassin LLC;P-37388904;;;;;1;Voluptate quo ipsa et natus. Eum impedit dignissimos et consequatur rerum magnam in provident. Quis qui nesciunt voluptatem minus ut distinctio illum. Consequatur rem et quia quis cupiditate animi.;
|
||||
142;Pre-emptive mission-critical extranet;Ullrich, Satterfield and Homenick;P-36044153;;;;;1;Reiciendis ullam voluptas quia corrupti quod est ut. Qui mollitia dolorem possimus asperiores. Non assumenda qui dolor odio et nihil. Accusantium sint laboriosam veniam quia.;2314
|
||||
125;Pre-emptive optimal extranet;Weber, Weber and Mosciski;P-44924478;;;;;1;Sed aut cumque aut id. Ea qui dignissimos natus inventore praesentium dolorem nihil. Aut sint doloremque minima consequuntur ut ut et. Magnam vel distinctio aperiam laudantium.;
|
||||
24;Proactive explicit info-mediaries;A customer name;P-19018812;;;;;1;Modi dignissimos numquam et quia magni. Magnam impedit ab natus quo.;
|
||||
174;Proactive solution-oriented groupware;Gaylord-Kling;P-42846727;;;;;1;Expedita error voluptatem occaecati. Architecto nobis et perferendis et ipsum soluta. Repudiandae occaecati iure et.;
|
||||
192;Proactive uniform internetsolution;Stanton PLC;P-75026981;;;;;0;Ipsum perferendis amet non sed non perferendis voluptates. Ut quasi quia vitae rerum eos quod. Nihil cum odit provident expedita.;#d2d6de
|
||||
135;Profit-focused contextually-based moratorium;Ullrich, Satterfield and Homenick;P-46607140;;;;;1;Et nisi placeat aut modi. Atque reprehenderit dolorum ducimus non in. Dolor id repudiandae dicta fugit ratione perspiciatis.;
|
||||
160;Profound didactic artificialintelligence;Gaylord-Kling;P-61821699;;;;;1;Repudiandae omnis labore sed vel. Sed vel aperiam nobis id sed mollitia. Sunt impedit ipsum reiciendis ut nobis at provident.;
|
||||
162;Profound fault-tolerant data-warehouse;Gaylord-Kling;P-04359890;;;;;1;Ut esse blanditiis totam consequatur illo quibusdam nostrum rerum. Aspernatur dolores est tempora incidunt voluptas facere vero quod. In autem sint iste architecto inventore itaque blanditiis eum.;
|
||||
12;Profound homogeneous instructionset;A customer name;P-87850895;;;;;1;Dignissimos a commodi cum et et incidunt molestias. Similique odio labore molestias ipsam velit nihil. Et excepturi fuga animi inventore consequatur minus.;
|
||||
|
4
tests/Importer/_data/projects_invalid.csv
Normal file
4
tests/Importer/_data/projects_invalid.csv
Normal file
@@ -0,0 +1,4 @@
|
||||
ID,Name,Customer,Order number,Order date,Project Start,Project End,Color,Visible,Comment,Meta.zzzzz
|
||||
147,Multi-channelled 4thgeneration strategy,"Ullrich, Satterfield and Homenick",P-53400468,,,,,1,Amet non magni est. Cupiditate in magni adipisci nisi voluptatem sequi id. Accusamus sed alias enim et perferendis quo ut. Veniam voluptatem dicta dolores ut tenetur.,
|
||||
188,Test,Stanton PLC,P-58944387,,,,,1,Facere architecto doloremque quae. Exercitationem ut velit molestiae alias dolore provident. Unde facere iste harum doloribus odio.,
|
||||
177,Multi-channelled transitional data-warehouse,Gaylord-Kling,P-17219648,,,,,1,Voluptatibus ipsa eos et maiores. Autem dolorem quia sit dolores. Qui adipisci aut aut eligendi voluptatibus. Libero voluptatem dignissimos rerum dignissimos aliquid consectetur.,
|
||||
|
Reference in New Issue
Block a user