added project importer and grandtotal converter (#1468)

This commit is contained in:
Kevin Papst
2020-10-11 22:08:00 +02:00
committed by GitHub
parent 0ce02b4f06
commit 996f05c73a
68 changed files with 2827 additions and 384 deletions

View 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);
}

View 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);
}

View 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();
}
}

View 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;
}

View 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;
}
}

View 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;
}
}

View 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;
}
}

View 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
{
}

View 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
{
}

View 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;
}

View 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);
}
}
}

View 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;
}

View 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);
}
}