From 9f8c1616f5fdf3aa164e92377f6a163762707fdd Mon Sep 17 00:00:00 2001 From: Lio Novelli Date: Sat, 30 Apr 2022 14:55:18 +0200 Subject: [PATCH] Add option for custom work and expenses. --- README.org | 16 + app/composer.json | 2 +- app/composer.lock | 404 +++++++++++++----- app/config/invoice-template.html | 66 +-- app/dependencies.php | 10 +- app/src/Commands/RprtCommand.php | 152 ++++++- app/src/Utils/CsvReport/CsvReport.php | 5 +- app/src/Utils/CsvReport/ReportCsv.php | 199 +++++++++ .../Utils/CsvReport/ReportCsvInterface.php | 36 ++ app/src/Utils/PdfExport/PdfExportService.php | 20 +- app/src/ValueObjects/Expenses.php | 33 ++ app/src/ValueObjects/ExpensesInterface.php | 11 + .../ValueObjects/InvoiceElementInterface.php | 17 + app/src/ValueObjects/WorkInvoiceElement.php | 29 ++ .../WorkInvoiceElementInterface.php | 15 + 15 files changed, 856 insertions(+), 159 deletions(-) create mode 100644 app/src/Utils/CsvReport/ReportCsv.php create mode 100644 app/src/Utils/CsvReport/ReportCsvInterface.php create mode 100644 app/src/ValueObjects/Expenses.php create mode 100644 app/src/ValueObjects/ExpensesInterface.php create mode 100644 app/src/ValueObjects/InvoiceElementInterface.php create mode 100644 app/src/ValueObjects/WorkInvoiceElement.php create mode 100644 app/src/ValueObjects/WorkInvoiceElementInterface.php diff --git a/README.org b/README.org index 36d1e12..2aa6e9a 100644 --- a/README.org +++ b/README.org @@ -47,6 +47,10 @@ - Create an invoice - send the invoice (to Wolfgang) +*** ValueObjects + + + ** Motivation - practice php - automatization of repetitive task @@ -58,6 +62,17 @@ ** Plan +*** most current + 1. For version 0.6.7 + - data value objects + - phar file + - additional expenses + 2. For version 1.0 + - plugin system + - for time tracking services + - for reports + - symfony/config & symfony/di components + *** current 1. For Version 1.0 @@ -94,6 +109,7 @@ 2. configuration wizard + ** Learning - https://www.youtube.com/watch?v=aCqM9YnjTe0 - Choices (~new ChoiceQuestion~) diff --git a/app/composer.json b/app/composer.json index 42153a7..5ab5900 100644 --- a/app/composer.json +++ b/app/composer.json @@ -2,7 +2,7 @@ "name": "lio/rprt-cli", "description": "Automate invoicing from youtrack service.", "type": "project", - "keywords": ["cli", "report"], + "keywords": ["cli", "report", "youtrack"], "readme": "README.org", "time": "2021-04-04", "license": "GPL-3.0-or-later", diff --git a/app/composer.lock b/app/composer.lock index 3291587..f07ba44 100644 --- a/app/composer.lock +++ b/app/composer.lock @@ -722,20 +722,20 @@ }, { "name": "psr/container", - "version": "1.1.1", + "version": "1.1.2", "source": { "type": "git", "url": "https://github.com/php-fig/container.git", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf" + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-fig/container/zipball/8622567409010282b7aeebe4bb841fe98b58dcaf", - "reference": "8622567409010282b7aeebe4bb841fe98b58dcaf", + "url": "https://api.github.com/repos/php-fig/container/zipball/513e0666f7216c7459170d56df27dfcefe1689ea", + "reference": "513e0666f7216c7459170d56df27dfcefe1689ea", "shasum": "" }, "require": { - "php": ">=7.2.0" + "php": ">=7.4.0" }, "type": "library", "autoload": { @@ -762,7 +762,11 @@ "container-interop", "psr" ], - "time": "2021-03-05T17:36:06+00:00" + "support": { + "issues": "https://github.com/php-fig/container/issues", + "source": "https://github.com/php-fig/container/tree/1.1.2" + }, + "time": "2021-11-05T16:50:12+00:00" }, { "name": "psr/event-dispatcher", @@ -1064,27 +1068,29 @@ }, { "name": "symfony/console", - "version": "v5.2.6", + "version": "v5.4.8", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d" + "reference": "ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/35f039df40a3b335ebf310f244cb242b3a83ac8d", - "reference": "35f039df40a3b335ebf310f244cb242b3a83ac8d", + "url": "https://api.github.com/repos/symfony/console/zipball/ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b", + "reference": "ffe3aed36c4d60da2cf1b0a1cee6b8f2e5fa881b", "shasum": "" }, "require": { "php": ">=7.2.5", + "symfony/deprecation-contracts": "^2.1|^3", "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php73": "^1.8", - "symfony/polyfill-php80": "^1.15", - "symfony/service-contracts": "^1.1|^2", - "symfony/string": "^5.1" + "symfony/polyfill-php73": "^1.9", + "symfony/polyfill-php80": "^1.16", + "symfony/service-contracts": "^1.1|^2|^3", + "symfony/string": "^5.1|^6.0" }, "conflict": { + "psr/log": ">=3", "symfony/dependency-injection": "<4.4", "symfony/dotenv": "<5.1", "symfony/event-dispatcher": "<4.4", @@ -1092,16 +1098,16 @@ "symfony/process": "<4.4" }, "provide": { - "psr/log-implementation": "1.0" + "psr/log-implementation": "1.0|2.0" }, "require-dev": { - "psr/log": "~1.0", - "symfony/config": "^4.4|^5.0", - "symfony/dependency-injection": "^4.4|^5.0", - "symfony/event-dispatcher": "^4.4|^5.0", - "symfony/lock": "^4.4|^5.0", - "symfony/process": "^4.4|^5.0", - "symfony/var-dumper": "^4.4|^5.0" + "psr/log": "^1|^2", + "symfony/config": "^4.4|^5.0|^6.0", + "symfony/dependency-injection": "^4.4|^5.0|^6.0", + "symfony/event-dispatcher": "^4.4|^5.0|^6.0", + "symfony/lock": "^4.4|^5.0|^6.0", + "symfony/process": "^4.4|^5.0|^6.0", + "symfony/var-dumper": "^4.4|^5.0|^6.0" }, "suggest": { "psr/log": "For using the console logger", @@ -1140,20 +1146,37 @@ "console", "terminal" ], - "time": "2021-03-28T09:42:18+00:00" + "support": { + "source": "https://github.com/symfony/console/tree/v5.4.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-12T16:02:29+00:00" }, { "name": "symfony/deprecation-contracts", - "version": "v2.2.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665" + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/5fa56b4074d1ae755beb55617ddafe6f5d78f665", - "reference": "5fa56b4074d1ae755beb55617ddafe6f5d78f665", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/e8b495ea28c1d97b5e0c121748d6f9b53d075c66", + "reference": "e8b495ea28c1d97b5e0c121748d6f9b53d075c66", "shasum": "" }, "require": { @@ -1162,7 +1185,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -1190,7 +1213,24 @@ ], "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", - "time": "2020-09-07T11:33:47+00:00" + "support": { + "source": "https://github.com/symfony/deprecation-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-01-02T09:53:40+00:00" }, { "name": "symfony/event-dispatcher", @@ -1581,28 +1621,31 @@ }, { "name": "symfony/polyfill-ctype", - "version": "v1.22.1", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e" + "reference": "30885182c981ab175d4d034db0f6f469898070ab" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/c6c942b1ac76c82448322025e084cadc56048b4e", - "reference": "c6c942b1ac76c82448322025e084cadc56048b4e", + "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/30885182c981ab175d4d034db0f6f469898070ab", + "reference": "30885182c981ab175d4d034db0f6f469898070ab", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-ctype": "*" + }, "suggest": { "ext-ctype": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1610,12 +1653,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Ctype\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Ctype\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1639,20 +1682,37 @@ "polyfill", "portable" ], - "time": "2021-01-07T16:49:33+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-10-20T20:35:02+00:00" }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.22.1", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", - "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170" + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/5601e09b69f26c1828b13b6bb87cb07cddba3170", - "reference": "5601e09b69f26c1828b13b6bb87cb07cddba3170", + "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/81b86b50cf841a64252b439e738e97f4a34e2783", + "reference": "81b86b50cf841a64252b439e738e97f4a34e2783", "shasum": "" }, "require": { @@ -1664,7 +1724,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1672,12 +1732,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Grapheme\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Grapheme\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1703,7 +1763,24 @@ "portable", "shim" ], - "time": "2021-01-22T09:19:47+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-23T21:10:46+00:00" }, { "name": "symfony/polyfill-intl-idn", @@ -1794,16 +1871,16 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.22.1", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", - "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248" + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/43a0283138253ed1d48d352ab6d0bdb3f809f248", - "reference": "43a0283138253ed1d48d352ab6d0bdb3f809f248", + "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/8590a5f561694770bdcd3f9b5c69dde6945028e8", + "reference": "8590a5f561694770bdcd3f9b5c69dde6945028e8", "shasum": "" }, "require": { @@ -1815,7 +1892,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1823,12 +1900,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Intl\\Normalizer\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Intl\\Normalizer\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -1857,32 +1934,52 @@ "portable", "shim" ], - "time": "2021-01-22T09:19:47+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-02-19T12:13:01+00:00" }, { "name": "symfony/polyfill-mbstring", - "version": "v1.22.1", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1" + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/5232de97ee3b75b0360528dae24e73db49566ab1", - "reference": "5232de97ee3b75b0360528dae24e73db49566ab1", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/0abb51d2f102e00a4eefcf46ba7fec406d245825", + "reference": "0abb51d2f102e00a4eefcf46ba7fec406d245825", "shasum": "" }, "require": { "php": ">=7.1" }, + "provide": { + "ext-mbstring": "*" + }, "suggest": { "ext-mbstring": "For best performance" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -1890,12 +1987,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Mbstring\\": "" - }, "files": [ "bootstrap.php" - ] + ], + "psr-4": { + "Symfony\\Polyfill\\Mbstring\\": "" + } }, "notification-url": "https://packagist.org/downloads/", "license": [ @@ -1920,7 +2017,24 @@ "portable", "shim" ], - "time": "2021-01-22T09:19:47+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-11-30T18:21:41+00:00" }, { "name": "symfony/polyfill-php72", @@ -2000,16 +2114,16 @@ }, { "name": "symfony/polyfill-php73", - "version": "v1.22.1", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php73.git", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2" + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", - "reference": "a678b42e92f86eca04b7fa4c0f6f19d097fb69e2", + "url": "https://api.github.com/repos/symfony/polyfill-php73/zipball/cc5db0e22b3cb4111010e48785a97f670b350ca5", + "reference": "cc5db0e22b3cb4111010e48785a97f670b350ca5", "shasum": "" }, "require": { @@ -2018,7 +2132,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2026,12 +2140,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php73\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php73\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -2058,20 +2172,37 @@ "portable", "shim" ], - "time": "2021-01-07T16:49:33+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php73/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2021-06-05T21:20:04+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.22.1", + "version": "v1.25.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91" + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/dc3063ba22c2a1fd2f45ed856374d79114998f91", - "reference": "dc3063ba22c2a1fd2f45ed856374d79114998f91", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/4407588e0d3f1f52efb65fbe92babe41f37fe50c", + "reference": "4407588e0d3f1f52efb65fbe92babe41f37fe50c", "shasum": "" }, "require": { @@ -2080,7 +2211,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "1.22-dev" + "dev-main": "1.23-dev" }, "thanks": { "name": "symfony/polyfill", @@ -2088,12 +2219,12 @@ } }, "autoload": { - "psr-4": { - "Symfony\\Polyfill\\Php80\\": "" - }, "files": [ "bootstrap.php" ], + "psr-4": { + "Symfony\\Polyfill\\Php80\\": "" + }, "classmap": [ "Resources/stubs" ] @@ -2124,25 +2255,46 @@ "portable", "shim" ], - "time": "2021-01-07T16:49:33+00:00" + "support": { + "source": "https://github.com/symfony/polyfill-php80/tree/v1.25.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-04T08:16:47+00:00" }, { "name": "symfony/service-contracts", - "version": "v2.2.0", + "version": "v2.5.1", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1" + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/d15da7ba4957ffb8f1747218be9e1a121fd298a1", - "reference": "d15da7ba4957ffb8f1747218be9e1a121fd298a1", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/24d9dc654b83e91aa59f9d167b131bc3b5bea24c", + "reference": "24d9dc654b83e91aa59f9d167b131bc3b5bea24c", "shasum": "" }, "require": { "php": ">=7.2.5", - "psr/container": "^1.0" + "psr/container": "^1.1", + "symfony/deprecation-contracts": "^2.1|^3" + }, + "conflict": { + "ext-psr": "<1.1|>=2" }, "suggest": { "symfony/service-implementation": "" @@ -2150,7 +2302,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "2.2-dev" + "dev-main": "2.5-dev" }, "thanks": { "name": "symfony/contracts", @@ -2186,44 +2338,63 @@ "interoperability", "standards" ], - "time": "2020-09-07T11:33:47+00:00" + "support": { + "source": "https://github.com/symfony/service-contracts/tree/v2.5.1" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-03-13T20:07:29+00:00" }, { "name": "symfony/string", - "version": "v5.2.6", + "version": "v6.0.8", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572" + "reference": "ac0aa5c2282e0de624c175b68d13f2c8f2e2649d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", - "reference": "ad0bd91bce2054103f5eaa18ebeba8d3bc2a0572", + "url": "https://api.github.com/repos/symfony/string/zipball/ac0aa5c2282e0de624c175b68d13f2c8f2e2649d", + "reference": "ac0aa5c2282e0de624c175b68d13f2c8f2e2649d", "shasum": "" }, "require": { - "php": ">=7.2.5", + "php": ">=8.0.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", - "symfony/polyfill-mbstring": "~1.0", - "symfony/polyfill-php80": "~1.15" + "symfony/polyfill-mbstring": "~1.0" + }, + "conflict": { + "symfony/translation-contracts": "<2.0" }, "require-dev": { - "symfony/error-handler": "^4.4|^5.0", - "symfony/http-client": "^4.4|^5.0", - "symfony/translation-contracts": "^1.1|^2", - "symfony/var-exporter": "^4.4|^5.0" + "symfony/error-handler": "^5.4|^6.0", + "symfony/http-client": "^5.4|^6.0", + "symfony/translation-contracts": "^2.0|^3.0", + "symfony/var-exporter": "^5.4|^6.0" }, "type": "library", "autoload": { - "psr-4": { - "Symfony\\Component\\String\\": "" - }, "files": [ "Resources/functions.php" ], + "psr-4": { + "Symfony\\Component\\String\\": "" + }, "exclude-from-classmap": [ "/Tests/" ] @@ -2252,7 +2423,24 @@ "utf-8", "utf8" ], - "time": "2021-03-17T17:12:15+00:00" + "support": { + "source": "https://github.com/symfony/string/tree/v6.0.8" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2022-04-22T08:18:02+00:00" }, { "name": "symfony/yaml", diff --git a/app/config/invoice-template.html b/app/config/invoice-template.html index 085a757..8fe3083 100644 --- a/app/config/invoice-template.html +++ b/app/config/invoice-template.html @@ -23,11 +23,6 @@ border-left: 0.1mm solid #000000; border-right: 0.1mm solid #000000; } - table thead th { - background-color: #EEEEEE; - text-align: center; - border: 0.1mm solid #000000; - } table tbody td { background-color: #FFF; border: 0.1mm solid #000000; @@ -35,9 +30,26 @@ padding-left: 2mm; } tbody td.td-3, - tbody td.td-1 { + tbody td.td-1, + .right { text-align: right; } + /*tbody td.td-2,*/ + .center { + text-align: center; + } + table thead th, + table tr.bold td, + table tr.bold td.td-3 { + background-color: #EEEEEE; + border: 0.1mm solid #000000; + font-weight: bold; + } + table tr.center td, + table tr.center td.td-3 { + text-align: center!important; + } + /* tbody tr.tr-2 td, tbody tr.tr-3 td { background-color: #EEE; @@ -46,32 +58,30 @@ tbody tr.tr-3 td { font-weight: bold; } - tbody td.td-2, - .center { - text-align: center; - } - .right { - text-align: right; + */ + .left { + text-align: left; } -

My company name
My company address
123456789, mycompany@email.com
987654321

-

-

Other company
Other company address
1111 City
State

-

-

-

-

City, on [[today]]

-

Honorarnote
Nummer [[number]]

-

-

Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:

- [[table]] -

Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer [[account_number]] mit der Bankleitzahl ABCDSI33 zu überweisen.

-

Vielen Dank für den Auftrag,
+

Spletni razvoj, Liopold Doron Novelli s.p.,
Prešernova 23, 1236 Trzin
8638314000, liopold@drunomics.com
25752910

+



+

drunomics GmbH
Getriedmarkt 11/12
1060 Wien
Austria

+


+


+


+

Laibach, am [[today]]

+

Honorarnote
Nummer [[number]]

+


+

Für meine Tätigkeit Programmieren von [[date_start]] bis [[date_end]] erlaube ich mir, folgenden Betrag in Rechnung zu stellen:

+


+
[[table]]
+


Ich ersuche Sie höflich, den oben angeführt auf meine Kontonummer SI56 6100 0002 3993 491 mit der Bankleitzahl HDELSI22 zu überweisen.

+


Vielen Dank für den Auftrag,
mit besten Grüßen,

-

-

-

My name

+



+



+

Liopold Doron Novelli

diff --git a/app/dependencies.php b/app/dependencies.php index 5d8c5e2..aa723ec 100644 --- a/app/dependencies.php +++ b/app/dependencies.php @@ -8,6 +8,8 @@ use RprtCli\Utils\Configuration\ConfigurationInterface; use RprtCli\Utils\Configuration\ConfigurationService; use RprtCli\Utils\CsvReport\CsvReport; use RprtCli\Utils\CsvReport\CsvReportInterface; +use RprtCli\Utils\CsvReport\ReportCsv; +use RprtCli\Utils\CsvReport\ReportCsvInterface; use GuzzleHttp\Client; use Mpdf\Mpdf; use RprtCli\Utils\Mailer\MailerInterface; @@ -24,8 +26,6 @@ return [ 'config.file' => 'rprt.config.yml', 'config.path' => '~/.config/rprt-cli/', 'default_locale' => 'en', - // 'translator' => ['default_path' => '%kernel.project_dir%/translations'], - // 'guzzle' => create()->constructor(Client::class), 'guzzle' => get(Client::class), 'mpdf' => get(Mpdf::class), ConfigurationInterface::class => get(ConfigurationService::class), @@ -49,11 +49,11 @@ return [ // 'locale' => get('config.service')->method('get', 'en'), // Translator::class => create()->constructor('sl')->method('addLoader', 'po', new PoFileLoader), // 'translator' => get(Translator::class), - CsvReportInterface::class => get(CsvReport::class), - CsvReport::class => create()->constructor( + ReportCsvInterface::class => get(ReportCsv::class), + ReportCsv::class => create()->constructor( get('config.service') ), - 'csv.report' => get(CsvReportInterface::class), + 'csv.report' => get(ReportCsvInterface::class), MailerInterface::class => get(MailerService::class), MailerService::class => create()->constructor( get('config.service'), diff --git a/app/src/Commands/RprtCommand.php b/app/src/Commands/RprtCommand.php index c922872..cb281f6 100644 --- a/app/src/Commands/RprtCommand.php +++ b/app/src/Commands/RprtCommand.php @@ -7,13 +7,16 @@ declare(strict_types=1); namespace RprtCli\Commands; use RprtCli\Utils\Configuration\ConfigurationInterface; -use RprtCli\Utils\CsvReport\CsvReportInterface; +use RprtCli\Utils\CsvReport\ReportCsvInterface; use RprtCli\Utils\Mailer\MailerInterface; use RprtCli\Utils\PdfExport\PdfExportInterface; use RprtCli\Utils\TimeTrackingServices\YoutrackInterface; +use RprtCli\ValueObjects\Expenses; +use RprtCli\ValueObjects\WorkInvoiceElement; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Helper\TableSeparator; +use Symfony\Component\Console\Helper\TableCell; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; @@ -34,8 +37,11 @@ class RprtCommand extends Command protected $pdfExport; + const TYPE_WORK = 1; + const TYPE_EXPENSE = 2; + public function __construct( - CsvReportInterface $csv, + ReportCsvInterface $csv, ConfigurationInterface $configuration, YoutrackInterface $youtrack, PdfExportInterface $pdf_export, @@ -81,7 +87,7 @@ class RprtCommand extends Command 'test', 't', InputOption::VALUE_NONE, - 'Test login into youtrack service.' + 'Test login into youtrack service. Prints out your name.' ); $this->addOption( 'output', @@ -101,6 +107,20 @@ class RprtCommand extends Command InputOption::VALUE_REQUIRED, 'Comma separated list of recipients that should get the exported pdf.' ); + $this->addOption( + 'expenses', + 'e', + InputOption::VALUE_OPTIONAL, + 'List of additional expenses in format expense1=value1;expenses2=value2... or empty for interactive output.', + FALSE + ); + $this->addOption( + 'custom', + 'c', + InputOption::VALUE_OPTIONAL, + 'Additional custom work untracked in format: name1=time1;name2=time2... Project to assign work items to has to be configured in app config. Leave empty for interactive output.', + FALSE + ); } protected function execute(InputInterface $input, OutputInterface $output) : int @@ -113,25 +133,38 @@ class RprtCommand extends Command $report_id = $this->youtrack->getReportId(); $file = $this->youtrack->downloadReport($report_id); } + if ($input->hasParameterOption('expenses') || $input->hasParameterOption('-e')) { + $expenses = $this->getExpenses($input->getOption('expenses')); + } + if ($input->hasParameterOption('custom') || $input->hasParameterOption('-c')) { + $custom = $this->getCustomWork($input->getOption('custom')); + } if ($youtrack || $file = $input->getOption('file')) { // Youtrack can also provide a file name. var_dump($file); $data = $this->csv->getReportData($file); - $table = $this->generateTable($output, $data); + if (!empty($expenses)) { + $data = array_merge($data, $expenses); + } + // $table = $this->generateTable($output, $data); + $table = $this->getTable($output, $data); $table->render(); - if ($pdf = $input->getOption('pdf')) { + if ($input->getOption('pdf')) { $nice_data = $this->csv->arangeDataForDefaultPdfExport($data); // @TODO method gatherTokens(); - if ($output = $input->getOption('output')) { - $this->pdfExport->setOutput($output); + if ($out = $input->getOption('output')) { + $this->pdfExport->setOutput($out); } $output_path = $this->pdfExport->fromDefaultDataToPdf($nice_data); + // Notify the user where the file was generated to. + $output->writeln("The file was generated at ${output_path}."); } // return Command::SUCCESS; } - if ($send = $input->getOption('send') && $output_path) { + if ($input->getOption('send') && $output_path) { + // @TODO If no output path print an error. // Send email to configured address. if ($recipients = $input->getOption('send-to')) { $this->mailer->setRecipients(explode(',', $recipients)); @@ -143,17 +176,35 @@ class RprtCommand extends Command return Command::SUCCESS; } + protected function getTable(OutputInterface $output, array $data) :Table { + $rows = $this->csv->generateTable($data); + $table = new Table($output); + $table->setHeaders([ + 'Project', 'Hours', 'Rate', 'Price', + ]); + foreach ($rows as $key => $row) { + if (!$row) { + $rows[$key] = new TableSeparator(); + } + elseif (is_array($row) && is_null($row[1]) && is_null($row[0])) { + // Check which elements in array are null. + $rows[$key] = [new TableCell($row[2], ['colspan' => 3]), $row[3]]; + } + } + $table->setRows($rows); + return $table; + } + /** * Create table from data that is already inline with configuration. + * + * @deprecated + * This method was almost exact copy of CsvReport::arangeDataForDefaultPdfExport */ protected function generateTable(OutputInterface $output, array $data) : Table { $table = new Table($output); $table->setHeaders([ - // $this->translator->trans('Project', [], 'messages', 'sl_SI'), - // $this->translator->trans('Hours', [], 'messages', 'sl_SI'), - // $this->translator->trans('Rate'), - // $this->translator->trans('Price'), 'Project', 'Hours', 'Rate', 'Price', ]); [$rows, $totalHours, $totalPrice] = [[], 0, 0]; @@ -168,7 +219,7 @@ class RprtCommand extends Command if ($config['time_format'] === 'm') { $hours /= 60; } - $price = $hours * (int) $config['price']; + $price = $hours * (float) $config['price']; $row = [ $config['name'], $hours, @@ -178,7 +229,15 @@ class RprtCommand extends Command $rows[] = $row; $totalHours += $hours; $totalPrice += $price; + unset($data[$name]); } + if (!empty($data)) { + foreach ($data as $name => $value) { + if (strpos(strtolower($name), 'expanses') !== FALSE) { + } + } + } + $rows[] = new TableSeparator(); // @TODO Check rate in final result. // $rows[] = [$this->translator->trans('Sum'), $totalHours, $config['price'], $totalPrice]; @@ -206,4 +265,71 @@ class RprtCommand extends Command // $table->setStyle('borderless'); $table->render(); } + + /** + * Gets the expenses array. + * + * @return Expenses[] + */ + protected function getExpenses($expenses) { + $output = []; + if (is_string($expenses)) { + foreach (explode(';', $expenses) as $expense) { + [$name, $value] = explode('=', $expense); + $output[] = new Expenses($name, (float) $value); + } + } + else { + $continue = TRUE; + while ($continue) { + $name = readline('Enter expenses name or leave empty to stop: '); + $value = (float) readline('Enter expenses value: '); + if (!empty($name)) { + $output[] = new Expenses($name, $value); + } + else { + $continue = FALSE; + } + } + } + return $output; + } + + protected function getCustomWorkOrExpenses($custom, $type) { + $output = []; + if (is_string($custom)) { + foreach (explode(';', $custom) as $item) { + [$name, $value] = explode('=', $item); + $output[] = $this->createInvoiceElement($name, (float) $value, $type); + } + } else { + $continue = TRUE; + if ($type == self::TYPE_WORK) { + $message_name = 'Enter project name or leave empty to stop: '; + $message_value = 'Enter time spent of project: '; + } elseif ($type == self::TYPE_EXPENSE) { + $message_name = 'Enter expenses name or leave empty to stop: '; + $message_value = 'Enter expenses value: '; + } + while ($continue) { + $name = readline($message_name); + $value = (float) readline($message_value); + if (!empty($name)) { + $output[] = $this->createInvoiceElement($name, $value, $type); + } else { + $continue = FALSE; + } + } + } + return $output; + } + + protected function createInvoiceElement(string $name, float $value, int $type) { + if ($type == self::TYPE_WORK) { + return new WorkInvoiceElement($name, (float) $value); + } elseif ($type == self::TYPE_EXPENSE) { + return new Expenses($name, (float) $value); + } + throw new \Exception('Unkown invoice element type.'); + } } diff --git a/app/src/Utils/CsvReport/CsvReport.php b/app/src/Utils/CsvReport/CsvReport.php index 1bcf488..775942f 100644 --- a/app/src/Utils/CsvReport/CsvReport.php +++ b/app/src/Utils/CsvReport/CsvReport.php @@ -15,6 +15,9 @@ use function reset; /** * Creates a report of projects and hours. + * + * @deprecated + * Use ReportCsv class instead. */ class CsvReport implements CsvReportInterface { @@ -90,7 +93,7 @@ class CsvReport implements CsvReportInterface } $hours = $data[$name]; if ($config['time_format'] === 'm') { - $hours /= 60; + $hours /= 60.0; } $price = $hours * (int) $config['price']; $row = [ diff --git a/app/src/Utils/CsvReport/ReportCsv.php b/app/src/Utils/CsvReport/ReportCsv.php new file mode 100644 index 0000000..35d2068 --- /dev/null +++ b/app/src/Utils/CsvReport/ReportCsv.php @@ -0,0 +1,199 @@ +configurationService = $config; + } + + /** + * {@inheritdoc} + */ + public function getReportData(string $filePath) : array + { + $output = []; + // @TODO replace with config service. + // $config = $this->dummyConfig()['projects']; + $config = $this->configurationService->get('projects'); + foreach (array_keys($config) as $key) { + $output[$key] = 0; + } + if ($file = fopen($filePath, 'r')) { + while (($line = fgetcsv($file)) !== false) { + $parsed = $this->parseCsvFile($line); + // $key = reset(array_keys($parsed)); + $key = array_key_first($parsed); + if (isset($output[$key])) { + $output[$key] += (float) reset($parsed); + } + } + } + $report_data = []; + foreach ($output as $project => $hours) { + $report_data[] = new WorkInvoiceElement($project, $hours); + } + return $report_data; + } + + /** + * Get correct values from the raw data lines of csv. + * + * @param array $rawData + * Columns with data are specified in config. + * + * @return array + * Project key and unit of time spent. + */ + protected function parseCsvFile(array $rawData) : array + { + $config = $this->configurationService->get('projects'); + foreach ($config as $key => $project) { + if (preg_match('/' . $project['pattern'] . '/', $rawData[1])) { + return [$key => $rawData[4]]; + } + } + return []; + } + + /** + * Input is array of Work elements and expenses. + */ + public function generateTable(array $data): array { + [$rows, $totalHours, $totalPrice, $add_separator] = [[], 0, 0, FALSE]; + $projectsConfig = $this->configurationService->get('projects'); + // $header = $this->configurationService->get('export.labels', null); + $header = null; + if (is_array($header)) { + $rows[] = $header; + } + // First only list work invoice elements. + foreach ($data as $key => $invoice_element) { + if ($invoice_element instanceof WorkInvoiceElementInterface) { + $add_separator = TRUE; + $project = $invoice_element->getName(); + $time = $invoice_element->getTime(); + $config = $projectsConfig[$project]; + $hours = $config['time_format'] == 'm' ? $time/60 : $time; + $price = $hours * (float) $config['price']; + $row = [ + $config['label'] ?? $project, + number_format($hours, 2, ',', '.'), + number_format($config['price'], 2, ',', '.'), + number_format($price, 2, ',', '.'), + ]; + $totalHours += $hours; + $totalPrice += $price; + $rows[] = $row; + unset($data[$key]); + } + } + if ($add_separator) { + $rows[] = null; + $rows[] = ['Gesamt netto', number_format($totalHours, 2, ',', '.'), ' ', number_format($totalPrice, 2, ',', '.')]; + $add_separator = FALSE; + } + foreach ($data as $invoice_element) { + if ($invoice_element instanceof ExpensesInterface) { + if (!isset($added_expenses)) { + // Make next line bold and centered. + $rows[] = 0; + $rows[] = [ + null, + null, + 'Kosten', + 'EUR', + ]; + // Don't make next line bold. See RprtCli\PdfExport\PdfExportService::parsedDataToHtml. + $rows[] = FALSE; + $added_expenses = TRUE; + } + $add_separator = TRUE; + $rows[] = [ + null, + null, + $invoice_element->getName(), + number_format($invoice_element->getValue(), 2, ',', '.'), + ]; + $totalPrice += $invoice_element->getValue(); + } + } + if ($add_separator) { + $rows[] = null; + } + $rows[] = [null, null, 'Gessamt brutto', number_format($totalPrice, 2, ',', '.')]; + return $rows; + } + + /** + * {@inheritdoc} + */ + public function arangeDataForDefaultPdfExport(array $data) :array { + $rows = $this->generateTable($data); + $header = $this->configurationService->get('export.labels', null); + array_unshift($rows, $header); + foreach ($rows as $key => $row) { + if (!$row) { + unset($data[$key]); + } + } + return $rows; + } + + /** + * Should be moved into test class. + */ + protected function dummyConfig() : array + { + return [ + 'projects' => [ + 'LDP' => [ + 'name' => 'lupus.digital', + 'pattern' => 'LDP-[0-9]+', + 'price' => 25, + // optional specify columns + ], + 'WV' => [ + 'name' => 'Wirtschaftsverlag', + 'pattern' => 'WV-[0-9]+', + 'price' => 25, + // optional specify columns + ], + 'Other' => [ + 'name' => 'Other projects', + 'pattern' => '(?!.\bLDP\b)(?!.\bWV\b)', + 'price' => 25, + // optional specify columns + ], + ], + ]; + } +} diff --git a/app/src/Utils/CsvReport/ReportCsvInterface.php b/app/src/Utils/CsvReport/ReportCsvInterface.php new file mode 100644 index 0000000..4983d26 --- /dev/null +++ b/app/src/Utils/CsvReport/ReportCsvInterface.php @@ -0,0 +1,36 @@ +'; $header = array_shift($data); + $classes = ''; foreach ($header as $index => $cell) { $table .= "{$cell}"; } $table .= ''; foreach ($data as $row_index => $row) { + if (!$row) { + if ($row === NULL) { + $classes = 'bold'; + } + elseif ($row === 0) { + $classes = 'bold center'; + } + continue; + } list($cells, $colspan) = [[], 0]; foreach ($row as $index => $cell) { if (!$cell) { @@ -70,10 +82,12 @@ class PdfExportService implements PdfExportInterface { $cells[] = "{$cell}"; } } - $rows[] = "" . implode($cells) . ''; + $rows[] = "" . implode($cells) . ''; + $classes = ''; } $table .= implode('', $rows); $table .= ''; + var_dump($table); return $table; } @@ -178,8 +192,8 @@ class PdfExportService implements PdfExportInterface { $tokens['today'] = date('j. m. y'); $month_ago = strtotime('1 month ago'); $tokens['number'] = date('m/y', $month_ago); - $tokens['date_start'] = date('1. m. y', $month_ago); - $tokens['date_end'] = date("Y-m-d", mktime(0, 0, 0, (int) date("m"), 0)); + $tokens['date_start'] = date('1. m. Y', $month_ago); + $tokens['date_end'] = date("d. m. Y", mktime(0, 0, 0, (int) date("m"), 0)); return $tokens; } diff --git a/app/src/ValueObjects/Expenses.php b/app/src/ValueObjects/Expenses.php new file mode 100644 index 0000000..5697635 --- /dev/null +++ b/app/src/ValueObjects/Expenses.php @@ -0,0 +1,33 @@ +name = $name; + $this->value = $value; + } + + public function getValue(): float + { + return $this->value; + } + + public function getName(): string + { + return $this->name; + } +} diff --git a/app/src/ValueObjects/ExpensesInterface.php b/app/src/ValueObjects/ExpensesInterface.php new file mode 100644 index 0000000..6f90ed2 --- /dev/null +++ b/app/src/ValueObjects/ExpensesInterface.php @@ -0,0 +1,11 @@ +name = $name; + $this->time = $time; + } + + public function getTime() :float { + return $this->time; + } + + public function getName() :string { + return $this->name; + } + +} diff --git a/app/src/ValueObjects/WorkInvoiceElementInterface.php b/app/src/ValueObjects/WorkInvoiceElementInterface.php new file mode 100644 index 0000000..83b4c3a --- /dev/null +++ b/app/src/ValueObjects/WorkInvoiceElementInterface.php @@ -0,0 +1,15 @@ +