added csv importer for timesheet and project data (#1216)
This commit is contained in:
@@ -27,6 +27,7 @@
|
||||
"kevinpapst/adminlte-bundle": "~3.0",
|
||||
"kimai/kimai2-composer": "^0.1",
|
||||
"laravolt/avatar": "^3.0",
|
||||
"league/csv": "^9.4",
|
||||
"mpdf/mpdf": "^7.1",
|
||||
"nelmio/api-doc-bundle": "^3.2",
|
||||
"nelmio/cors-bundle": "^1.5",
|
||||
|
||||
297
composer.lock
generated
297
composer.lock
generated
@@ -4,20 +4,20 @@
|
||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "348a459e96d09528ed01477c95c8dd6a",
|
||||
"content-hash": "89fb45fe0706c25775a066f5f6138d62",
|
||||
"packages": [
|
||||
{
|
||||
"name": "beberlei/doctrineextensions",
|
||||
"version": "v1.2.3",
|
||||
"version": "v1.2.4",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/beberlei/DoctrineExtensions.git",
|
||||
"reference": "a8747a2aef34f602c365cf2efd1abc94c5ae7e0a"
|
||||
"reference": "5d5a7259b7d44f6d9d5ced4c09f9db6b7f460139"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/a8747a2aef34f602c365cf2efd1abc94c5ae7e0a",
|
||||
"reference": "a8747a2aef34f602c365cf2efd1abc94c5ae7e0a",
|
||||
"url": "https://api.github.com/repos/beberlei/DoctrineExtensions/zipball/5d5a7259b7d44f6d9d5ced4c09f9db6b7f460139",
|
||||
"reference": "5d5a7259b7d44f6d9d5ced4c09f9db6b7f460139",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -58,7 +58,7 @@
|
||||
"doctrine",
|
||||
"orm"
|
||||
],
|
||||
"time": "2019-07-25T15:08:53+00:00"
|
||||
"time": "2019-11-22T14:30:56+00:00"
|
||||
},
|
||||
{
|
||||
"name": "behat/transliterator",
|
||||
@@ -502,16 +502,16 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/doctrine-bundle",
|
||||
"version": "1.11.2",
|
||||
"version": "1.12.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/DoctrineBundle.git",
|
||||
"reference": "28101e20776d8fa20a00b54947fbae2db0d09103"
|
||||
"reference": "a374f3bc54fe7c23ddb722ef6b9ddd2d9dcb9115"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/28101e20776d8fa20a00b54947fbae2db0d09103",
|
||||
"reference": "28101e20776d8fa20a00b54947fbae2db0d09103",
|
||||
"url": "https://api.github.com/repos/doctrine/DoctrineBundle/zipball/a374f3bc54fe7c23ddb722ef6b9ddd2d9dcb9115",
|
||||
"reference": "a374f3bc54fe7c23ddb722ef6b9ddd2d9dcb9115",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -519,11 +519,11 @@
|
||||
"doctrine/doctrine-cache-bundle": "~1.2",
|
||||
"jdorn/sql-formatter": "^1.2.16",
|
||||
"php": "^7.1",
|
||||
"symfony/config": "^3.4|^4.1",
|
||||
"symfony/console": "^3.4|^4.1",
|
||||
"symfony/dependency-injection": "^3.4|^4.1",
|
||||
"symfony/doctrine-bridge": "^3.4|^4.1",
|
||||
"symfony/framework-bundle": "^3.4|^4.1"
|
||||
"symfony/config": "^3.4.30|^4.3.3",
|
||||
"symfony/console": "^3.4.30|^4.3.3",
|
||||
"symfony/dependency-injection": "^3.4.30|^4.3.3",
|
||||
"symfony/doctrine-bridge": "^3.4.30|^4.3.3",
|
||||
"symfony/framework-bundle": "^3.4.30|^4.3.3"
|
||||
},
|
||||
"conflict": {
|
||||
"doctrine/orm": "<2.6",
|
||||
@@ -533,14 +533,15 @@
|
||||
"doctrine/coding-standard": "^6.0",
|
||||
"doctrine/orm": "^2.6",
|
||||
"php-coveralls/php-coveralls": "^2.1",
|
||||
"phpunit/phpunit": "7.0",
|
||||
"symfony/cache": "^3.4|^4.1",
|
||||
"phpunit/phpunit": "^7.5",
|
||||
"symfony/cache": "^3.4.30|^4.3.3",
|
||||
"symfony/phpunit-bridge": "^4.2",
|
||||
"symfony/property-info": "^3.4|^4.1",
|
||||
"symfony/validator": "^3.4|^4.1",
|
||||
"symfony/web-profiler-bundle": "^3.4|^4.1",
|
||||
"symfony/yaml": "^3.4|^4.1",
|
||||
"twig/twig": "^1.34|^2.4"
|
||||
"symfony/property-info": "^3.4.30|^4.3.3",
|
||||
"symfony/twig-bridge": "^3.4|^4.1",
|
||||
"symfony/validator": "^3.4.30|^4.3.3",
|
||||
"symfony/web-profiler-bundle": "^3.4.30|^4.3.3",
|
||||
"symfony/yaml": "^3.4.30|^4.3.3",
|
||||
"twig/twig": "^1.34|^2.12"
|
||||
},
|
||||
"suggest": {
|
||||
"doctrine/orm": "The Doctrine ORM integration is optional in the bundle.",
|
||||
@@ -549,7 +550,7 @@
|
||||
"type": "symfony-bundle",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "1.11.x-dev"
|
||||
"dev-master": "1.12.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -563,20 +564,20 @@
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Symfony Community",
|
||||
"homepage": "http://symfony.com/contributors"
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
},
|
||||
{
|
||||
"name": "Benjamin Eberlei",
|
||||
"email": "kontakt@beberlei.de"
|
||||
},
|
||||
{
|
||||
"name": "Doctrine Project",
|
||||
"homepage": "http://www.doctrine-project.org/"
|
||||
"name": "Symfony Community",
|
||||
"homepage": "http://symfony.com/contributors"
|
||||
},
|
||||
{
|
||||
"name": "Fabien Potencier",
|
||||
"email": "fabien@symfony.com"
|
||||
"name": "Doctrine Project",
|
||||
"homepage": "http://www.doctrine-project.org/"
|
||||
}
|
||||
],
|
||||
"description": "Symfony DoctrineBundle",
|
||||
@@ -587,7 +588,7 @@
|
||||
"orm",
|
||||
"persistence"
|
||||
],
|
||||
"time": "2019-06-04T07:35:05+00:00"
|
||||
"time": "2019-11-19T11:42:20+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/doctrine-cache-bundle",
|
||||
@@ -1093,33 +1094,35 @@
|
||||
},
|
||||
{
|
||||
"name": "doctrine/orm",
|
||||
"version": "v2.6.4",
|
||||
"version": "v2.7.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/doctrine/orm.git",
|
||||
"reference": "b52ef5a1002f99ab506a5a2d6dba5a2c236c5f43"
|
||||
"reference": "4d763ca4c925f647b248b9fa01b5f47aa3685d62"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/doctrine/orm/zipball/b52ef5a1002f99ab506a5a2d6dba5a2c236c5f43",
|
||||
"reference": "b52ef5a1002f99ab506a5a2d6dba5a2c236c5f43",
|
||||
"url": "https://api.github.com/repos/doctrine/orm/zipball/4d763ca4c925f647b248b9fa01b5f47aa3685d62",
|
||||
"reference": "4d763ca4c925f647b248b9fa01b5f47aa3685d62",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"doctrine/annotations": "~1.5",
|
||||
"doctrine/cache": "~1.6",
|
||||
"doctrine/collections": "^1.4",
|
||||
"doctrine/common": "^2.7.1",
|
||||
"doctrine/dbal": "^2.6",
|
||||
"doctrine/instantiator": "~1.1",
|
||||
"doctrine/annotations": "^1.8",
|
||||
"doctrine/cache": "^1.9.1",
|
||||
"doctrine/collections": "^1.5",
|
||||
"doctrine/common": "^2.11",
|
||||
"doctrine/dbal": "^2.9.3",
|
||||
"doctrine/event-manager": "^1.1",
|
||||
"doctrine/instantiator": "^1.3",
|
||||
"doctrine/persistence": "^1.2",
|
||||
"ext-pdo": "*",
|
||||
"php": "^7.1",
|
||||
"symfony/console": "~3.0|~4.0"
|
||||
"symfony/console": "^3.0|^4.0|^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"doctrine/coding-standard": "^5.0",
|
||||
"phpunit/phpunit": "^7.5",
|
||||
"symfony/yaml": "~3.4|~4.0"
|
||||
"symfony/yaml": "^3.4|^4.0|^5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/yaml": "If you want to use YAML Metadata Mapping Driver"
|
||||
@@ -1130,7 +1133,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.6.x-dev"
|
||||
"dev-master": "2.7.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -1165,12 +1168,12 @@
|
||||
}
|
||||
],
|
||||
"description": "Object-Relational-Mapper for PHP",
|
||||
"homepage": "http://www.doctrine-project.org",
|
||||
"homepage": "https://www.doctrine-project.org/projects/orm.html",
|
||||
"keywords": [
|
||||
"database",
|
||||
"orm"
|
||||
],
|
||||
"time": "2019-09-20T14:30:26+00:00"
|
||||
"time": "2019-11-19T08:38:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "doctrine/persistence",
|
||||
@@ -2638,7 +2641,7 @@
|
||||
},
|
||||
{
|
||||
"name": "illuminate/cache",
|
||||
"version": "v6.5.1",
|
||||
"version": "v6.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/cache.git",
|
||||
@@ -2689,7 +2692,7 @@
|
||||
},
|
||||
{
|
||||
"name": "illuminate/contracts",
|
||||
"version": "v6.5.1",
|
||||
"version": "v6.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/contracts.git",
|
||||
@@ -2733,16 +2736,16 @@
|
||||
},
|
||||
{
|
||||
"name": "illuminate/support",
|
||||
"version": "v6.5.1",
|
||||
"version": "v6.5.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/illuminate/support.git",
|
||||
"reference": "236fa24d2e510c4800429cde89ed72e535051a5d"
|
||||
"reference": "7a93f9e5a15bbf41236a8695fb738bf293f3c186"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/illuminate/support/zipball/236fa24d2e510c4800429cde89ed72e535051a5d",
|
||||
"reference": "236fa24d2e510c4800429cde89ed72e535051a5d",
|
||||
"url": "https://api.github.com/repos/illuminate/support/zipball/7a93f9e5a15bbf41236a8695fb738bf293f3c186",
|
||||
"reference": "7a93f9e5a15bbf41236a8695fb738bf293f3c186",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -2790,7 +2793,7 @@
|
||||
],
|
||||
"description": "The Illuminate Support package.",
|
||||
"homepage": "https://laravel.com",
|
||||
"time": "2019-11-04T13:46:29+00:00"
|
||||
"time": "2019-11-18T21:49:40+00:00"
|
||||
},
|
||||
{
|
||||
"name": "intervention/image",
|
||||
@@ -3308,6 +3311,75 @@
|
||||
],
|
||||
"time": "2019-09-04T14:39:26+00:00"
|
||||
},
|
||||
{
|
||||
"name": "league/csv",
|
||||
"version": "9.4.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/thephpleague/csv.git",
|
||||
"reference": "bf83acc23a7d60978fce36a384f97bf9d7d2ea0c"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/thephpleague/csv/zipball/bf83acc23a7d60978fce36a384f97bf9d7d2ea0c",
|
||||
"reference": "bf83acc23a7d60978fce36a384f97bf9d7d2ea0c",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-dom": "*",
|
||||
"ext-json": "*",
|
||||
"ext-mbstring": "*",
|
||||
"php": ">=7.0.10"
|
||||
},
|
||||
"require-dev": {
|
||||
"ext-curl": "*",
|
||||
"friendsofphp/php-cs-fixer": "^2.12",
|
||||
"phpstan/phpstan": "^0.9.2",
|
||||
"phpstan/phpstan-phpunit": "^0.9.4",
|
||||
"phpstan/phpstan-strict-rules": "^0.9.0",
|
||||
"phpunit/phpunit": "^6.0"
|
||||
},
|
||||
"suggest": {
|
||||
"ext-iconv": "Needed to ease transcoding CSV using iconv stream filters"
|
||||
},
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "9.x-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"League\\Csv\\": "src"
|
||||
},
|
||||
"files": [
|
||||
"src/functions_include.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Ignace Nyamagana Butera",
|
||||
"email": "nyamsprod@gmail.com",
|
||||
"homepage": "https://github.com/nyamsprod/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Csv data manipulation made easy in PHP",
|
||||
"homepage": "http://csv.thephpleague.com",
|
||||
"keywords": [
|
||||
"csv",
|
||||
"export",
|
||||
"filter",
|
||||
"import",
|
||||
"read",
|
||||
"write"
|
||||
],
|
||||
"time": "2019-10-17T06:05:32+00:00"
|
||||
},
|
||||
{
|
||||
"name": "markbaker/complex",
|
||||
"version": "1.4.7",
|
||||
@@ -3668,16 +3740,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nelmio/api-doc-bundle",
|
||||
"version": "v3.4.0",
|
||||
"version": "v3.5.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nelmio/NelmioApiDocBundle.git",
|
||||
"reference": "279d20be784b9879ad0a12859efa3a301404f06c"
|
||||
"reference": "f596adfb4d16e65d1a1941f907092a119d4c76cb"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/279d20be784b9879ad0a12859efa3a301404f06c",
|
||||
"reference": "279d20be784b9879ad0a12859efa3a301404f06c",
|
||||
"url": "https://api.github.com/repos/nelmio/NelmioApiDocBundle/zipball/f596adfb4d16e65d1a1941f907092a119d4c76cb",
|
||||
"reference": "f596adfb4d16e65d1a1941f907092a119d4c76cb",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -3689,13 +3761,17 @@
|
||||
"symfony/property-info": "^3.4|^4.0",
|
||||
"zircote/swagger-php": "^2.0.9"
|
||||
},
|
||||
"conflict": {
|
||||
"symfony/framework-bundle": "4.2.7"
|
||||
},
|
||||
"require-dev": {
|
||||
"api-platform/core": "^2.1.0",
|
||||
"doctrine/annotations": "^1.2",
|
||||
"doctrine/common": "^2.4",
|
||||
"friendsofsymfony/rest-bundle": "^2.0",
|
||||
"jms/serializer": "^1.14|^3.0",
|
||||
"jms/serializer-bundle": "^2.0|^3.0",
|
||||
"sensio/framework-extra-bundle": "^3.0",
|
||||
"sensio/framework-extra-bundle": "^3.0.13|^4.0|^5.0",
|
||||
"symfony/asset": "^3.4|^4.0",
|
||||
"symfony/browser-kit": "^3.4|^4.0",
|
||||
"symfony/cache": "^3.4|^4.0",
|
||||
@@ -3703,8 +3779,9 @@
|
||||
"symfony/console": "^3.4|^4.0",
|
||||
"symfony/dom-crawler": "^3.4|^4.0",
|
||||
"symfony/form": "^3.4|^4.0",
|
||||
"symfony/phpunit-bridge": "^3.4|^4.0",
|
||||
"symfony/phpunit-bridge": "^3.4.24|^4.0",
|
||||
"symfony/property-access": "^3.4|^4.0",
|
||||
"symfony/routing": "^3.4|^4.0",
|
||||
"symfony/stopwatch": "^3.4|^4.0",
|
||||
"symfony/templating": "^3.4|^4.0",
|
||||
"symfony/twig-bundle": "^3.4|^4.0",
|
||||
@@ -3747,7 +3824,7 @@
|
||||
"documentation",
|
||||
"rest"
|
||||
],
|
||||
"time": "2019-02-18T16:27:10+00:00"
|
||||
"time": "2019-11-21T17:18:16+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nelmio/cors-bundle",
|
||||
@@ -3809,22 +3886,22 @@
|
||||
},
|
||||
{
|
||||
"name": "nesbot/carbon",
|
||||
"version": "2.26.0",
|
||||
"version": "2.27.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/briannesbitt/Carbon.git",
|
||||
"reference": "e01ecc0b71168febb52ae1fdc1cfcc95428e604e"
|
||||
"reference": "13b8485a8690f103bf19cba64879c218b102b726"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/e01ecc0b71168febb52ae1fdc1cfcc95428e604e",
|
||||
"reference": "e01ecc0b71168febb52ae1fdc1cfcc95428e604e",
|
||||
"url": "https://api.github.com/repos/briannesbitt/Carbon/zipball/13b8485a8690f103bf19cba64879c218b102b726",
|
||||
"reference": "13b8485a8690f103bf19cba64879c218b102b726",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"ext-json": "*",
|
||||
"php": "^7.1.8 || ^8.0",
|
||||
"symfony/translation": "^3.4 || ^4.0"
|
||||
"symfony/translation": "^3.4 || ^4.0 || ^5.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"friendsofphp/php-cs-fixer": "^2.14 || ^3.0",
|
||||
@@ -3839,6 +3916,9 @@
|
||||
],
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "2.x-dev"
|
||||
},
|
||||
"laravel": {
|
||||
"providers": [
|
||||
"Carbon\\Laravel\\ServiceProvider"
|
||||
@@ -3872,7 +3952,7 @@
|
||||
"datetime",
|
||||
"time"
|
||||
],
|
||||
"time": "2019-10-21T21:32:25+00:00"
|
||||
"time": "2019-11-20T06:59:06+00:00"
|
||||
},
|
||||
{
|
||||
"name": "ocramius/package-versions",
|
||||
@@ -4351,16 +4431,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpspreadsheet",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
|
||||
"reference": "8dea03eaf60a349b6097e4bcad11f894668280df"
|
||||
"reference": "f734783d826bd84c3d54fcf7b71c37ab9bac4b04"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/8dea03eaf60a349b6097e4bcad11f894668280df",
|
||||
"reference": "8dea03eaf60a349b6097e4bcad11f894668280df",
|
||||
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/f734783d826bd84c3d54fcf7b71c37ab9bac4b04",
|
||||
"reference": "f734783d826bd84c3d54fcf7b71c37ab9bac4b04",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -4378,19 +4458,19 @@
|
||||
"ext-zip": "*",
|
||||
"ext-zlib": "*",
|
||||
"markbaker/complex": "^1.4",
|
||||
"markbaker/matrix": "^1.1",
|
||||
"markbaker/matrix": "^1.2",
|
||||
"php": "^7.1",
|
||||
"psr/simple-cache": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"dompdf/dompdf": "^0.8.0",
|
||||
"friendsofphp/php-cs-fixer": "@stable",
|
||||
"dompdf/dompdf": "^0.8.3",
|
||||
"friendsofphp/php-cs-fixer": "^2.16",
|
||||
"jpgraph/jpgraph": "^4.0",
|
||||
"mpdf/mpdf": "^7.0.0",
|
||||
"phpcompatibility/php-compatibility": "^8.0",
|
||||
"mpdf/mpdf": "^8.0",
|
||||
"phpcompatibility/php-compatibility": "^9.3",
|
||||
"phpunit/phpunit": "^7.5",
|
||||
"squizlabs/php_codesniffer": "^3.3",
|
||||
"tecnickcom/tcpdf": "^6.2"
|
||||
"squizlabs/php_codesniffer": "^3.5",
|
||||
"tecnickcom/tcpdf": "^6.3"
|
||||
},
|
||||
"suggest": {
|
||||
"dompdf/dompdf": "Option for rendering PDF with PDF Writer",
|
||||
@@ -4406,7 +4486,7 @@
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"LGPL-2.1-or-later"
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
@@ -4440,7 +4520,7 @@
|
||||
"xls",
|
||||
"xlsx"
|
||||
],
|
||||
"time": "2019-08-17T22:24:35+00:00"
|
||||
"time": "2019-11-18T11:33:05+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpoffice/phpword",
|
||||
@@ -8941,26 +9021,29 @@
|
||||
},
|
||||
{
|
||||
"name": "white-october/pagerfanta-bundle",
|
||||
"version": "v1.2.4",
|
||||
"version": "v1.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/whiteoctober/WhiteOctoberPagerfantaBundle.git",
|
||||
"reference": "19bf14bf8c72e4205c9ca97028436701fe3991a6"
|
||||
"reference": "5421d5e79b72495b34b745d37e4d341e6fa2190d"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/whiteoctober/WhiteOctoberPagerfantaBundle/zipball/19bf14bf8c72e4205c9ca97028436701fe3991a6",
|
||||
"reference": "19bf14bf8c72e4205c9ca97028436701fe3991a6",
|
||||
"url": "https://api.github.com/repos/whiteoctober/WhiteOctoberPagerfantaBundle/zipball/5421d5e79b72495b34b745d37e4d341e6fa2190d",
|
||||
"reference": "5421d5e79b72495b34b745d37e4d341e6fa2190d",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"pagerfanta/pagerfanta": "^1.1.0|^2.0.0",
|
||||
"php": ">=5.3",
|
||||
"php": ">=5.3.3",
|
||||
"symfony/framework-bundle": "~2.3|~3.0|~4.0",
|
||||
"symfony/property-access": "~2.3|~3.0|~4.0",
|
||||
"symfony/translation": "~2.3|~3.0|~4.0",
|
||||
"symfony/twig-bundle": "~2.3|~3.0|~4.0"
|
||||
},
|
||||
"conflict": {
|
||||
"twig/twig": "<1.34|>=2.0,<2.4"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "~3.7|~4.0|^5.0",
|
||||
"symfony/symfony": "~2.3|~3.0|~4.0"
|
||||
@@ -8995,7 +9078,7 @@
|
||||
"page",
|
||||
"paging"
|
||||
],
|
||||
"time": "2019-02-14T08:42:52+00:00"
|
||||
"time": "2019-11-21T17:13:04+00:00"
|
||||
},
|
||||
{
|
||||
"name": "willdurand/jsonp-callback-validator",
|
||||
@@ -10062,16 +10145,16 @@
|
||||
},
|
||||
{
|
||||
"name": "nette/php-generator",
|
||||
"version": "v3.2.3",
|
||||
"version": "v3.3.1",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/nette/php-generator.git",
|
||||
"reference": "aea6e81437bb238e5f0e5b5ce06337433908e63b"
|
||||
"reference": "4240fd7adf499138c07b814ef9b9a6df9f6d7187"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/nette/php-generator/zipball/aea6e81437bb238e5f0e5b5ce06337433908e63b",
|
||||
"reference": "aea6e81437bb238e5f0e5b5ce06337433908e63b",
|
||||
"url": "https://api.github.com/repos/nette/php-generator/zipball/4240fd7adf499138c07b814ef9b9a6df9f6d7187",
|
||||
"reference": "4240fd7adf499138c07b814ef9b9a6df9f6d7187",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10085,7 +10168,7 @@
|
||||
"type": "library",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "3.2-dev"
|
||||
"dev-master": "3.3-dev"
|
||||
}
|
||||
},
|
||||
"autoload": {
|
||||
@@ -10117,7 +10200,7 @@
|
||||
"php",
|
||||
"scaffolding"
|
||||
],
|
||||
"time": "2019-07-05T13:01:56+00:00"
|
||||
"time": "2019-11-22T11:12:11+00:00"
|
||||
},
|
||||
{
|
||||
"name": "nette/robot-loader",
|
||||
@@ -10897,16 +10980,16 @@
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-code-coverage",
|
||||
"version": "7.0.8",
|
||||
"version": "7.0.10",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
|
||||
"reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f"
|
||||
"reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/aa0d179a13284c7420fc281fc32750e6cc7c9e2f",
|
||||
"reference": "aa0d179a13284c7420fc281fc32750e6cc7c9e2f",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/f1884187926fbb755a9aaf0b3836ad3165b478bf",
|
||||
"reference": "f1884187926fbb755a9aaf0b3836ad3165b478bf",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -10956,7 +11039,7 @@
|
||||
"testing",
|
||||
"xunit"
|
||||
],
|
||||
"time": "2019-09-17T06:24:36+00:00"
|
||||
"time": "2019-11-20T13:55:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "phpunit/php-file-iterator",
|
||||
@@ -11397,16 +11480,16 @@
|
||||
},
|
||||
{
|
||||
"name": "sebastian/environment",
|
||||
"version": "4.2.2",
|
||||
"version": "4.2.3",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/sebastianbergmann/environment.git",
|
||||
"reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404"
|
||||
"reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
|
||||
"reference": "f2a2c8e1c97c11ace607a7a667d73d47c19fe404",
|
||||
"url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
|
||||
"reference": "464c90d7bdf5ad4e8a6aea15c091fec0603d4368",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -11446,7 +11529,7 @@
|
||||
"environment",
|
||||
"hhvm"
|
||||
],
|
||||
"time": "2019-05-05T09:05:15+00:00"
|
||||
"time": "2019-11-20T08:46:58+00:00"
|
||||
},
|
||||
{
|
||||
"name": "sebastian/exporter",
|
||||
@@ -12088,16 +12171,16 @@
|
||||
},
|
||||
{
|
||||
"name": "symfony/phpunit-bridge",
|
||||
"version": "v4.3.8",
|
||||
"version": "v4.4.0",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/phpunit-bridge.git",
|
||||
"reference": "c216b32261358a820bb4217eb3a20e3f437a484e"
|
||||
"reference": "7daa9d4a2f457f9cae32f1c2406de1921dc86a79"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/c216b32261358a820bb4217eb3a20e3f437a484e",
|
||||
"reference": "c216b32261358a820bb4217eb3a20e3f437a484e",
|
||||
"url": "https://api.github.com/repos/symfony/phpunit-bridge/zipball/7daa9d4a2f457f9cae32f1c2406de1921dc86a79",
|
||||
"reference": "7daa9d4a2f457f9cae32f1c2406de1921dc86a79",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
@@ -12107,7 +12190,7 @@
|
||||
"phpunit/phpunit": "<4.8.35|<5.4.3,>=5.0"
|
||||
},
|
||||
"suggest": {
|
||||
"symfony/debug": "For tracking deprecated interfaces usages at runtime with DebugClassLoader"
|
||||
"symfony/error-handler": "For tracking deprecated interfaces usages at runtime with DebugClassLoader"
|
||||
},
|
||||
"bin": [
|
||||
"bin/simple-phpunit"
|
||||
@@ -12115,7 +12198,7 @@
|
||||
"type": "symfony-bridge",
|
||||
"extra": {
|
||||
"branch-alias": {
|
||||
"dev-master": "4.3-dev"
|
||||
"dev-master": "4.4-dev"
|
||||
},
|
||||
"thanks": {
|
||||
"name": "phpunit/phpunit",
|
||||
@@ -12149,7 +12232,7 @@
|
||||
],
|
||||
"description": "Symfony PHPUnit Bridge",
|
||||
"homepage": "https://symfony.com",
|
||||
"time": "2019-10-30T12:58:49+00:00"
|
||||
"time": "2019-11-08T15:59:14+00:00"
|
||||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
|
||||
338
src/Command/ImportProjectCommand.php
Normal file
338
src/Command/ImportProjectCommand.php
Normal file
@@ -0,0 +1,338 @@
|
||||
<?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\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 Symfony\Component\Console\Command\Command;
|
||||
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!
|
||||
*
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ImportProjectCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'kimai:import:customer';
|
||||
|
||||
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)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->customers = $customers;
|
||||
$this->projects = $projects;
|
||||
$this->teams = $teams;
|
||||
$this->users = $users;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName(self::$defaultName)
|
||||
->setDescription('Import projects 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
|
||||
)
|
||||
->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')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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');
|
||||
|
||||
$csvFile = $input->getArgument('file');
|
||||
if (!file_exists($csvFile)) {
|
||||
$io->error('File not existing: ' . $csvFile);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!is_readable($csvFile)) {
|
||||
$io->error('File cannot be read: ' . $csvFile);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
$this->dateTime = (new \DateTime())->format('Y.m.d H:i');
|
||||
$this->comment = sprintf($input->getOption('comment'), $this->dateTime);
|
||||
|
||||
$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;
|
||||
|
||||
foreach ($records as $record) {
|
||||
try {
|
||||
$this->validateRow($record);
|
||||
} catch (InvalidFieldsException $ex) {
|
||||
$io->error(sprintf('Invalid row %s, invalid fields: %s', $row, implode(', ', $ex->getFields())));
|
||||
$doImport = false;
|
||||
$errors++;
|
||||
}
|
||||
|
||||
$row++;
|
||||
}
|
||||
|
||||
if (!$doImport) {
|
||||
$io->caution(sprintf('Not importing, previous %s errors need to be fixed first.', $errors));
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
$row = 0;
|
||||
foreach ($records as $record) {
|
||||
$row++;
|
||||
try {
|
||||
$customer = $this->getCustomer($record['Customer']);
|
||||
$projectName = $record['Name'];
|
||||
|
||||
$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);
|
||||
}
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(sprintf('Failed importing project row %s with: %s', $row, $ex->getMessage()));
|
||||
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
$io->success(sprintf('Imported %s rows', $row));
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
515
src/Command/ImportTimesheetCommand.php
Normal file
515
src/Command/ImportTimesheetCommand.php
Normal file
@@ -0,0 +1,515 @@
|
||||
<?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\Configuration\FormConfiguration;
|
||||
use App\Entity\Activity;
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\Project;
|
||||
use App\Entity\Tag;
|
||||
use App\Entity\Timesheet;
|
||||
use App\Entity\User;
|
||||
use App\Importer\InvalidFieldsException;
|
||||
use App\Importer\UnknownUserException;
|
||||
use App\Repository\ActivityRepository;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\ProjectRepository;
|
||||
use App\Repository\TimesheetRepository;
|
||||
use App\Repository\UserRepository;
|
||||
use App\Utils\Duration;
|
||||
use League\Csv\Reader;
|
||||
use Symfony\Component\Console\Command\Command;
|
||||
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!
|
||||
*
|
||||
* @internal
|
||||
* @codeCoverageIgnore
|
||||
*/
|
||||
class ImportTimesheetCommand extends Command
|
||||
{
|
||||
protected static $defaultName = 'kimai:import:timesheet';
|
||||
|
||||
public const DEFAULT_BEGIN = '00:00';
|
||||
public const DEFAULT_CUSTOMER = 'Imported customer - %s';
|
||||
|
||||
private static $supportedHeader = [
|
||||
'Date',
|
||||
'From',
|
||||
'To',
|
||||
'Duration',
|
||||
'Rate',
|
||||
'User',
|
||||
'Customer',
|
||||
'Project',
|
||||
'Activity',
|
||||
'Description',
|
||||
'Exported',
|
||||
'Tags',
|
||||
'Hourly rate',
|
||||
'Fixed rate',
|
||||
];
|
||||
|
||||
/**
|
||||
* @var CustomerRepository
|
||||
*/
|
||||
private $customers;
|
||||
/**
|
||||
* @var ProjectRepository
|
||||
*/
|
||||
private $projects;
|
||||
/**
|
||||
* @var ActivityRepository
|
||||
*/
|
||||
private $activities;
|
||||
/**
|
||||
* @var UserRepository
|
||||
*/
|
||||
private $users;
|
||||
/**
|
||||
* @var TimesheetRepository
|
||||
*/
|
||||
private $timesheets;
|
||||
/**
|
||||
* @var FormConfiguration
|
||||
*/
|
||||
private $configuration;
|
||||
/**
|
||||
* @var Customer
|
||||
*/
|
||||
private $customerFallback;
|
||||
/**
|
||||
* @var Customer[]
|
||||
*/
|
||||
private $customerCache = [];
|
||||
/**
|
||||
* @var User[]
|
||||
*/
|
||||
private $userCache = [];
|
||||
/**
|
||||
* Comment that will be added to new customers, projects and activities.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $comment = '';
|
||||
/**
|
||||
* The datetime of this import as formatted string.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private $dateTime = '';
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $begin = self::DEFAULT_BEGIN;
|
||||
|
||||
public function __construct(CustomerRepository $customers, ProjectRepository $projects, ActivityRepository $activities, UserRepository $users, TimesheetRepository $timesheets, FormConfiguration $configuration)
|
||||
{
|
||||
parent::__construct();
|
||||
$this->customers = $customers;
|
||||
$this->projects = $projects;
|
||||
$this->activities = $activities;
|
||||
$this->users = $users;
|
||||
$this->timesheets = $timesheets;
|
||||
$this->configuration = $configuration;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function configure()
|
||||
{
|
||||
$this
|
||||
->setName(self::$defaultName)
|
||||
->setDescription('Import timesheets from CSV file')
|
||||
->setHelp(
|
||||
'This command allows to import timesheets from a CSV file, which are formatted like CSV exports.' . PHP_EOL .
|
||||
'Imported customer, projects and activities will be matched by name.' . PHP_EOL .
|
||||
'Supported columns names: ' . implode(', ', self::$supportedHeader) . PHP_EOL
|
||||
)
|
||||
->addOption('timezone', null, InputOption::VALUE_OPTIONAL, 'The timezone to be used. Supports: "valid timezone names", the string "user" (using the configured users timezone) and the string "server" (PHP default timezone)', 'user')
|
||||
->addOption('customer', null, InputOption::VALUE_OPTIONAL, 'A customer ID or name to assign for empty entries. Defaults to creating a new customer which is used for all un-linked projects')
|
||||
->addOption('activity', null, InputOption::VALUE_OPTIONAL, 'Whether new activities should be "global" or "project" specific. Allowed values are "global" and "project"', 'project')
|
||||
->addOption('delimiter', null, InputOption::VALUE_OPTIONAL, 'The CSV field delimiter', ',')
|
||||
->addOption('begin', null, InputOption::VALUE_OPTIONAL, 'Default begin if none was provided in the format HH:MM', self::DEFAULT_BEGIN)
|
||||
->addOption('comment', null, InputOption::VALUE_OPTIONAL, 'A description to be added to created customers, projects and activities. %s will be replaced with the current datetime', 'Imported at %s')
|
||||
->addArgument('file', InputArgument::REQUIRED, 'The CSV file to be imported')
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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: Timesheets');
|
||||
|
||||
$csvFile = $input->getArgument('file');
|
||||
if (!file_exists($csvFile)) {
|
||||
$io->error('File not existing: ' . $csvFile);
|
||||
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!is_readable($csvFile)) {
|
||||
$io->error('File cannot be read: ' . $csvFile);
|
||||
|
||||
return 2;
|
||||
}
|
||||
|
||||
$this->dateTime = (new \DateTime())->format('Y.m.d H:i');
|
||||
$this->comment = sprintf($input->getOption('comment'), $this->dateTime);
|
||||
$this->begin = $input->getOption('begin');
|
||||
|
||||
$timezone = $input->getOption('timezone');
|
||||
switch ($timezone) {
|
||||
case 'server':
|
||||
$timezone = new \DateTimeZone(date_default_timezone_get());
|
||||
break;
|
||||
|
||||
case 'user':
|
||||
// null means fetch from user
|
||||
$timezone = null;
|
||||
break;
|
||||
|
||||
default:
|
||||
try {
|
||||
$timezone = new \DateTimeZone($timezone);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error('Invalid timezone given, import canceled.');
|
||||
|
||||
return 3;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
$activityType = $input->getOption('activity');
|
||||
$allowedActivityTypes = ['project', 'global'];
|
||||
if (!in_array($activityType, $allowedActivityTypes)) {
|
||||
$io->error(sprintf('Invalid activity type "%s" given, allowed values are: %s', $activityType, implode(', ', $allowedActivityTypes)));
|
||||
|
||||
return 4;
|
||||
}
|
||||
|
||||
$csv = Reader::createFromPath($csvFile, 'r');
|
||||
$csv->setDelimiter($input->getOption('delimiter'));
|
||||
$csv->setHeaderOffset(0);
|
||||
$header = $csv->getHeader();
|
||||
if (!$this->validateHeader($header)) {
|
||||
$io->error(
|
||||
sprintf(
|
||||
'Found invalid CSV. The header: ' . PHP_EOL .
|
||||
'%s' . PHP_EOL .
|
||||
'did not match the expected structure: ' . PHP_EOL .
|
||||
'%s',
|
||||
implode(', ', $header),
|
||||
implode(', ', self::$supportedHeader)
|
||||
)
|
||||
);
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
$records = $csv->getRecords();
|
||||
|
||||
$doImport = true;
|
||||
$row = 1;
|
||||
$errors = 0;
|
||||
|
||||
// ======================= validate rows =======================
|
||||
foreach ($records as $record) {
|
||||
try {
|
||||
$this->validateRow($record);
|
||||
} catch (InvalidFieldsException $ex) {
|
||||
$io->error(sprintf('Invalid row %s, invalid fields: %s', $row, implode(', ', $ex->getFields())));
|
||||
$doImport = false;
|
||||
$errors++;
|
||||
}
|
||||
|
||||
try {
|
||||
$user = $this->getUser($record['User']);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(sprintf('Unknown user %s in row %s', $record['User'], $row));
|
||||
$doImport = false;
|
||||
$errors++;
|
||||
}
|
||||
|
||||
$row++;
|
||||
}
|
||||
|
||||
if (!$doImport) {
|
||||
$io->caution(sprintf('Not importing, previous %s errors need to be fixed first.', $errors));
|
||||
|
||||
return 5;
|
||||
}
|
||||
|
||||
$durationParser = new Duration();
|
||||
$row = 0;
|
||||
foreach ($records as $record) {
|
||||
$row++;
|
||||
try {
|
||||
$project = $this->getProject($record['Project'], $record['Customer'], $input->getOption('customer'));
|
||||
$activity = $this->getActivity($record['Activity'], $project, $activityType);
|
||||
$user = $this->getUser($record['User']);
|
||||
|
||||
$begin = null;
|
||||
$end = null;
|
||||
$duration = 0;
|
||||
|
||||
if (!empty($record['Duration'])) {
|
||||
if (is_int($record['Duration'])) {
|
||||
$duration = $record['Duration'];
|
||||
} else {
|
||||
$duration = $durationParser->parseDurationString($record['Duration']);
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $timezone) {
|
||||
$timezone = new \DateTimeZone($user->getTimezone());
|
||||
}
|
||||
|
||||
if (empty($record['From']) && empty($record['To'])) {
|
||||
$begin = new \DateTime($record['Date'] . ' ' . $this->begin, $timezone);
|
||||
$end = (new \DateTime())->setTimezone($timezone)->setTimestamp($begin->getTimestamp() + $duration);
|
||||
} elseif (empty($record['From'])) {
|
||||
$end = new \DateTime($record['Date'] . ' ' . $record['To'], $timezone);
|
||||
$begin = (new \DateTime())->setTimezone($timezone)->setTimestamp($end->getTimestamp() - $duration);
|
||||
} elseif (empty($record['To'])) {
|
||||
$begin = new \DateTime($record['Date'] . ' ' . $record['From'], $timezone);
|
||||
$end = (new \DateTime())->setTimezone($timezone)->setTimestamp($begin->getTimestamp() + $duration);
|
||||
} else {
|
||||
$begin = new \DateTime($record['Date'] . ' ' . $record['From'], $timezone);
|
||||
$end = new \DateTime($record['Date'] . ' ' . $record['To'], $timezone);
|
||||
}
|
||||
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setActivity($activity);
|
||||
$timesheet->setProject($project);
|
||||
$timesheet->setBegin($begin);
|
||||
$timesheet->setEnd($end);
|
||||
$timesheet->setUser($user);
|
||||
$timesheet->setDescription($record['Description']);
|
||||
$timesheet->setExported((bool) $record['Exported']);
|
||||
|
||||
if (!empty($record['Tags'])) {
|
||||
foreach (explode(',', $record['Tags']) as $tag) {
|
||||
if (empty($tag)) {
|
||||
continue;
|
||||
}
|
||||
$timesheet->addTag((new Tag())->setName($tag));
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($record['Rate'])) {
|
||||
$timesheet->setRate($record['Rate']);
|
||||
}
|
||||
if (!empty($record['Hourly rate'])) {
|
||||
$timesheet->setHourlyRate($record['Hourly rate']);
|
||||
}
|
||||
if (!empty($record['Fixed rate'])) {
|
||||
$timesheet->setFixedRate($record['Fixed rate']);
|
||||
}
|
||||
|
||||
$this->timesheets->save($timesheet);
|
||||
} catch (\Exception $ex) {
|
||||
$io->error(sprintf('Failed importing timesheet row %s with: %s', $row, $ex->getMessage()));
|
||||
|
||||
return 6;
|
||||
}
|
||||
}
|
||||
|
||||
$io->success(sprintf('Imported %s rows', $row));
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private function getUser($user): User
|
||||
{
|
||||
if (!array_key_exists($user, $this->userCache)) {
|
||||
$tmpUser = $this->users->findOneBy(['username' => $user]);
|
||||
if (null === $tmpUser) {
|
||||
$tmpUser = $this->users->findOneBy(['email' => $user]);
|
||||
if (null === $tmpUser) {
|
||||
throw new UnknownUserException($user);
|
||||
}
|
||||
}
|
||||
$this->userCache[$user] = $tmpUser;
|
||||
}
|
||||
|
||||
return $this->userCache[$user];
|
||||
}
|
||||
|
||||
private function getActivity($activity, Project $project, $activityType): Activity
|
||||
{
|
||||
$tmpActivity = null;
|
||||
|
||||
$tmpActivities = $this->activities->findBy(['project' => $project, 'name' => $activity]);
|
||||
|
||||
if (count($tmpActivities) === 0) {
|
||||
$tmpActivity = $this->activities->findOneBy(['project' => null, 'name' => $activity]);
|
||||
} elseif (count($tmpActivities) === 1) {
|
||||
$tmpActivity = $tmpActivities[0];
|
||||
}
|
||||
|
||||
if (null === $tmpActivity) {
|
||||
$tmpActivity = new Activity();
|
||||
$tmpActivity->setName($activity);
|
||||
$tmpActivity->setComment($this->comment);
|
||||
if ($activityType === 'project') {
|
||||
$tmpActivity->setProject($project);
|
||||
}
|
||||
$this->activities->saveActivity($tmpActivity);
|
||||
}
|
||||
|
||||
return $tmpActivity;
|
||||
}
|
||||
|
||||
private function getProject($project, $customer, $fallbackCustomer): Project
|
||||
{
|
||||
/** @var Customer $tmpCustomer */
|
||||
$tmpCustomer = $this->getCustomer($customer, $fallbackCustomer);
|
||||
/** @var Project $tmpProject */
|
||||
$tmpProject = null;
|
||||
/** @var Project[] $tmpProjects */
|
||||
$tmpProjects = $this->projects->findBy(['name' => $project]);
|
||||
|
||||
if (count($tmpProjects) > 1) {
|
||||
/** @var Project $prj */
|
||||
foreach ($tmpProjects as $prj) {
|
||||
if ($prj->getCustomer()->getName() !== $tmpCustomer->getName()) {
|
||||
continue;
|
||||
}
|
||||
$tmpProject = $prj;
|
||||
break;
|
||||
}
|
||||
} elseif (count($tmpProjects) === 1) {
|
||||
$tmpProject = $tmpProjects[0];
|
||||
}
|
||||
|
||||
if (null !== $tmpProject) {
|
||||
if ($tmpProject->getCustomer()->getName() !== $tmpCustomer->getName()) {
|
||||
$tmpProject = null;
|
||||
}
|
||||
}
|
||||
|
||||
if ($tmpProject === null) {
|
||||
$tmpProject = new Project();
|
||||
$tmpProject->setName($project);
|
||||
$tmpProject->setComment($this->comment);
|
||||
$tmpProject->setCustomer($tmpCustomer);
|
||||
$this->projects->saveProject($tmpProject);
|
||||
}
|
||||
|
||||
return $tmpProject;
|
||||
}
|
||||
|
||||
private function getCustomer($customer, $fallback): Customer
|
||||
{
|
||||
if (!empty($customer)) {
|
||||
if (!array_key_exists($customer, $this->customerCache)) {
|
||||
$tmpCustomer = $this->customers->findBy(['name' => $customer]);
|
||||
if (count($tmpCustomer) > 1) {
|
||||
throw new \Exception(sprintf('Found multiple customers with the name: %s', $customer));
|
||||
} elseif (count($tmpCustomer) === 1) {
|
||||
$tmpCustomer = $tmpCustomer[0];
|
||||
}
|
||||
|
||||
if ($tmpCustomer instanceof Customer) {
|
||||
$this->customerCache[$customer] = $tmpCustomer;
|
||||
}
|
||||
}
|
||||
|
||||
if (array_key_exists($customer, $this->customerCache)) {
|
||||
return $this->customerCache[$customer];
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $this->customerFallback) {
|
||||
$tmpFallback = null;
|
||||
|
||||
if (!empty($fallback)) {
|
||||
if (is_int($customer)) {
|
||||
$tmpFallback = $this->customers->find($fallback);
|
||||
} else {
|
||||
$tmpFallback = $this->customers->findOneBy(['name' => $fallback]);
|
||||
}
|
||||
}
|
||||
|
||||
if (null === $tmpFallback) {
|
||||
$newName = self::DEFAULT_CUSTOMER;
|
||||
if (!empty($fallback) && is_string($fallback)) {
|
||||
$newName = $fallback;
|
||||
}
|
||||
$tmpFallback = new Customer();
|
||||
$tmpFallback->setName(sprintf($newName, $this->dateTime));
|
||||
$tmpFallback->setComment($this->comment);
|
||||
$tmpFallback->setCountry($this->configuration->getCustomerDefaultCountry());
|
||||
$timezone = date_default_timezone_get();
|
||||
if (null !== $this->configuration->getCustomerDefaultTimezone()) {
|
||||
$timezone = $this->configuration->getCustomerDefaultTimezone();
|
||||
}
|
||||
$tmpFallback->setTimezone($timezone);
|
||||
$this->customers->saveCustomer($tmpFallback);
|
||||
}
|
||||
|
||||
$this->customerFallback = $tmpFallback;
|
||||
}
|
||||
|
||||
return $this->customerFallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array $row
|
||||
* @return bool
|
||||
* @throws InvalidFieldsException
|
||||
*/
|
||||
private function validateRow(array $row)
|
||||
{
|
||||
$fields = [];
|
||||
|
||||
if (empty($row['Project'])) {
|
||||
$fields[] = 'Project';
|
||||
}
|
||||
|
||||
if (empty($row['Activity'])) {
|
||||
$fields[] = 'Activity';
|
||||
}
|
||||
|
||||
if (empty($row['Date'])) {
|
||||
$fields[] = 'Date';
|
||||
}
|
||||
|
||||
if ((empty($row['From']) || empty($row['To'])) && empty($row['Duration'])) {
|
||||
$fields[] = 'Duration';
|
||||
}
|
||||
|
||||
if (!empty($fields)) {
|
||||
throw new InvalidFieldsException($fields);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private function validateHeader(array $header)
|
||||
{
|
||||
$result = array_diff(self::$supportedHeader, $header);
|
||||
|
||||
return empty($result);
|
||||
}
|
||||
}
|
||||
28
src/Importer/InvalidFieldsException.php
Normal file
28
src/Importer/InvalidFieldsException.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?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 InvalidFieldsException extends \Exception
|
||||
{
|
||||
private $fields = [];
|
||||
|
||||
public function __construct(array $fields, $code = 0, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(sprintf('Missing fields: %s', implode(', ', $fields)), $code, $previous);
|
||||
$this->fields = $fields;
|
||||
}
|
||||
|
||||
public function getFields(): array
|
||||
{
|
||||
return $this->fields;
|
||||
}
|
||||
}
|
||||
31
src/Importer/UnknownUserException.php
Normal file
31
src/Importer/UnknownUserException.php
Normal file
@@ -0,0 +1,31 @@
|
||||
<?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 UnknownUserException extends \Exception
|
||||
{
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
private $username;
|
||||
|
||||
public function __construct(string $username, $code = 0, Throwable $previous = null)
|
||||
{
|
||||
parent::__construct(sprintf('Unknown user: %s', $username), $code, $previous);
|
||||
$this->username = $username;
|
||||
}
|
||||
|
||||
public function getUsername(): string
|
||||
{
|
||||
return $this->username;
|
||||
}
|
||||
}
|
||||
@@ -231,6 +231,9 @@
|
||||
"laravolt/avatar": {
|
||||
"version": "3.0.0"
|
||||
},
|
||||
"league/csv": {
|
||||
"version": "9.4.1"
|
||||
},
|
||||
"markbaker/complex": {
|
||||
"version": "1.4.6"
|
||||
},
|
||||
|
||||
26
tests/Importer/InvalidFieldsExceptionTest.php
Normal file
26
tests/Importer/InvalidFieldsExceptionTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Importer;
|
||||
|
||||
use App\Importer\InvalidFieldsException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Importer\InvalidFieldsException
|
||||
*/
|
||||
class InvalidFieldsExceptionTest extends TestCase
|
||||
{
|
||||
public function testException()
|
||||
{
|
||||
$sut = new InvalidFieldsException(['test', 'foo']);
|
||||
self::assertEquals(['test', 'foo'], $sut->getFields());
|
||||
self::assertEquals('Missing fields: test, foo', $sut->getMessage());
|
||||
}
|
||||
}
|
||||
26
tests/Importer/UnknownUserExceptionTest.php
Normal file
26
tests/Importer/UnknownUserExceptionTest.php
Normal file
@@ -0,0 +1,26 @@
|
||||
<?php
|
||||
|
||||
/*
|
||||
* This file is part of the Kimai time-tracking app.
|
||||
*
|
||||
* For the full copyright and license information, please view the LICENSE
|
||||
* file that was distributed with this source code.
|
||||
*/
|
||||
|
||||
namespace App\Tests\Importer;
|
||||
|
||||
use App\Importer\UnknownUserException;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Importer\UnknownUserException
|
||||
*/
|
||||
class UnknownUserExceptionTest extends TestCase
|
||||
{
|
||||
public function testException()
|
||||
{
|
||||
$sut = new UnknownUserException('test');
|
||||
self::assertEquals('test', $sut->getUsername());
|
||||
self::assertEquals('Unknown user: test', $sut->getMessage());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user