added project importer and grandtotal converter (#1468)
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user