added export module (#538)
This commit is contained in:
@@ -1,6 +1,10 @@
|
|||||||
build:
|
build:
|
||||||
environment:
|
environment:
|
||||||
node: v8.11.2
|
node: v8.11.2
|
||||||
|
php:
|
||||||
|
version: 7.2
|
||||||
|
ini:
|
||||||
|
date.timezone: "UTC"
|
||||||
nodes:
|
nodes:
|
||||||
analysis:
|
analysis:
|
||||||
tests:
|
tests:
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ Kimai is a [multi-language application](var/docs/translations.md) and already tr
|
|||||||
### Requirements
|
### Requirements
|
||||||
|
|
||||||
- PHP 7.1.3 or higher (test your system compatibility with the [requirements-checker](http://symfony.com/doc/current/reference/requirements.html))
|
- PHP 7.1.3 or higher (test your system compatibility with the [requirements-checker](http://symfony.com/doc/current/reference/requirements.html))
|
||||||
- The PHP extensions [intl](https://php.net/manual/en/book.intl.php), [zip](https://php.net/manual/en/book.zip.php) and [PDO](https://php.net/manual/en/book.pdo.php) with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled
|
- The PHP extensions [mbstring](http://php.net/manual/en/book.mbstring.php), [gd](http://php.net/manual/en/book.image.php), [intl](https://php.net/manual/en/book.intl.php), [zip](https://php.net/manual/en/book.zip.php) and [PDO](https://php.net/manual/en/book.pdo.php) with either [pdo_sqlite](https://php.net/manual/en/ref.pdo-sqlite.php) or [pdo_mysql](https://php.net/manual/en/ref.pdo-mysql.php) enabled
|
||||||
- If you use MariaDB, make sure its at least v10.2.7 (see [FAQ](var/docs/faq.md))
|
- If you use MariaDB, make sure its at least v10.2.7 (see [FAQ](var/docs/faq.md))
|
||||||
- Kimai needs to be installed in the root directory of a domain or you need to [recompile the frontend assets](var/docs/developers.md)
|
- Kimai needs to be installed in the root directory of a domain or you need to [recompile the frontend assets](var/docs/developers.md)
|
||||||
- A modern browser, Kimai v2 might be broken on old browsers like IE 10
|
- A modern browser, Kimai v2 might be broken on old browsers like IE 10
|
||||||
|
|||||||
@@ -120,9 +120,14 @@ table.dataTable thead .sorting_desc_disabled:after {
|
|||||||
}
|
}
|
||||||
|
|
||||||
table.dataTable thead > tr > th {
|
table.dataTable thead > tr > th {
|
||||||
padding-right: 30px;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
table.dataTable thead > tr > th {
|
||||||
|
padding-right: 30px;
|
||||||
|
}
|
||||||
|
*/
|
||||||
table.dataTable th:active {
|
table.dataTable th:active {
|
||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
@@ -223,14 +228,17 @@ div.dataTables_scrollHead table.table-bordered {
|
|||||||
background-color: #017ebc;
|
background-color: #017ebc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
table.dataTable {
|
||||||
|
td {
|
||||||
/*
|
/*
|
||||||
Make sure that the action buttons do not line-break if another column takes all available space
|
Make sure that the action buttons do not line-break if another column takes all available space
|
||||||
See https://github.com/twbs/bootstrap/issues/9939#issuecomment-37682952
|
See https://github.com/twbs/bootstrap/issues/9939#issuecomment-37682952
|
||||||
*/
|
*/
|
||||||
table.dataTable {
|
|
||||||
td {
|
|
||||||
.btn-group {
|
.btn-group {
|
||||||
display: flex;
|
display: flex;
|
||||||
}
|
}
|
||||||
|
.label {
|
||||||
|
font-size: 85%;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -285,7 +285,7 @@ table.invoice-sum th, table.invoice-sum td {
|
|||||||
* {
|
* {
|
||||||
-webkit-print-color-adjust: exact;
|
-webkit-print-color-adjust: exact;
|
||||||
}
|
}
|
||||||
body, .invoice, .wrapper, #freelancer-invoice {
|
body, .wrapper, #freelancer-invoice {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,6 +25,10 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.toolbar-pad {
|
||||||
|
padding: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
/* The filter form is available on most pages above the datatable */
|
/* The filter form is available on most pages above the datatable */
|
||||||
@media (min-width: $screen-sm-min) {
|
@media (min-width: $screen-sm-min) {
|
||||||
.toolbar {
|
.toolbar {
|
||||||
@@ -43,5 +47,8 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.toolbar-pad {
|
||||||
|
padding: 10px 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@
|
|||||||
"description": "Kimai Time-Tracking 2",
|
"description": "Kimai Time-Tracking 2",
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^7.1.3",
|
"php": "^7.1.3",
|
||||||
|
"ext-gd": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-zip": "*",
|
"ext-zip": "*",
|
||||||
"beberlei/DoctrineExtensions": "^1.0",
|
"beberlei/DoctrineExtensions": "^1.0",
|
||||||
@@ -17,6 +19,7 @@
|
|||||||
"fzaninotto/faker": "^1.8",
|
"fzaninotto/faker": "^1.8",
|
||||||
"jms/serializer-bundle": "^2.4",
|
"jms/serializer-bundle": "^2.4",
|
||||||
"kevinpapst/adminlte-bundle": "~2.1",
|
"kevinpapst/adminlte-bundle": "~2.1",
|
||||||
|
"mpdf/mpdf": "^7.1",
|
||||||
"nelmio/api-doc-bundle": "^3.2",
|
"nelmio/api-doc-bundle": "^3.2",
|
||||||
"ocramius/proxy-manager": "2.1.1",
|
"ocramius/proxy-manager": "2.1.1",
|
||||||
"phpoffice/phpspreadsheet": "^1.4",
|
"phpoffice/phpspreadsheet": "^1.4",
|
||||||
@@ -78,6 +81,9 @@
|
|||||||
"symfony/polyfill-php70": "*",
|
"symfony/polyfill-php70": "*",
|
||||||
"symfony/polyfill-php56": "*"
|
"symfony/polyfill-php56": "*"
|
||||||
},
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-mbstring": "If ext-mbstring is not available you MUST install symfony/polyfill-mbstring"
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"auto-scripts": {
|
"auto-scripts": {
|
||||||
"cache:clear": "symfony-cmd",
|
"cache:clear": "symfony-cmd",
|
||||||
|
|||||||
217
composer.lock
generated
217
composer.lock
generated
@@ -4,7 +4,7 @@
|
|||||||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
|
||||||
"This file is @generated automatically"
|
"This file is @generated automatically"
|
||||||
],
|
],
|
||||||
"content-hash": "303432e4fedef20a41e3279148147685",
|
"content-hash": "230c5b328a69b340e1261bc392acf9c9",
|
||||||
"packages": [
|
"packages": [
|
||||||
{
|
{
|
||||||
"name": "beberlei/DoctrineExtensions",
|
"name": "beberlei/DoctrineExtensions",
|
||||||
@@ -2350,6 +2350,122 @@
|
|||||||
],
|
],
|
||||||
"time": "2017-06-19T01:22:40+00:00"
|
"time": "2017-06-19T01:22:40+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "mpdf/mpdf",
|
||||||
|
"version": "v7.1.8",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/mpdf/mpdf.git",
|
||||||
|
"reference": "5a124ed382cf9241597c7ba12f98a756daa7f8df"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/mpdf/mpdf/zipball/5a124ed382cf9241597c7ba12f98a756daa7f8df",
|
||||||
|
"reference": "5a124ed382cf9241597c7ba12f98a756daa7f8df",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"ext-gd": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
|
"myclabs/deep-copy": "^1.7",
|
||||||
|
"paragonie/random_compat": "^1.4|^2.0|9.99.99",
|
||||||
|
"php": "^5.6 || ~7.0.0 || ~7.1.0 || ~7.2.0 || ~7.3.0",
|
||||||
|
"psr/log": "^1.0",
|
||||||
|
"setasign/fpdi": "1.6.*"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"mockery/mockery": "^0.9.5",
|
||||||
|
"phpunit/phpunit": "^5.0",
|
||||||
|
"squizlabs/php_codesniffer": "^2.7.0",
|
||||||
|
"tracy/tracy": "^2.4"
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"ext-bcmath": "Needed for generation of some types of barcodes",
|
||||||
|
"ext-xml": "Needed mainly for SVG manipulation",
|
||||||
|
"ext-zlib": "Needed for compression of embedded resources, such as fonts"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"extra": {
|
||||||
|
"branch-alias": {
|
||||||
|
"dev-development": "7.x-dev"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"Mpdf\\": "src/"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"GPL-2.0-only"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Matěj Humpál",
|
||||||
|
"role": "Developer, maintainer"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Ian Back",
|
||||||
|
"role": "Developer (retired)"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "PHP library generating PDF files from UTF-8 encoded HTML",
|
||||||
|
"homepage": "https://mpdf.github.io",
|
||||||
|
"keywords": [
|
||||||
|
"pdf",
|
||||||
|
"php",
|
||||||
|
"utf-8"
|
||||||
|
],
|
||||||
|
"time": "2019-01-08T11:38:17+00:00"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "myclabs/deep-copy",
|
||||||
|
"version": "1.8.1",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/myclabs/DeepCopy.git",
|
||||||
|
"reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
|
||||||
|
"reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"require": {
|
||||||
|
"php": "^7.1"
|
||||||
|
},
|
||||||
|
"replace": {
|
||||||
|
"myclabs/deep-copy": "self.version"
|
||||||
|
},
|
||||||
|
"require-dev": {
|
||||||
|
"doctrine/collections": "^1.0",
|
||||||
|
"doctrine/common": "^2.6",
|
||||||
|
"phpunit/phpunit": "^7.1"
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"psr-4": {
|
||||||
|
"DeepCopy\\": "src/DeepCopy/"
|
||||||
|
},
|
||||||
|
"files": [
|
||||||
|
"src/DeepCopy/deep_copy.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"description": "Create deep copies (clones) of your objects",
|
||||||
|
"keywords": [
|
||||||
|
"clone",
|
||||||
|
"copy",
|
||||||
|
"duplicate",
|
||||||
|
"object",
|
||||||
|
"object graph"
|
||||||
|
],
|
||||||
|
"time": "2018-06-11T23:09:50+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "nelmio/api-doc-bundle",
|
"name": "nelmio/api-doc-bundle",
|
||||||
"version": "v3.3.0",
|
"version": "v3.3.0",
|
||||||
@@ -3519,6 +3635,55 @@
|
|||||||
],
|
],
|
||||||
"time": "2018-09-30T05:16:57+00:00"
|
"time": "2018-09-30T05:16:57+00:00"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "setasign/fpdi",
|
||||||
|
"version": "1.6.2",
|
||||||
|
"source": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://github.com/Setasign/FPDI.git",
|
||||||
|
"reference": "a6ad58897a6d97cc2d2cd2adaeda343b25a368ea"
|
||||||
|
},
|
||||||
|
"dist": {
|
||||||
|
"type": "zip",
|
||||||
|
"url": "https://api.github.com/repos/Setasign/FPDI/zipball/a6ad58897a6d97cc2d2cd2adaeda343b25a368ea",
|
||||||
|
"reference": "a6ad58897a6d97cc2d2cd2adaeda343b25a368ea",
|
||||||
|
"shasum": ""
|
||||||
|
},
|
||||||
|
"suggest": {
|
||||||
|
"setasign/fpdf": "FPDI will extend this class but as it is also possible to use \"tecnickcom/tcpdf\" as an alternative there's no fixed dependency configured.",
|
||||||
|
"setasign/fpdi-fpdf": "Use this package to automatically evaluate dependencies to FPDF.",
|
||||||
|
"setasign/fpdi-tcpdf": "Use this package to automatically evaluate dependencies to TCPDF."
|
||||||
|
},
|
||||||
|
"type": "library",
|
||||||
|
"autoload": {
|
||||||
|
"classmap": [
|
||||||
|
"filters/",
|
||||||
|
"fpdi.php",
|
||||||
|
"fpdf_tpl.php",
|
||||||
|
"fpdi_pdf_parser.php",
|
||||||
|
"pdf_context.php"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"notification-url": "https://packagist.org/downloads/",
|
||||||
|
"license": [
|
||||||
|
"MIT"
|
||||||
|
],
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Jan Slabon",
|
||||||
|
"email": "jan.slabon@setasign.com",
|
||||||
|
"homepage": "https://www.setasign.com"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "FPDI is a collection of PHP classes facilitating developers to read pages from existing PDF documents and use them as templates in FPDF. Because it is also possible to use FPDI with TCPDF, there are no fixed dependencies defined. Please see suggestions for packages which evaluates the dependencies automatically.",
|
||||||
|
"homepage": "https://www.setasign.com/fpdi",
|
||||||
|
"keywords": [
|
||||||
|
"fpdf",
|
||||||
|
"fpdi",
|
||||||
|
"pdf"
|
||||||
|
],
|
||||||
|
"time": "2017-05-11T14:25:49+00:00"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "swiftmailer/swiftmailer",
|
"name": "swiftmailer/swiftmailer",
|
||||||
"version": "v6.1.3",
|
"version": "v6.1.3",
|
||||||
@@ -7451,54 +7616,6 @@
|
|||||||
"description": "A tool to automatically fix PHP code style",
|
"description": "A tool to automatically fix PHP code style",
|
||||||
"time": "2018-08-23T13:15:44+00:00"
|
"time": "2018-08-23T13:15:44+00:00"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "myclabs/deep-copy",
|
|
||||||
"version": "1.8.1",
|
|
||||||
"source": {
|
|
||||||
"type": "git",
|
|
||||||
"url": "https://github.com/myclabs/DeepCopy.git",
|
|
||||||
"reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8"
|
|
||||||
},
|
|
||||||
"dist": {
|
|
||||||
"type": "zip",
|
|
||||||
"url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
|
|
||||||
"reference": "3e01bdad3e18354c3dce54466b7fbe33a9f9f7f8",
|
|
||||||
"shasum": ""
|
|
||||||
},
|
|
||||||
"require": {
|
|
||||||
"php": "^7.1"
|
|
||||||
},
|
|
||||||
"replace": {
|
|
||||||
"myclabs/deep-copy": "self.version"
|
|
||||||
},
|
|
||||||
"require-dev": {
|
|
||||||
"doctrine/collections": "^1.0",
|
|
||||||
"doctrine/common": "^2.6",
|
|
||||||
"phpunit/phpunit": "^7.1"
|
|
||||||
},
|
|
||||||
"type": "library",
|
|
||||||
"autoload": {
|
|
||||||
"psr-4": {
|
|
||||||
"DeepCopy\\": "src/DeepCopy/"
|
|
||||||
},
|
|
||||||
"files": [
|
|
||||||
"src/DeepCopy/deep_copy.php"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"notification-url": "https://packagist.org/downloads/",
|
|
||||||
"license": [
|
|
||||||
"MIT"
|
|
||||||
],
|
|
||||||
"description": "Create deep copies (clones) of your objects",
|
|
||||||
"keywords": [
|
|
||||||
"clone",
|
|
||||||
"copy",
|
|
||||||
"duplicate",
|
|
||||||
"object",
|
|
||||||
"object graph"
|
|
||||||
],
|
|
||||||
"time": "2018-06-11T23:09:50+00:00"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "phar-io/manifest",
|
"name": "phar-io/manifest",
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
@@ -9020,7 +9137,9 @@
|
|||||||
"prefer-lowest": false,
|
"prefer-lowest": false,
|
||||||
"platform": {
|
"platform": {
|
||||||
"php": "^7.1.3",
|
"php": "^7.1.3",
|
||||||
|
"ext-gd": "*",
|
||||||
"ext-intl": "*",
|
"ext-intl": "*",
|
||||||
|
"ext-mbstring": "*",
|
||||||
"ext-pdo": "*",
|
"ext-pdo": "*",
|
||||||
"ext-zip": "*"
|
"ext-zip": "*"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -66,8 +66,8 @@ kimai:
|
|||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# Free configurable permission system, see var/docs/permissions.md
|
# Free configurable permission system, see var/docs/permissions.md
|
||||||
permissions:
|
permissions:
|
||||||
# mapping complex rulesets of single permissions to named "sets" ("set name" = [array of "permissions"])
|
|
||||||
sets:
|
sets:
|
||||||
|
# mapping complex rulesets of single permissions to named "sets" ("set name" = [array of "permissions"])
|
||||||
ACTIVITIES: [view_activity,create_activity,edit_activity,delete_activity]
|
ACTIVITIES: [view_activity,create_activity,edit_activity,delete_activity]
|
||||||
PROJECTS: [view_project,create_project,edit_project,delete_project]
|
PROJECTS: [view_project,create_project,edit_project,delete_project]
|
||||||
CUSTOMERS: [view_customer,create_customer,edit_customer,delete_customer]
|
CUSTOMERS: [view_customer,create_customer,edit_customer,delete_customer]
|
||||||
@@ -80,18 +80,19 @@ kimai:
|
|||||||
USER: [view_user,create_user,delete_user]
|
USER: [view_user,create_user,delete_user]
|
||||||
RATE: [view_rate_own_timesheet,edit_rate_own_timesheet]
|
RATE: [view_rate_own_timesheet,edit_rate_own_timesheet]
|
||||||
RATE_OTHER: [view_rate_other_timesheet,edit_rate_other_timesheet]
|
RATE_OTHER: [view_rate_other_timesheet,edit_rate_other_timesheet]
|
||||||
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
EXPORT: [view_export,create_export,edit_export_own_timesheet,edit_export_other_timesheet]
|
||||||
maps:
|
maps:
|
||||||
|
# mapping "sets" or permissions to user roles ("role name" = [array of "set names"])
|
||||||
ROLE_USER: [TIMESHEET,PROFILE]
|
ROLE_USER: [TIMESHEET,PROFILE]
|
||||||
ROLE_TEAMLEAD: [INVOICE,TIMESHEET,TIMESHEET_OTHER,PROFILE]
|
ROLE_TEAMLEAD: [INVOICE,TIMESHEET,TIMESHEET_OTHER,PROFILE,EXPORT]
|
||||||
ROLE_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,RATE,RATE_OTHER]
|
ROLE_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,RATE,RATE_OTHER,EXPORT]
|
||||||
ROLE_SUPER_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,PROFILE_OTHER,USER,RATE,RATE_OTHER]
|
ROLE_SUPER_ADMIN: [ACTIVITIES,PROJECTS,CUSTOMERS,INVOICE,INVOICE_TEMPLATE,TIMESHEET,TIMESHEET_OTHER,PROFILE,PROFILE_OTHER,USER,RATE,RATE_OTHER,EXPORT]
|
||||||
# adding single permissions to user roles, extending the definition from "sets" ("role name" = [array of "permissions"])
|
|
||||||
roles:
|
roles:
|
||||||
|
# adding single permissions to user roles, extending the definition from "sets" ("role name" = [array of "permissions"])
|
||||||
ROLE_USER: []
|
ROLE_USER: []
|
||||||
ROLE_TEAMLEAD: [view_invoice_template,create_invoice_template,edit_invoice_template,view_rate_own_timesheet,view_rate_other_timesheet,hourly-rate_own_profile]
|
ROLE_TEAMLEAD: [view_invoice_template,create_invoice_template,edit_invoice_template,view_rate_own_timesheet,view_rate_other_timesheet,hourly-rate_own_profile]
|
||||||
ROLE_ADMIN: [hourly-rate_own_profile]
|
ROLE_ADMIN: [hourly-rate_own_profile]
|
||||||
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile]
|
ROLE_SUPER_ADMIN: [hourly-rate_own_profile,hourly-rate_other_profile,delete_own_profile,roles_own_profile,system_information]
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
# Language specific settings, like the date formats
|
# Language specific settings, like the date formats
|
||||||
@@ -102,6 +103,7 @@ kimai:
|
|||||||
date_type: 'dd.MM.yyyy'
|
date_type: 'dd.MM.yyyy'
|
||||||
date_picker: 'DD.MM.YYYY'
|
date_picker: 'DD.MM.YYYY'
|
||||||
date: 'd.m.Y'
|
date: 'd.m.Y'
|
||||||
|
date_time: 'd.m. H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
en:
|
en:
|
||||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||||
@@ -109,6 +111,7 @@ kimai:
|
|||||||
date_type: 'yyyy-MM-dd'
|
date_type: 'yyyy-MM-dd'
|
||||||
date_picker: 'YYYY-MM-DD'
|
date_picker: 'YYYY-MM-DD'
|
||||||
date: 'Y-m-d'
|
date: 'Y-m-d'
|
||||||
|
date_time: 'm-d H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
pt_BR:
|
pt_BR:
|
||||||
date_time_type: 'dd-MM-yyyy HH:mm'
|
date_time_type: 'dd-MM-yyyy HH:mm'
|
||||||
@@ -116,6 +119,7 @@ kimai:
|
|||||||
date_type: 'dd-MM-yyyy'
|
date_type: 'dd-MM-yyyy'
|
||||||
date_picker: 'DD-MM-YYYY'
|
date_picker: 'DD-MM-YYYY'
|
||||||
date: 'd-m-Y'
|
date: 'd-m-Y'
|
||||||
|
date_time: 'd-m H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
it:
|
it:
|
||||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||||
@@ -123,6 +127,7 @@ kimai:
|
|||||||
date_type: 'dd.MM.yyyy'
|
date_type: 'dd.MM.yyyy'
|
||||||
date_picker: 'DD.MM.YYYY'
|
date_picker: 'DD.MM.YYYY'
|
||||||
date: 'd.m.Y'
|
date: 'd.m.Y'
|
||||||
|
date_time: 'd.m. H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
fr:
|
fr:
|
||||||
date_time_type: 'dd/MM/yyyy HH:mm'
|
date_time_type: 'dd/MM/yyyy HH:mm'
|
||||||
@@ -130,6 +135,7 @@ kimai:
|
|||||||
date_type: 'dd/MM/yyyy'
|
date_type: 'dd/MM/yyyy'
|
||||||
date_picker: 'DD/MM/YYYY'
|
date_picker: 'DD/MM/YYYY'
|
||||||
date: 'd/m/Y'
|
date: 'd/m/Y'
|
||||||
|
date_time: 'd/m H:i'
|
||||||
duration: '%%h h %%m'
|
duration: '%%h h %%m'
|
||||||
es:
|
es:
|
||||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||||
@@ -137,6 +143,7 @@ kimai:
|
|||||||
date_type: 'dd.MM.yyyy'
|
date_type: 'dd.MM.yyyy'
|
||||||
date_picker: 'DD.MM.YYYY'
|
date_picker: 'DD.MM.YYYY'
|
||||||
date: 'd.m.Y'
|
date: 'd.m.Y'
|
||||||
|
date_time: 'd.m. H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
ru:
|
ru:
|
||||||
date_time_type: 'dd.MM.yyyy HH:mm'
|
date_time_type: 'dd.MM.yyyy HH:mm'
|
||||||
@@ -144,6 +151,7 @@ kimai:
|
|||||||
date_type: 'dd.MM.yyyy'
|
date_type: 'dd.MM.yyyy'
|
||||||
date_picker: 'DD.MM.YYYY'
|
date_picker: 'DD.MM.YYYY'
|
||||||
date: 'd.m.Y'
|
date: 'd.m.Y'
|
||||||
|
date_time: 'd.m. H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
ar:
|
ar:
|
||||||
date_time_type: 'yyyy-MM-dd HH:mm'
|
date_time_type: 'yyyy-MM-dd HH:mm'
|
||||||
@@ -151,6 +159,7 @@ kimai:
|
|||||||
date_type: 'yyyy-MM-dd'
|
date_type: 'yyyy-MM-dd'
|
||||||
date_picker: 'YYYY-MM-DD'
|
date_picker: 'YYYY-MM-DD'
|
||||||
date: 'Y-m-d'
|
date: 'Y-m-d'
|
||||||
|
date_time: 'm-d H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
hu:
|
hu:
|
||||||
date_time_type: 'yyyy.MM.dd HH:mm'
|
date_time_type: 'yyyy.MM.dd HH:mm'
|
||||||
@@ -158,6 +167,7 @@ kimai:
|
|||||||
date_type: 'yyyy.MM.dd'
|
date_type: 'yyyy.MM.dd'
|
||||||
date_picker: 'YYYY.MM.DD'
|
date_picker: 'YYYY.MM.DD'
|
||||||
date: 'Y.m.d.'
|
date: 'Y.m.d.'
|
||||||
|
date_time: 'm.d. H:i'
|
||||||
duration: '%%h:%%m h'
|
duration: '%%h:%%m h'
|
||||||
|
|
||||||
# --------------------------------------------------------------------------------
|
# --------------------------------------------------------------------------------
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
App\Entity\Timesheet:
|
App\Entity\Timesheet:
|
||||||
exclusion_policy: All
|
exclusion_policy: All
|
||||||
custom_accessor_order: [id, begin, end, duration, rate, activity, project, user, description, fixedRate, hourlyRate]
|
custom_accessor_order: [id, begin, end, duration, rate, activity, project, user, description, fixedRate, hourlyRate, exported]
|
||||||
properties:
|
properties:
|
||||||
id:
|
id:
|
||||||
include: true
|
include: true
|
||||||
@@ -26,6 +26,9 @@ App\Entity\Timesheet:
|
|||||||
hourlyRate:
|
hourlyRate:
|
||||||
include: true
|
include: true
|
||||||
groups: [Entity]
|
groups: [Entity]
|
||||||
|
exported:
|
||||||
|
include: true
|
||||||
|
groups: [Entity]
|
||||||
activity:
|
activity:
|
||||||
include: false
|
include: false
|
||||||
exclude: true
|
exclude: true
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"build/app.js": "/build/app.js?bf44c766db95ef7d121c",
|
"build/app.js": "/build/app.js?bf44c766db95ef7d121c",
|
||||||
"build/app.css": "/build/app.css?cc325c031344117b92b2eaada5cf2319",
|
"build/app.css": "/build/app.css?caadc4d6101e9889a4fa28e77f1d9362",
|
||||||
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
"build/images/blue@2x.png": "/build/images/blue@2x.png?2694acfd",
|
||||||
"build/images/blue.png": "/build/images/blue.png?96f8a905",
|
"build/images/blue.png": "/build/images/blue.png?96f8a905",
|
||||||
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
|
"build/fonts/fa-solid-900.woff2": "/build/fonts/fa-solid-900.woff2?e8a92a29",
|
||||||
|
|||||||
@@ -169,6 +169,7 @@ class TimesheetController extends BaseApiController
|
|||||||
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||||
'csrf_protection' => false,
|
'csrf_protection' => false,
|
||||||
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||||
|
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
$form->setData($timesheet);
|
$form->setData($timesheet);
|
||||||
@@ -216,4 +217,64 @@ class TimesheetController extends BaseApiController
|
|||||||
|
|
||||||
return $this->viewHandler->handle($view);
|
return $this->viewHandler->handle($view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @SWG\Post(
|
||||||
|
* description="Update an existing timesheet entry, you can pass all or just a subset of all attributes",
|
||||||
|
* @SWG\Schema(ref="#/definitions/TimesheetFormEntity"),
|
||||||
|
* @SWG\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Returns the updated timesheet entry",
|
||||||
|
* @SWG\Schema(ref="#/definitions/TimesheetEntity"),
|
||||||
|
* )
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @param string $id
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function patchAction(Request $request, string $id)
|
||||||
|
{
|
||||||
|
$timesheet = $this->repository->find($id);
|
||||||
|
|
||||||
|
if (!$this->isGranted('edit', $timesheet)) {
|
||||||
|
throw $this->createAccessDeniedException('User cannot update timesheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
$form = $this->createForm(TimesheetEditForm::class, $timesheet, [
|
||||||
|
'csrf_protection' => false,
|
||||||
|
'include_rate' => $this->isGranted('edit_rate', $timesheet),
|
||||||
|
'include_exported' => $this->isGranted('edit_export', $timesheet),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$form->setData($timesheet);
|
||||||
|
$form->submit($request->request->all(), false);
|
||||||
|
|
||||||
|
if (false === $form->isValid()) {
|
||||||
|
$view = new View($form, Response::HTTP_OK);
|
||||||
|
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||||
|
|
||||||
|
return $this->viewHandler->handle($view);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($form->has('duration')) {
|
||||||
|
$duration = $form->get('duration')->getData();
|
||||||
|
if ($duration > 0) {
|
||||||
|
/** @var Timesheet $record */
|
||||||
|
$record = $form->getData();
|
||||||
|
$end = clone $record->getBegin();
|
||||||
|
$end->modify('+ ' . $duration . 'seconds');
|
||||||
|
$record->setEnd($end);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$entityManager = $this->getDoctrine()->getManager();
|
||||||
|
$entityManager->persist($timesheet);
|
||||||
|
$entityManager->flush();
|
||||||
|
|
||||||
|
$view = new View($timesheet, Response::HTTP_OK);
|
||||||
|
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||||
|
|
||||||
|
return $this->viewHandler->handle($view);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -848,7 +848,7 @@ class KimaiImporterCommand extends Command
|
|||||||
* ["description"]=> NULL
|
* ["description"]=> NULL
|
||||||
* ["comment"]=> string(36) "a work description"
|
* ["comment"]=> string(36) "a work description"
|
||||||
* -- ["commentType"]=> string(1) "0"
|
* -- ["commentType"]=> string(1) "0"
|
||||||
* -- ["cleared"]=> string(1) "0"
|
* ["cleared"]=> string(1) "0"
|
||||||
* -- ["location"]=> string(0) ""
|
* -- ["location"]=> string(0) ""
|
||||||
* -- ["trackingNumber"]=> NULL
|
* -- ["trackingNumber"]=> NULL
|
||||||
* ["rate"]=> string(5) "50.00"
|
* ["rate"]=> string(5) "50.00"
|
||||||
@@ -936,6 +936,7 @@ class KimaiImporterCommand extends Command
|
|||||||
->setDuration($duration)
|
->setDuration($duration)
|
||||||
->setActivity($activity)
|
->setActivity($activity)
|
||||||
->setProject($project)
|
->setProject($project)
|
||||||
|
->setExported(intval($oldRecord['cleared']) !== 0)
|
||||||
;
|
;
|
||||||
|
|
||||||
if (!$this->validateImport($io, $timesheet)) {
|
if (!$this->validateImport($io, $timesheet)) {
|
||||||
|
|||||||
@@ -14,6 +14,10 @@ namespace App;
|
|||||||
*/
|
*/
|
||||||
class Constants
|
class Constants
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* The software name
|
||||||
|
*/
|
||||||
|
public const SOFTWARE = 'Kimai 2';
|
||||||
/**
|
/**
|
||||||
* The current release version
|
* The current release version
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ use Symfony\Component\Routing\Annotation\Route;
|
|||||||
* Controller used for executing system relevant tasks.
|
* Controller used for executing system relevant tasks.
|
||||||
*
|
*
|
||||||
* @Route(path="/admin/about")
|
* @Route(path="/admin/about")
|
||||||
* @Security("is_granted('ROLE_SUPER_ADMIN')")
|
* @Security("is_granted('system_information')")
|
||||||
*/
|
*/
|
||||||
class AboutController extends AbstractController
|
class AboutController extends AbstractController
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -189,6 +189,7 @@ class TimesheetController extends AbstractController
|
|||||||
'page' => $page,
|
'page' => $page,
|
||||||
]),
|
]),
|
||||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
|
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||||
'include_user' => true,
|
'include_user' => true,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
162
src/Controller/ExportController.php
Normal file
162
src/Controller/ExportController.php
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Controller;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Export\ServiceExport;
|
||||||
|
use App\Form\Toolbar\ExportToolbarForm;
|
||||||
|
use App\Repository\Query\ExportQuery;
|
||||||
|
use App\Repository\TimesheetRepository;
|
||||||
|
use Sensio\Bundle\FrameworkExtraBundle\Configuration\Security;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\Routing\Annotation\Route;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Controller used to export timesheet data.
|
||||||
|
*
|
||||||
|
* @Route(path="/export")
|
||||||
|
* @Security("is_granted('view_export')")
|
||||||
|
*/
|
||||||
|
class ExportController extends AbstractController
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var TimesheetRepository
|
||||||
|
*/
|
||||||
|
protected $timesheetRepository;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var ServiceExport
|
||||||
|
*/
|
||||||
|
protected $export;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TimesheetRepository $timesheet
|
||||||
|
* @param ServiceExport $export
|
||||||
|
*/
|
||||||
|
public function __construct(TimesheetRepository $timesheet, ServiceExport $export)
|
||||||
|
{
|
||||||
|
$this->timesheetRepository = $timesheet;
|
||||||
|
$this->export = $export;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return ExportQuery
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
protected function getDefaultQuery()
|
||||||
|
{
|
||||||
|
$begin = new \DateTime('first day of this month');
|
||||||
|
$end = new \DateTime('last day of this month');
|
||||||
|
|
||||||
|
$query = new ExportQuery();
|
||||||
|
$query->setOrder(ExportQuery::ORDER_ASC);
|
||||||
|
$query->setBegin($begin);
|
||||||
|
$query->setEnd($end);
|
||||||
|
$query->setState(ExportQuery::STATE_STOPPED);
|
||||||
|
$query->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||||
|
|
||||||
|
return $query;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/", name="export", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('view_export')")
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function indexAction(Request $request)
|
||||||
|
{
|
||||||
|
$query = $this->getDefaultQuery();
|
||||||
|
$form = $this->getToolbarForm($query);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
/** @var ExportQuery $query */
|
||||||
|
$query = $form->getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries = $this->getEntries($query);
|
||||||
|
|
||||||
|
return $this->render('export/index.html.twig', [
|
||||||
|
'entries' => $entries,
|
||||||
|
'form' => $form->createView(),
|
||||||
|
'renderer' => $this->export->getRenderer(),
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @Route(path="/data", name="export_data", methods={"GET", "POST"})
|
||||||
|
* @Security("is_granted('create_export')")
|
||||||
|
*
|
||||||
|
* @param Request $request
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function export(Request $request)
|
||||||
|
{
|
||||||
|
$query = $this->getDefaultQuery();
|
||||||
|
$form = $this->getToolbarForm($query);
|
||||||
|
$form->handleRequest($request);
|
||||||
|
|
||||||
|
if ($form->isSubmitted() && $form->isValid()) {
|
||||||
|
/** @var ExportQuery $query */
|
||||||
|
$query = $form->getData();
|
||||||
|
}
|
||||||
|
|
||||||
|
$type = $query->getType();
|
||||||
|
if (null === $type) {
|
||||||
|
throw $this->createNotFoundException('Missing export renderer');
|
||||||
|
}
|
||||||
|
|
||||||
|
$renderer = $this->export->getRendererById($type);
|
||||||
|
|
||||||
|
// this code should not be reached, as the query already filters invalid values
|
||||||
|
// when trying to call setType() with an unknown value
|
||||||
|
if (null === $renderer) {
|
||||||
|
throw $this->createNotFoundException('Invalid export renderer');
|
||||||
|
}
|
||||||
|
|
||||||
|
$entries = $this->getEntries($query);
|
||||||
|
|
||||||
|
return $renderer->render($entries, $query);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ExportQuery $query
|
||||||
|
* @return Timesheet[]
|
||||||
|
*/
|
||||||
|
protected function getEntries(ExportQuery $query)
|
||||||
|
{
|
||||||
|
$query->setResultType(ExportQuery::RESULT_TYPE_QUERYBUILDER);
|
||||||
|
$query->getBegin()->setTime(0, 0, 0);
|
||||||
|
$query->getEnd()->setTime(23, 59, 59);
|
||||||
|
|
||||||
|
$queryBuilder = $this->timesheetRepository->findByQuery($query);
|
||||||
|
|
||||||
|
return $queryBuilder->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param ExportQuery $query
|
||||||
|
* @return \Symfony\Component\Form\FormInterface
|
||||||
|
*/
|
||||||
|
protected function getToolbarForm(ExportQuery $query)
|
||||||
|
{
|
||||||
|
return $this->createForm(ExportToolbarForm::class, $query, [
|
||||||
|
'action' => $this->generateUrl('export', []),
|
||||||
|
'method' => 'POST',
|
||||||
|
'attr' => [
|
||||||
|
'id' => 'export-form'
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -269,6 +269,7 @@ class TimesheetController extends AbstractController
|
|||||||
'origin' => $redirectRoute,
|
'origin' => $redirectRoute,
|
||||||
]),
|
]),
|
||||||
'include_rate' => $this->isGranted('edit_rate', $entry),
|
'include_rate' => $this->isGranted('edit_rate', $entry),
|
||||||
|
'include_exported' => $this->isGranted('edit_export', $entry),
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
<?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\DependencyInjection\Compiler;
|
||||||
|
|
||||||
|
use App\Export\ServiceExport;
|
||||||
|
use App\Kernel;
|
||||||
|
use Symfony\Component\DependencyInjection\Compiler\CompilerPassInterface;
|
||||||
|
use Symfony\Component\DependencyInjection\ContainerBuilder;
|
||||||
|
use Symfony\Component\DependencyInjection\Reference;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dynamically adds all dependencies to the ExportService.
|
||||||
|
*/
|
||||||
|
class ExportServiceCompilerPass implements CompilerPassInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param ContainerBuilder $container
|
||||||
|
* @throws \Exception
|
||||||
|
*/
|
||||||
|
public function process(ContainerBuilder $container)
|
||||||
|
{
|
||||||
|
// always first check if the primary service is defined
|
||||||
|
if (!$container->has(ServiceExport::class)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$definition = $container->findDefinition(ServiceExport::class);
|
||||||
|
|
||||||
|
$taggedRenderer = $container->findTaggedServiceIds(Kernel::TAG_EXPORT_RENDERER);
|
||||||
|
foreach ($taggedRenderer as $id => $tags) {
|
||||||
|
$definition->addMethodCall('addRenderer', [new Reference($id)]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -177,6 +177,7 @@ class Configuration implements ConfigurationInterface
|
|||||||
->scalarNode('date_type')->defaultValue('yyyy-MM-dd')->end() // for DateType
|
->scalarNode('date_type')->defaultValue('yyyy-MM-dd')->end() // for DateType
|
||||||
->scalarNode('date_picker')->defaultValue('YYYY-MM-DD')->end() // for DateType JS component
|
->scalarNode('date_picker')->defaultValue('YYYY-MM-DD')->end() // for DateType JS component
|
||||||
->scalarNode('date')->defaultValue('Y-m-d')->end() // for display via twig
|
->scalarNode('date')->defaultValue('Y-m-d')->end() // for display via twig
|
||||||
|
->scalarNode('date_time')->defaultValue('m-d H:i')->end() // for display via twig
|
||||||
->scalarNode('duration')->defaultValue('%%h:%%m h')->end() // for display via twig
|
->scalarNode('duration')->defaultValue('%%h:%%m h')->end() // for display via twig
|
||||||
->end()
|
->end()
|
||||||
->end()
|
->end()
|
||||||
|
|||||||
@@ -119,6 +119,14 @@ class Timesheet
|
|||||||
*/
|
*/
|
||||||
private $hourlyRate = null;
|
private $hourlyRate = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var bool
|
||||||
|
*
|
||||||
|
* @ORM\Column(name="exported", type="boolean", nullable=false)
|
||||||
|
* @Assert\NotNull()
|
||||||
|
*/
|
||||||
|
private $exported = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get entry id
|
* Get entry id
|
||||||
*
|
*
|
||||||
@@ -345,6 +353,25 @@ class Timesheet
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return bool
|
||||||
|
*/
|
||||||
|
public function isExported(): bool
|
||||||
|
{
|
||||||
|
return $this->exported;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param bool $exported
|
||||||
|
* @return Timesheet
|
||||||
|
*/
|
||||||
|
public function setExported(bool $exported)
|
||||||
|
{
|
||||||
|
$this->exported = $exported;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param ExecutionContextInterface $context
|
* @param ExecutionContextInterface $context
|
||||||
* @param $payload
|
* @param $payload
|
||||||
|
|||||||
@@ -70,6 +70,12 @@ class MenuSubscriber implements EventSubscriberInterface
|
|||||||
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
new MenuItemModel('invoice', 'menu.invoice', 'invoice', [], 'fas fa-file-invoice')
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($auth->isGranted('view_export')) {
|
||||||
|
$menu->addItem(
|
||||||
|
new MenuItemModel('export', 'menu.export', 'export', [], 'fas fa-file-export')
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
215
src/Export/Renderer/AbstractSpreadsheetRenderer.php
Normal file
215
src/Export/Renderer/AbstractSpreadsheetRenderer.php
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use App\Twig\DateExtensions;
|
||||||
|
use App\Twig\Extensions;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
abstract class AbstractSpreadsheetRenderer
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var DateExtensions
|
||||||
|
*/
|
||||||
|
protected $dateExtension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var Extensions
|
||||||
|
*/
|
||||||
|
protected $extension;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var TranslatorInterface
|
||||||
|
*/
|
||||||
|
protected $translator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param TranslatorInterface $translator
|
||||||
|
* @param DateExtensions $dateExtension
|
||||||
|
* @param Extensions $extensions
|
||||||
|
*/
|
||||||
|
public function __construct(TranslatorInterface $translator, DateExtensions $dateExtension, Extensions $extensions)
|
||||||
|
{
|
||||||
|
$this->translator = $translator;
|
||||||
|
$this->dateExtension = $dateExtension;
|
||||||
|
$this->extension = $extensions;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function getFormattedDateTime(\DateTime $date)
|
||||||
|
{
|
||||||
|
return $this->dateExtension->dateShort($date) . ' ' . date('H:i', $date->getTimestamp());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $amount
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function getFormattedMoney($amount, $currency)
|
||||||
|
{
|
||||||
|
return $this->extension->money($amount, $currency);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Timesheet $timesheet
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getUsername(Timesheet $timesheet)
|
||||||
|
{
|
||||||
|
if (!empty($timesheet->getUser()->getAlias())) {
|
||||||
|
return $timesheet->getUser()->getAlias();
|
||||||
|
}
|
||||||
|
|
||||||
|
return $timesheet->getUser()->getUsername();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param $seconds
|
||||||
|
* @return mixed
|
||||||
|
*/
|
||||||
|
protected function getFormattedDuration($seconds)
|
||||||
|
{
|
||||||
|
return $this->extension->duration($seconds);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Timesheet[] $timesheets
|
||||||
|
* @param TimesheetQuery $query
|
||||||
|
* @return Spreadsheet
|
||||||
|
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||||
|
*/
|
||||||
|
protected function fromArrayToSpreadsheet(array $timesheets, TimesheetQuery $query): Spreadsheet
|
||||||
|
{
|
||||||
|
$spreadsheet = new Spreadsheet();
|
||||||
|
$sheet = $spreadsheet->getActiveSheet();
|
||||||
|
|
||||||
|
$recordsHeaderColumn = 1;
|
||||||
|
$recordsHeaderRow = 1;
|
||||||
|
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.begin'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.end'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.user'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.customer'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.project'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.activity'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.description'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.exported'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.hourly_rate'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.fixed_rate'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.duration'));
|
||||||
|
$sheet->setCellValueByColumnAndRow($recordsHeaderColumn++, $recordsHeaderRow, $this->translator->trans('label.rate'));
|
||||||
|
|
||||||
|
$entryHeaderRow = $recordsHeaderRow + 1;
|
||||||
|
|
||||||
|
$durationTotal = 0;
|
||||||
|
$currency = false;
|
||||||
|
$rateTotal = 0;
|
||||||
|
|
||||||
|
foreach ($timesheets as $timesheet) {
|
||||||
|
$entryHeaderColumn = 1;
|
||||||
|
|
||||||
|
$durationTotal += $timesheet->getDuration();
|
||||||
|
$rateTotal += $timesheet->getRate();
|
||||||
|
if ($currency === false) {
|
||||||
|
$currency = $timesheet->getProject()->getCustomer()->getCurrency();
|
||||||
|
}
|
||||||
|
if ($currency !== $timesheet->getProject()->getCustomer()->getCurrency()) {
|
||||||
|
$currency = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
$customerCurrency = $timesheet->getProject()->getCustomer()->getCurrency();
|
||||||
|
|
||||||
|
$exported = $timesheet->isExported() ? 'entryState.exported' : 'entryState.not_exported';
|
||||||
|
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDateTime($timesheet->getBegin()));
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDateTime($timesheet->getEnd()));
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getUsername($timesheet));
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getProject()->getCustomer()->getName());
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getProject()->getName());
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getActivity()->getName());
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $timesheet->getDescription());
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->translator->trans($exported));
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getHourlyRate(), $customerCurrency));
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getFixedRate(), $customerCurrency));
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedDuration($timesheet->getDuration()));
|
||||||
|
$sheet->setCellValueByColumnAndRow($entryHeaderColumn++, $entryHeaderRow, $this->getFormattedMoney($timesheet->getRate(), $customerCurrency));
|
||||||
|
|
||||||
|
$entryHeaderRow++;
|
||||||
|
}
|
||||||
|
|
||||||
|
$sheet->setCellValueByColumnAndRow(11, $entryHeaderRow, $this->getFormattedDuration($durationTotal));
|
||||||
|
$sheet->setCellValueByColumnAndRow(12, $entryHeaderRow, $this->getFormattedMoney($rateTotal, $currency));
|
||||||
|
$sheet->getCellByColumnAndRow(11, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
|
||||||
|
$sheet->getCellByColumnAndRow(11, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||||
|
|
||||||
|
$sheet->getCellByColumnAndRow(12, $entryHeaderRow)->getStyle()->getBorders()->getTop()->setBorderStyle(\PhpOffice\PhpSpreadsheet\Style\Border::BORDER_THIN);
|
||||||
|
$sheet->getCellByColumnAndRow(12, $entryHeaderRow)->getStyle()->getFont()->setBold(true);
|
||||||
|
|
||||||
|
return $spreadsheet;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Timesheet[] $timesheets
|
||||||
|
* @param TimesheetQuery $query
|
||||||
|
* @return Response
|
||||||
|
* @throws \PhpOffice\PhpSpreadsheet\Exception
|
||||||
|
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||||
|
*/
|
||||||
|
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||||
|
{
|
||||||
|
$spreadsheet = $this->fromArrayToSpreadsheet($timesheets, $query);
|
||||||
|
$filename = $this->saveSpreadsheet($spreadsheet);
|
||||||
|
|
||||||
|
return $this->getFileResponse($filename, 'kimai-export' . $this->getFileExtension());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
abstract public function getFileExtension(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param mixed $file
|
||||||
|
* @param string $filename
|
||||||
|
* @return BinaryFileResponse
|
||||||
|
*/
|
||||||
|
protected function getFileResponse($file, $filename): Response
|
||||||
|
{
|
||||||
|
$response = new BinaryFileResponse($file);
|
||||||
|
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, $filename);
|
||||||
|
|
||||||
|
$response->headers->set('Content-Type', $this->getContentType());
|
||||||
|
$response->headers->set('Content-Disposition', $disposition);
|
||||||
|
$response->deleteFileAfterSend(true);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
abstract protected function getContentType(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Spreadsheet $spreadsheet
|
||||||
|
* @return string
|
||||||
|
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||||
|
*/
|
||||||
|
abstract protected function saveSpreadsheet(Spreadsheet $spreadsheet): string;
|
||||||
|
}
|
||||||
71
src/Export/Renderer/CsvRenderer.php
Normal file
71
src/Export/Renderer/CsvRenderer.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\RendererInterface;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
|
class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFileExtension(): string
|
||||||
|
{
|
||||||
|
return '.csv';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getContentType(): string
|
||||||
|
{
|
||||||
|
return 'text/csv';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Spreadsheet $spreadsheet
|
||||||
|
* @return bool|string
|
||||||
|
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||||
|
*/
|
||||||
|
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||||
|
{
|
||||||
|
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-csv');
|
||||||
|
$writer = IOFactory::createWriter($spreadsheet, 'Csv');
|
||||||
|
$writer->save($filename);
|
||||||
|
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'csv';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIcon(): string
|
||||||
|
{
|
||||||
|
return 'csv';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'csv';
|
||||||
|
}
|
||||||
|
}
|
||||||
79
src/Export/Renderer/HtmlRenderer.php
Normal file
79
src/Export/Renderer/HtmlRenderer.php
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Export\RendererInterface;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
class HtmlRenderer implements RendererInterface
|
||||||
|
{
|
||||||
|
use RendererTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Twig_Environment
|
||||||
|
*/
|
||||||
|
protected $twig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Twig_Environment $twig
|
||||||
|
*/
|
||||||
|
public function __construct(\Twig_Environment $twig)
|
||||||
|
{
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Timesheet[] $timesheets
|
||||||
|
* @param TimesheetQuery $query
|
||||||
|
* @return Response
|
||||||
|
* @throws \Twig_Error_Loader
|
||||||
|
* @throws \Twig_Error_Runtime
|
||||||
|
* @throws \Twig_Error_Syntax
|
||||||
|
*/
|
||||||
|
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||||
|
{
|
||||||
|
$content = $this->twig->render('export/renderer/default.html.twig', [
|
||||||
|
'entries' => $timesheets,
|
||||||
|
'query' => $query,
|
||||||
|
'summaries' => $this->calculateSummary($timesheets),
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = new Response();
|
||||||
|
$response->setContent($content);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'html';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIcon(): string
|
||||||
|
{
|
||||||
|
return 'print';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'print';
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Export/Renderer/OdsRenderer.php
Normal file
71
src/Export/Renderer/OdsRenderer.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\RendererInterface;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
|
class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFileExtension(): string
|
||||||
|
{
|
||||||
|
return '.ods';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getContentType(): string
|
||||||
|
{
|
||||||
|
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Spreadsheet $spreadsheet
|
||||||
|
* @return bool|string
|
||||||
|
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||||
|
*/
|
||||||
|
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||||
|
{
|
||||||
|
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-ods');
|
||||||
|
$writer = IOFactory::createWriter($spreadsheet, 'Ods');
|
||||||
|
$writer->save($filename);
|
||||||
|
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'ods';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIcon(): string
|
||||||
|
{
|
||||||
|
return 'ods';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'ods';
|
||||||
|
}
|
||||||
|
}
|
||||||
94
src/Export/Renderer/PDFRenderer.php
Normal file
94
src/Export/Renderer/PDFRenderer.php
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Export\RendererInterface;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use Mpdf\Mpdf;
|
||||||
|
use Mpdf\Output\Destination;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
use Symfony\Component\HttpFoundation\ResponseHeaderBag;
|
||||||
|
|
||||||
|
class PDFRenderer implements RendererInterface
|
||||||
|
{
|
||||||
|
use RendererTrait;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var \Twig_Environment
|
||||||
|
*/
|
||||||
|
protected $twig;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param \Twig_Environment $twig
|
||||||
|
*/
|
||||||
|
public function __construct(\Twig_Environment $twig)
|
||||||
|
{
|
||||||
|
$this->twig = $twig;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Timesheet[] $timesheets
|
||||||
|
* @param TimesheetQuery $query
|
||||||
|
* @return Response
|
||||||
|
* @throws \Mpdf\MpdfException
|
||||||
|
* @throws \Twig_Error_Loader
|
||||||
|
* @throws \Twig_Error_Runtime
|
||||||
|
* @throws \Twig_Error_Syntax
|
||||||
|
*/
|
||||||
|
public function render(array $timesheets, TimesheetQuery $query): Response
|
||||||
|
{
|
||||||
|
$content = $this->twig->render('export/renderer/pdf.html.twig', [
|
||||||
|
'entries' => $timesheets,
|
||||||
|
'query' => $query,
|
||||||
|
'now' => new \DateTime(),
|
||||||
|
'summaries' => $this->calculateSummary($timesheets),
|
||||||
|
]);
|
||||||
|
|
||||||
|
//return new Response($content);
|
||||||
|
|
||||||
|
$mpdf = new Mpdf();
|
||||||
|
$mpdf->WriteHTML($content);
|
||||||
|
$content = $mpdf->Output('test', Destination::STRING_RETURN);
|
||||||
|
|
||||||
|
$response = new Response($content);
|
||||||
|
|
||||||
|
$disposition = $response->headers->makeDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT, 'kimai-export.pdf');
|
||||||
|
|
||||||
|
$response->headers->set('Content-Type', 'application/pdf');
|
||||||
|
$response->headers->set('Content-Disposition', $disposition);
|
||||||
|
|
||||||
|
return $response;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'pdf';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIcon(): string
|
||||||
|
{
|
||||||
|
return 'pdf';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'pdf';
|
||||||
|
}
|
||||||
|
}
|
||||||
43
src/Export/Renderer/RendererTrait.php
Normal file
43
src/Export/Renderer/RendererTrait.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
|
||||||
|
trait RendererTrait
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Timesheet[] $timesheets
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
protected function calculateSummary(array $timesheets)
|
||||||
|
{
|
||||||
|
$summary = [];
|
||||||
|
|
||||||
|
foreach ($timesheets as $timesheet) {
|
||||||
|
$id = $timesheet->getProject()->getCustomer()->getId() . '_' . $timesheet->getProject()->getId();
|
||||||
|
if (!isset($summary[$id])) {
|
||||||
|
$summary[$id] = [
|
||||||
|
'customer' => $timesheet->getProject()->getCustomer()->getName(),
|
||||||
|
'project' => $timesheet->getProject()->getName(),
|
||||||
|
'currency' => $timesheet->getProject()->getCustomer()->getCurrency(),
|
||||||
|
'rate' => 0,
|
||||||
|
'duration' => 0,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$summary[$id]['rate'] += $timesheet->getRate();
|
||||||
|
$summary[$id]['duration'] += $timesheet->getDuration();
|
||||||
|
}
|
||||||
|
|
||||||
|
asort($summary);
|
||||||
|
|
||||||
|
return $summary;
|
||||||
|
}
|
||||||
|
}
|
||||||
71
src/Export/Renderer/XlsxRenderer.php
Normal file
71
src/Export/Renderer/XlsxRenderer.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\RendererInterface;
|
||||||
|
use PhpOffice\PhpSpreadsheet\IOFactory;
|
||||||
|
use PhpOffice\PhpSpreadsheet\Spreadsheet;
|
||||||
|
|
||||||
|
class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getFileExtension(): string
|
||||||
|
{
|
||||||
|
return '.xlsx';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
protected function getContentType(): string
|
||||||
|
{
|
||||||
|
return 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Spreadsheet $spreadsheet
|
||||||
|
* @return bool|string
|
||||||
|
* @throws \PhpOffice\PhpSpreadsheet\Writer\Exception
|
||||||
|
*/
|
||||||
|
protected function saveSpreadsheet(Spreadsheet $spreadsheet): string
|
||||||
|
{
|
||||||
|
$filename = tempnam(sys_get_temp_dir(), 'kimai-export-xlsx');
|
||||||
|
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||||
|
$writer->save($filename);
|
||||||
|
|
||||||
|
return $filename;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getId(): string
|
||||||
|
{
|
||||||
|
return 'xlsx';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIcon(): string
|
||||||
|
{
|
||||||
|
return 'xlsx';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return 'xlsx';
|
||||||
|
}
|
||||||
|
}
|
||||||
39
src/Export/RendererInterface.php
Normal file
39
src/Export/RendererInterface.php
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
<?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\Export;
|
||||||
|
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
interface RendererInterface
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param Timesheet[] $timesheets
|
||||||
|
* @param TimesheetQuery $query
|
||||||
|
* @return Response
|
||||||
|
*/
|
||||||
|
public function render(array $timesheets, TimesheetQuery $query): Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getId(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getIcon(): string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getTitle(): string;
|
||||||
|
}
|
||||||
54
src/Export/ServiceExport.php
Normal file
54
src/Export/ServiceExport.php
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
<?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\Export;
|
||||||
|
|
||||||
|
class ServiceExport
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @var RendererInterface[]
|
||||||
|
*/
|
||||||
|
protected $renderer = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param RendererInterface $renderer
|
||||||
|
* @return $this
|
||||||
|
*/
|
||||||
|
public function addRenderer(RendererInterface $renderer)
|
||||||
|
{
|
||||||
|
$this->renderer[] = $renderer;
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an array of export renderer.
|
||||||
|
*
|
||||||
|
* @return RendererInterface[]
|
||||||
|
*/
|
||||||
|
public function getRenderer()
|
||||||
|
{
|
||||||
|
return $this->renderer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $id
|
||||||
|
* @return RendererInterface|null
|
||||||
|
*/
|
||||||
|
public function getRendererById(string $id)
|
||||||
|
{
|
||||||
|
foreach ($this->renderer as $renderer) {
|
||||||
|
if ($renderer->getId() === $id) {
|
||||||
|
return $renderer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -16,6 +16,7 @@ use App\Form\Type\DateTimePickerType;
|
|||||||
use App\Form\Type\DurationType;
|
use App\Form\Type\DurationType;
|
||||||
use App\Form\Type\ProjectType;
|
use App\Form\Type\ProjectType;
|
||||||
use App\Form\Type\UserType;
|
use App\Form\Type\UserType;
|
||||||
|
use App\Form\Type\YesNoType;
|
||||||
use App\Repository\ActivityRepository;
|
use App\Repository\ActivityRepository;
|
||||||
use App\Repository\CustomerRepository;
|
use App\Repository\CustomerRepository;
|
||||||
use App\Repository\ProjectRepository;
|
use App\Repository\ProjectRepository;
|
||||||
@@ -226,6 +227,12 @@ class TimesheetEditForm extends AbstractType
|
|||||||
if ($options['include_user']) {
|
if ($options['include_user']) {
|
||||||
$builder->add('user', UserType::class);
|
$builder->add('user', UserType::class);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($options['include_exported']) {
|
||||||
|
$builder->add('exported', YesNoType::class, [
|
||||||
|
'label' => 'label.exported'
|
||||||
|
]);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -240,6 +247,7 @@ class TimesheetEditForm extends AbstractType
|
|||||||
'csrf_token_id' => 'timesheet_edit',
|
'csrf_token_id' => 'timesheet_edit',
|
||||||
'duration_only' => $this->durationOnly,
|
'duration_only' => $this->durationOnly,
|
||||||
'include_user' => false,
|
'include_user' => false,
|
||||||
|
'include_exported' => false,
|
||||||
'include_rate' => true,
|
'include_rate' => true,
|
||||||
'docu_chapter' => 'timesheet',
|
'docu_chapter' => 'timesheet',
|
||||||
'method' => 'POST',
|
'method' => 'POST',
|
||||||
|
|||||||
73
src/Form/Toolbar/ExportToolbarForm.php
Normal file
73
src/Form/Toolbar/ExportToolbarForm.php
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
<?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\Form\Toolbar;
|
||||||
|
|
||||||
|
use App\Repository\Query\ExportQuery;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\HiddenType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Defines the form used for filtering timesheet entries for exports.
|
||||||
|
*/
|
||||||
|
class ExportToolbarForm extends AbstractToolbarForm
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||||
|
{
|
||||||
|
$this->addExportStateChoice($builder);
|
||||||
|
$this->addUserChoice($builder);
|
||||||
|
$this->addDateRangeChoice($builder);
|
||||||
|
$this->addCustomerChoice($builder);
|
||||||
|
$this->addProjectChoice($builder);
|
||||||
|
$this->addActivityChoice($builder);
|
||||||
|
$this->addExportType($builder);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param FormBuilderInterface $builder
|
||||||
|
*/
|
||||||
|
protected function addExportType(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
$builder->add('type', HiddenType::class, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param FormBuilderInterface $builder
|
||||||
|
*/
|
||||||
|
protected function addExportStateChoice(FormBuilderInterface $builder)
|
||||||
|
{
|
||||||
|
$builder->add('exported', ChoiceType::class, [
|
||||||
|
'label' => 'label.exported',
|
||||||
|
'required' => false,
|
||||||
|
'placeholder' => null,
|
||||||
|
'choices' => [
|
||||||
|
'entryState.all' => TimesheetQuery::STATE_ALL,
|
||||||
|
'entryState.exported' => TimesheetQuery::STATE_EXPORTED,
|
||||||
|
'entryState.not_exported' => TimesheetQuery::STATE_NOT_EXPORTED
|
||||||
|
],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function configureOptions(OptionsResolver $resolver)
|
||||||
|
{
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'data_class' => ExportQuery::class,
|
||||||
|
'csrf_protection' => false,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,7 @@ class YesNoType extends AbstractType
|
|||||||
{
|
{
|
||||||
$resolver->setDefaults([
|
$resolver->setDefaults([
|
||||||
'value' => true,
|
'value' => true,
|
||||||
'false_values' => [null, 0, false],
|
'false_values' => [null, 0, false, 'false'],
|
||||||
'required' => false,
|
'required' => false,
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class CsvRenderer extends AbstractSpreadsheetRenderer implements RendererInterfa
|
|||||||
*/
|
*/
|
||||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
||||||
{
|
{
|
||||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-csv');
|
$filename = tempnam(sys_get_temp_dir(), 'kimai-invoice-csv');
|
||||||
$writer = IOFactory::createWriter($spreadsheet, 'Csv');
|
$writer = IOFactory::createWriter($spreadsheet, 'Csv');
|
||||||
$writer->save($filename);
|
$writer->save($filename);
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class OdsRenderer extends AbstractSpreadsheetRenderer implements RendererInterfa
|
|||||||
*/
|
*/
|
||||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
||||||
{
|
{
|
||||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-ods');
|
$filename = tempnam(sys_get_temp_dir(), 'kimai-invoice-ods');
|
||||||
$writer = IOFactory::createWriter($spreadsheet, 'Ods');
|
$writer = IOFactory::createWriter($spreadsheet, 'Ods');
|
||||||
$writer->save($filename);
|
$writer->save($filename);
|
||||||
|
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ class XlsxRenderer extends AbstractSpreadsheetRenderer implements RendererInterf
|
|||||||
*/
|
*/
|
||||||
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
protected function saveSpreadsheet(Spreadsheet $spreadsheet)
|
||||||
{
|
{
|
||||||
$filename = tempnam(sys_get_temp_dir(), 'kimai-xslx');
|
$filename = tempnam(sys_get_temp_dir(), 'kimai-invoice-xlsx');
|
||||||
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
$writer = IOFactory::createWriter($spreadsheet, 'Xlsx');
|
||||||
$writer->save($filename);
|
$writer->save($filename);
|
||||||
|
|
||||||
|
|||||||
@@ -11,11 +11,13 @@ namespace App;
|
|||||||
|
|
||||||
use App\DependencyInjection\AppExtension;
|
use App\DependencyInjection\AppExtension;
|
||||||
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
|
use App\DependencyInjection\Compiler\DoctrineCompilerPass;
|
||||||
|
use App\DependencyInjection\Compiler\ExportServiceCompilerPass;
|
||||||
use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
|
use App\DependencyInjection\Compiler\InvoiceServiceCompilerPass;
|
||||||
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
|
use App\DependencyInjection\Compiler\TwigContextCompilerPass;
|
||||||
|
use App\Export\RendererInterface as ExportRendererInterface;
|
||||||
use App\Invoice\CalculatorInterface as InvoiceCalculator;
|
use App\Invoice\CalculatorInterface as InvoiceCalculator;
|
||||||
use App\Invoice\NumberGeneratorInterface;
|
use App\Invoice\NumberGeneratorInterface;
|
||||||
use App\Invoice\RendererInterface;
|
use App\Invoice\RendererInterface as InvoiceRendererInterface;
|
||||||
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
use App\Timesheet\CalculatorInterface as TimesheetCalculator;
|
||||||
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
use Symfony\Bundle\FrameworkBundle\Kernel\MicroKernelTrait;
|
||||||
use Symfony\Component\Config\Loader\LoaderInterface;
|
use Symfony\Component\Config\Loader\LoaderInterface;
|
||||||
@@ -30,6 +32,7 @@ class Kernel extends BaseKernel
|
|||||||
|
|
||||||
public const CONFIG_EXTS = '.{php,xml,yaml,yml}';
|
public const CONFIG_EXTS = '.{php,xml,yaml,yml}';
|
||||||
|
|
||||||
|
public const TAG_EXPORT_RENDERER = 'export.renderer';
|
||||||
public const TAG_INVOICE_RENDERER = 'invoice.renderer';
|
public const TAG_INVOICE_RENDERER = 'invoice.renderer';
|
||||||
public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator';
|
public const TAG_INVOICE_NUMBER_GENERATOR = 'invoice.number_generator';
|
||||||
public const TAG_INVOICE_CALCULATOR = 'invoice.calculator';
|
public const TAG_INVOICE_CALCULATOR = 'invoice.calculator';
|
||||||
@@ -47,7 +50,8 @@ class Kernel extends BaseKernel
|
|||||||
protected function build(ContainerBuilder $container)
|
protected function build(ContainerBuilder $container)
|
||||||
{
|
{
|
||||||
$container->registerForAutoconfiguration(TimesheetCalculator::class)->addTag('timesheet.calculator');
|
$container->registerForAutoconfiguration(TimesheetCalculator::class)->addTag('timesheet.calculator');
|
||||||
$container->registerForAutoconfiguration(RendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
|
$container->registerForAutoconfiguration(ExportRendererInterface::class)->addTag(self::TAG_EXPORT_RENDERER);
|
||||||
|
$container->registerForAutoconfiguration(InvoiceRendererInterface::class)->addTag(self::TAG_INVOICE_RENDERER);
|
||||||
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
|
$container->registerForAutoconfiguration(NumberGeneratorInterface::class)->addTag(self::TAG_INVOICE_NUMBER_GENERATOR);
|
||||||
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
|
$container->registerForAutoconfiguration(InvoiceCalculator::class)->addTag(self::TAG_INVOICE_CALCULATOR);
|
||||||
}
|
}
|
||||||
@@ -80,6 +84,7 @@ class Kernel extends BaseKernel
|
|||||||
$container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
$container->addCompilerPass(new DoctrineCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||||
$container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
$container->addCompilerPass(new TwigContextCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||||
$container->addCompilerPass(new InvoiceServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
$container->addCompilerPass(new InvoiceServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||||
|
$container->addCompilerPass(new ExportServiceCompilerPass(), PassConfig::TYPE_BEFORE_OPTIMIZATION, -1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function configureRoutes(RouteCollectionBuilder $routes)
|
protected function configureRoutes(RouteCollectionBuilder $routes)
|
||||||
|
|||||||
65
src/Migrations/Version20190124004014.php
Normal file
65
src/Migrations/Version20190124004014.php
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
/*
|
||||||
|
* 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 DoctrineMigrations;
|
||||||
|
|
||||||
|
use App\Doctrine\AbstractMigration;
|
||||||
|
use Doctrine\DBAL\Schema\Schema;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds the exported column to the timesheet table
|
||||||
|
*/
|
||||||
|
final class Version20190124004014 extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(Schema $schema): void
|
||||||
|
{
|
||||||
|
$platform = $this->getPlatform();
|
||||||
|
|
||||||
|
if (!in_array($platform, ['sqlite', 'mysql'])) {
|
||||||
|
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
$timesheet = $this->getTableName('timesheet');
|
||||||
|
|
||||||
|
if ($platform === 'sqlite') {
|
||||||
|
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD COLUMN exported BOOLEAN NOT NULL DEFAULT false');
|
||||||
|
} else {
|
||||||
|
$this->addSql('ALTER TABLE ' . $timesheet . ' ADD exported TINYINT(1) NOT NULL DEFAULT false');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function down(Schema $schema): void
|
||||||
|
{
|
||||||
|
$platform = $this->getPlatform();
|
||||||
|
|
||||||
|
if (!in_array($platform, ['sqlite', 'mysql'])) {
|
||||||
|
$this->abortIf(true, 'Unsupported database platform: ' . $platform);
|
||||||
|
}
|
||||||
|
|
||||||
|
$timesheet = $this->getTableName('timesheet');
|
||||||
|
|
||||||
|
if ($platform === 'sqlite') {
|
||||||
|
$this->addSql('DROP INDEX IDX_4F60C6B1166D1F9C');
|
||||||
|
$this->addSql('DROP INDEX IDX_4F60C6B18D93D649');
|
||||||
|
$this->addSql('DROP INDEX IDX_4F60C6B181C06096');
|
||||||
|
$this->addSql('CREATE TEMPORARY TABLE __temp__' . $timesheet . ' AS SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM ' . $timesheet);
|
||||||
|
$this->addSql('DROP TABLE ' . $timesheet);
|
||||||
|
$this->addSql('CREATE TABLE ' . $timesheet . ' (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, user INTEGER NOT NULL, activity_id INTEGER NOT NULL, project_id INTEGER NOT NULL, start_time DATETIME NOT NULL, end_time DATETIME DEFAULT NULL, duration INTEGER DEFAULT NULL, description CLOB DEFAULT NULL, rate NUMERIC(10, 2) NOT NULL, fixed_rate NUMERIC(10, 2) DEFAULT NULL, hourly_rate NUMERIC(10, 2) DEFAULT NULL)');
|
||||||
|
$this->addSql('INSERT INTO ' . $timesheet . ' (id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate) SELECT id, user, activity_id, project_id, start_time, end_time, duration, description, rate, fixed_rate, hourly_rate FROM __temp__' . $timesheet);
|
||||||
|
$this->addSql('DROP TABLE __temp__' . $timesheet);
|
||||||
|
$this->addSql('CREATE INDEX IDX_4F60C6B1166D1F9C ON ' . $timesheet . ' (project_id)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_4F60C6B18D93D649 ON ' . $timesheet . ' (user)');
|
||||||
|
$this->addSql('CREATE INDEX IDX_4F60C6B181C06096 ON ' . $timesheet . ' (activity_id)');
|
||||||
|
} else {
|
||||||
|
$this->addSql('ALTER TABLE ' . $timesheet . ' DROP exported');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
src/Repository/Query/ExportQuery.php
Normal file
48
src/Repository/Query/ExportQuery.php
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
<?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\Repository\Query;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Can be used for export queries.
|
||||||
|
*/
|
||||||
|
class ExportQuery extends TimesheetQuery
|
||||||
|
{
|
||||||
|
public const TYPE_HTML = 'html';
|
||||||
|
public const TYPE_CSV = 'csv';
|
||||||
|
public const TYPE_PDF = 'pdf';
|
||||||
|
public const TYPE_XLSX = 'xlsx';
|
||||||
|
public const TYPE_ODS = 'ods';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string
|
||||||
|
*/
|
||||||
|
protected $type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getType(): ?string
|
||||||
|
{
|
||||||
|
return $this->type;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $type
|
||||||
|
* @return ExportQuery
|
||||||
|
*/
|
||||||
|
public function setType(string $type)
|
||||||
|
{
|
||||||
|
if (in_array($type, [self::TYPE_PDF, self::TYPE_CSV, self::TYPE_HTML, self::TYPE_XLSX, self::TYPE_ODS])) {
|
||||||
|
$this->type = $type;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -22,6 +22,8 @@ class InvoiceQuery extends TimesheetQuery
|
|||||||
protected $template;
|
protected $template;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* TODO can this be removed ???
|
||||||
|
*
|
||||||
* @var InvoiceTemplate[]
|
* @var InvoiceTemplate[]
|
||||||
*/
|
*/
|
||||||
protected $templates = [];
|
protected $templates = [];
|
||||||
|
|||||||
@@ -21,6 +21,8 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
public const STATE_ALL = 1;
|
public const STATE_ALL = 1;
|
||||||
public const STATE_RUNNING = 2;
|
public const STATE_RUNNING = 2;
|
||||||
public const STATE_STOPPED = 3;
|
public const STATE_STOPPED = 3;
|
||||||
|
public const STATE_EXPORTED = 4;
|
||||||
|
public const STATE_NOT_EXPORTED = 5;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Overwritten for different default order
|
* Overwritten for different default order
|
||||||
@@ -44,6 +46,10 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
* @var int
|
* @var int
|
||||||
*/
|
*/
|
||||||
protected $state = self::STATE_ALL;
|
protected $state = self::STATE_ALL;
|
||||||
|
/**
|
||||||
|
* @var int
|
||||||
|
*/
|
||||||
|
protected $exported = self::STATE_ALL;
|
||||||
/**
|
/**
|
||||||
* @var DateRange
|
* @var DateRange
|
||||||
*/
|
*/
|
||||||
@@ -84,10 +90,10 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Activity $activity
|
* @param Activity|int $activity
|
||||||
* @return TimesheetQuery
|
* @return TimesheetQuery
|
||||||
*/
|
*/
|
||||||
public function setActivity(Activity $activity = null)
|
public function setActivity($activity = null)
|
||||||
{
|
{
|
||||||
$this->activity = $activity;
|
$this->activity = $activity;
|
||||||
|
|
||||||
@@ -108,7 +114,7 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
*/
|
*/
|
||||||
public function setState($state)
|
public function setState($state)
|
||||||
{
|
{
|
||||||
if (!is_int($state) && $state != (int) $state) {
|
if (!is_int($state) && $state !== (int) $state) {
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -120,6 +126,32 @@ class TimesheetQuery extends ActivityQuery
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return int
|
||||||
|
*/
|
||||||
|
public function getExported()
|
||||||
|
{
|
||||||
|
return $this->exported;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param int $exported
|
||||||
|
* @return TimesheetQuery
|
||||||
|
*/
|
||||||
|
public function setExported($exported)
|
||||||
|
{
|
||||||
|
if (!is_int($exported) && $exported !== (int) $exported) {
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exported = (int) $exported;
|
||||||
|
if (in_array($exported, [self::STATE_ALL, self::STATE_EXPORTED, self::STATE_NOT_EXPORTED], true)) {
|
||||||
|
$this->exported = $exported;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return \DateTime
|
* @return \DateTime
|
||||||
*/
|
*/
|
||||||
|
|||||||
@@ -338,11 +338,18 @@ class TimesheetRepository extends AbstractRepository
|
|||||||
->setParameter('end', $query->getEnd());
|
->setParameter('end', $query->getEnd());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if ($query->getExported() === TimesheetQuery::STATE_EXPORTED) {
|
||||||
|
$qb->andWhere('t.exported = :exported')->setParameter('exported', true);
|
||||||
|
} elseif ($query->getExported() === TimesheetQuery::STATE_NOT_EXPORTED) {
|
||||||
|
$qb->andWhere('t.exported = :exported')->setParameter('exported', false);
|
||||||
|
}
|
||||||
|
|
||||||
if (null !== $query->getActivity()) {
|
if (null !== $query->getActivity()) {
|
||||||
$qb->andWhere('t.activity = :activity')
|
$qb->andWhere('t.activity = :activity')
|
||||||
->setParameter('activity', $query->getActivity());
|
->setParameter('activity', $query->getActivity());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO if activity is an int, this will fail
|
||||||
if (null === $query->getActivity() || null === $query->getActivity()->getProject()) {
|
if (null === $query->getActivity() || null === $query->getActivity()->getProject()) {
|
||||||
if (null !== $query->getProject()) {
|
if (null !== $query->getProject()) {
|
||||||
$qb->andWhere('t.project = :project')
|
$qb->andWhere('t.project = :project')
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ class DateExtensions extends \Twig_Extension
|
|||||||
return [
|
return [
|
||||||
new TwigFilter('month_name', [$this, 'monthName']),
|
new TwigFilter('month_name', [$this, 'monthName']),
|
||||||
new TwigFilter('date_short', [$this, 'dateShort']),
|
new TwigFilter('date_short', [$this, 'dateShort']),
|
||||||
|
new TwigFilter('date_time', [$this, 'dateTime']),
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,6 +54,17 @@ class DateExtensions extends \Twig_Extension
|
|||||||
return date_format($date, $format);
|
return date_format($date, $format);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param DateTime $date
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function dateTime(DateTime $date)
|
||||||
|
{
|
||||||
|
$format = $this->localeSettings->getDateTimeFormat();
|
||||||
|
|
||||||
|
return date_format($date, $format);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \DateTime $date
|
* @param \DateTime $date
|
||||||
* @return string
|
* @return string
|
||||||
|
|||||||
@@ -86,6 +86,13 @@ class Extensions extends \Twig_Extension
|
|||||||
'user' => 'fas fa-user',
|
'user' => 'fas fa-user',
|
||||||
'visibility' => 'far fa-eye',
|
'visibility' => 'far fa-eye',
|
||||||
'settings' => 'fas fa-wrench',
|
'settings' => 'fas fa-wrench',
|
||||||
|
'export' => 'fas fa-database',
|
||||||
|
'pdf' => 'fas fa-file-pdf',
|
||||||
|
'csv' => 'fas fa-table',
|
||||||
|
'ods' => 'fas fa-table',
|
||||||
|
'xlsx' => 'fas fa-file-excel',
|
||||||
|
'on' => 'fas fa-toggle-on',
|
||||||
|
'off' => 'fas fa-toggle-off',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -111,6 +111,17 @@ class LocaleSettings
|
|||||||
return $this->getConfigByLocaleAndKey('date', $locale);
|
return $this->getConfigByLocaleAndKey('date', $locale);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the locale specific datetime format, which should be used in combination with the twig filter "|date".
|
||||||
|
*
|
||||||
|
* @param null|string $locale
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getDateTimeFormat(?string $locale = null): string
|
||||||
|
{
|
||||||
|
return $this->getConfigByLocaleAndKey('date_time', $locale);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the format used in the "|duration" twig filter to display a Timesheet duration.
|
* Returns the format used in the "|duration" twig filter to display a Timesheet duration.
|
||||||
*
|
*
|
||||||
@@ -143,5 +154,4 @@ class LocaleSettings
|
|||||||
|
|
||||||
return $this->settings[$locale][$key];
|
return $this->settings[$locale][$key];
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
public const EXPORT = 'export';
|
public const EXPORT = 'export';
|
||||||
public const VIEW_RATE = 'view_rate';
|
public const VIEW_RATE = 'view_rate';
|
||||||
public const EDIT_RATE = 'edit_rate';
|
public const EDIT_RATE = 'edit_rate';
|
||||||
|
public const EDIT_EXPORT = 'edit_export';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* support rules based on the given $subject (here: Timesheet)
|
* support rules based on the given $subject (here: Timesheet)
|
||||||
@@ -38,6 +39,7 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
self::EXPORT,
|
self::EXPORT,
|
||||||
self::VIEW_RATE,
|
self::VIEW_RATE,
|
||||||
self::EDIT_RATE,
|
self::EDIT_RATE,
|
||||||
|
self::EDIT_EXPORT,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -92,6 +94,7 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
case self::EDIT:
|
case self::EDIT:
|
||||||
case self::DELETE:
|
case self::DELETE:
|
||||||
case self::EXPORT:
|
case self::EXPORT:
|
||||||
|
case self::EDIT_EXPORT:
|
||||||
$permission .= $attribute;
|
$permission .= $attribute;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
|||||||
@@ -167,6 +167,9 @@
|
|||||||
"monolog/monolog": {
|
"monolog/monolog": {
|
||||||
"version": "1.23.0"
|
"version": "1.23.0"
|
||||||
},
|
},
|
||||||
|
"mpdf/mpdf": {
|
||||||
|
"version": "v7.1.8"
|
||||||
|
},
|
||||||
"myclabs/deep-copy": {
|
"myclabs/deep-copy": {
|
||||||
"version": "1.7.0"
|
"version": "1.7.0"
|
||||||
},
|
},
|
||||||
@@ -311,6 +314,9 @@
|
|||||||
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
|
"ref": "aaddfdf43cdecd4cf91f992052d76c2cadc04543"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"setasign/fpdi": {
|
||||||
|
"version": "1.6.2"
|
||||||
|
},
|
||||||
"squizlabs/php_codesniffer": {
|
"squizlabs/php_codesniffer": {
|
||||||
"version": "3.2.2"
|
"version": "3.2.2"
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{% if entries.count == 0 %}
|
{% if entries.count == 0 %}
|
||||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
|
|
||||||
{% set columns = {
|
{% set columns = {
|
||||||
'name': 'alwaysVisible',
|
'name': 'alwaysVisible',
|
||||||
'customer': 'hidden-xs',
|
'customer': 'hidden-xs',
|
||||||
@@ -68,4 +68,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{{ tables.data_table_footer(entries, 'admin_activity_paginated') }}
|
{{ tables.data_table_footer(entries, 'admin_activity_paginated') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,13 +18,12 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{% if entries.count == 0 %}
|
{% if entries.count == 0 %}
|
||||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
|
|
||||||
{% set columns = {
|
{% set columns = {
|
||||||
'name': 'alwaysVisible',
|
'name': 'alwaysVisible',
|
||||||
'project': '',
|
|
||||||
'comment': 'hidden-xs',
|
'comment': 'hidden-xs',
|
||||||
'country': 'hidden-xs',
|
'country': 'hidden-xs',
|
||||||
'customer_number': 'hidden-xs',
|
'customer_number': 'hidden-xs',
|
||||||
@@ -40,12 +39,6 @@
|
|||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{ entry.name }} {% if entry.company is not empty %}({{ entry.company }}){% endif %}</td>
|
<td>{{ entry.name }} {% if entry.company is not empty %}({{ entry.company }}){% endif %}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'project') }}">
|
|
||||||
{% for project in entry.projects %}
|
|
||||||
<a href="{{ path('admin_project_edit', {'id' : project.id}) }}">{{ widgets.label_project(project) }}</a>
|
|
||||||
<br class="visible-xs-block">
|
|
||||||
{% endfor %}
|
|
||||||
</td>
|
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'comment') }}">{{ entry.comment }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'country') }}">{{ entry.country|country }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'country') }}">{{ entry.country|country }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer_number') }}">{{ entry.number }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'customer_number') }}">{{ entry.number }}</td>
|
||||||
@@ -56,6 +49,9 @@
|
|||||||
{% if is_granted('edit', entry) %}
|
{% if is_granted('edit', entry) %}
|
||||||
{% set actionButtons = {'edit': path('admin_customer_edit', {'id': entry.id})}|merge(actionButtons) %}
|
{% set actionButtons = {'edit': path('admin_customer_edit', {'id': entry.id})}|merge(actionButtons) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if is_granted('view_project') %}
|
||||||
|
{% set actionButtons = actionButtons|merge({'project': path('admin_project', {'customer': entry.id})}) %}
|
||||||
|
{% endif %}
|
||||||
{% if is_granted('delete', entry) %}
|
{% if is_granted('delete', entry) %}
|
||||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_customer_delete', {'id': entry.id})}) %}
|
{% set actionButtons = actionButtons|merge({'trash': path('admin_customer_delete', {'id': entry.id})}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -65,4 +61,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{{ tables.data_table_footer(entries, 'admin_customer_paginated') }}
|
{{ tables.data_table_footer(entries, 'admin_customer_paginated') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -18,10 +18,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{% if entries.count == 0 %}
|
{% if entries.count == 0 %}
|
||||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
|
|
||||||
{% set columns = {
|
{% set columns = {
|
||||||
'name': 'alwaysVisible',
|
'name': 'alwaysVisible',
|
||||||
'customer': '',
|
'customer': '',
|
||||||
@@ -49,6 +49,9 @@
|
|||||||
{% if is_granted('edit', entry) %}
|
{% if is_granted('edit', entry) %}
|
||||||
{% set actionButtons = {'edit': path('admin_project_edit', {'id': entry.id})}|merge(actionButtons) %}
|
{% set actionButtons = {'edit': path('admin_project_edit', {'id': entry.id})}|merge(actionButtons) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if is_granted('view_activity') %}
|
||||||
|
{% set actionButtons = actionButtons|merge({'activity': path('admin_activity', {'customer': entry.customer.id, 'project': entry.id})}) %}
|
||||||
|
{% endif %}
|
||||||
{% if is_granted('delete', entry) %}
|
{% if is_granted('delete', entry) %}
|
||||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_project_delete', {'id': entry.id})}) %}
|
{% set actionButtons = actionButtons|merge({'trash': path('admin_project_delete', {'id': entry.id})}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -58,4 +61,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{{ tables.data_table_footer(entries, 'admin_project_paginated') }}
|
{{ tables.data_table_footer(entries, 'admin_project_paginated') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -22,10 +22,10 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{% if entries.count == 0 %}
|
{% if entries.count == 0 %}
|
||||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
|
|
||||||
{% set columns = {'date': ''} %}
|
{% set columns = {'date': ''} %}
|
||||||
|
|
||||||
{% if not duration_only %}
|
{% if not duration_only %}
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
|
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
|
||||||
|
|
||||||
{% if not duration_only %}
|
{% if not duration_only %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'starttime') }}">{{ entry.begin|date("H:i") }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'starttime') }}">{{ entry.begin|date("H:i") }}</td>
|
||||||
@@ -59,14 +59,14 @@
|
|||||||
{% if not duration_only %}
|
{% if not duration_only %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">{{ entry.end|date("H:i") }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">{{ entry.end|date("H:i") }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if not duration_only %}
|
{% if not duration_only %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">‐</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">‐</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}"><i>{{ entry|duration }}</i></td>
|
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}"><i>{{ entry|duration }}</i></td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">
|
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">
|
||||||
{% if not entry.end or not is_granted('view_rate', entry) %}
|
{% if not entry.end or not is_granted('view_rate', entry) %}
|
||||||
‐
|
‐
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -101,6 +101,8 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{{ tables.data_table_footer(entries, 'admin_timesheet_paginated') }}
|
{{ tables.data_table_footer(entries, 'admin_timesheet_paginated') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block javascripts %}
|
{% block javascripts %}
|
||||||
|
|||||||
@@ -19,12 +19,11 @@
|
|||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{% if entries.count == 0 %}
|
{% if entries.count == 0 %}
|
||||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
|
|
||||||
{% set columns = {
|
{% set columns = {
|
||||||
'id': 'hidden-xs',
|
|
||||||
'alias': 'alwaysVisible',
|
'alias': 'alwaysVisible',
|
||||||
'username': 'hidden-xs',
|
'username': 'hidden-xs',
|
||||||
'email': 'hidden-xs hidden-sm',
|
'email': 'hidden-xs hidden-sm',
|
||||||
@@ -40,7 +39,6 @@
|
|||||||
|
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'id') }}">{{ entry.id }}</td>
|
|
||||||
<td>{{ widgets.username(entry) }}</td>
|
<td>{{ widgets.username(entry) }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">{{ entry.username }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'username') }}">{{ entry.username }}</td>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">{{ entry.email }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'email') }}">{{ entry.email }}</td>
|
||||||
@@ -54,7 +52,10 @@
|
|||||||
<td>
|
<td>
|
||||||
{% set actionButtons = {} %}
|
{% set actionButtons = {} %}
|
||||||
{% if is_granted('edit', entry) %}
|
{% if is_granted('edit', entry) %}
|
||||||
{% set actionButtons = {'edit': path('user_profile', {'username' : entry.username})}|merge(actionButtons) %}
|
{% set actionButtons = actionButtons|merge({'edit': path('user_profile', {'username' : entry.username})}) %}
|
||||||
|
{% endif %}
|
||||||
|
{% if is_granted('view_other_timesheet') %}
|
||||||
|
{% set actionButtons = actionButtons|merge({'timesheet': path('admin_timesheet', {'user' : entry.id})}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_granted('delete', entry) %}
|
{% if is_granted('delete', entry) %}
|
||||||
{% set actionButtons = actionButtons|merge({'trash': path('admin_user_delete', {'id': entry.id})}) %}
|
{% set actionButtons = actionButtons|merge({'trash': path('admin_user_delete', {'id': entry.id})}) %}
|
||||||
@@ -65,4 +66,6 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{{ tables.data_table_footer(entries, 'admin_user_paginated') }}
|
{{ tables.data_table_footer(entries, 'admin_user_paginated') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{% extends '@AdminLTE/layout/default-layout-avanzu.html.twig' %}
|
{% extends '@AdminLTE/layout/default-layout-avanzu.html.twig' %}
|
||||||
|
|
||||||
{% block avanzu_page_content_before %}
|
{% block avanzu_page_content_before %}
|
||||||
<div class="pad no-print">
|
<div class="toolbar-pad no-print">
|
||||||
{% block main_before %}{% endblock %}
|
{% block main_before %}{% endblock %}
|
||||||
</div>
|
</div>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -42,14 +42,14 @@
|
|||||||
|
|
||||||
{% block avanzu_navbar_messages %}
|
{% block avanzu_navbar_messages %}
|
||||||
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
|
{% if app.user is not null and is_granted('IS_AUTHENTICATED_REMEMBERED') %}
|
||||||
{% if is_granted('ROLE_USER') %}
|
{% if is_granted('view_own_timesheet') %}
|
||||||
<li class="visible-xs-inline-block">
|
<li class="visible-xs-inline-block">
|
||||||
<a href="{{ path('timesheet') }}" class="ddt-large">
|
<a href="{{ path('timesheet') }}" class="ddt-large">
|
||||||
<i class="{{ 'timesheet'|icon }} fa-2x"></i>
|
<i class="{{ 'timesheet'|icon }} fa-2x"></i>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_granted('ROLE_TEAMLEAD') %}
|
{% if is_granted('view_invoice') %}
|
||||||
<li class="visible-xs-inline-block">
|
<li class="visible-xs-inline-block">
|
||||||
<a href="{{ path('invoice') }}" class="ddt-large">
|
<a href="{{ path('invoice') }}" class="ddt-large">
|
||||||
<i class="{{ 'invoice'|icon }} fa-2x"></i>
|
<i class="{{ 'invoice'|icon }} fa-2x"></i>
|
||||||
|
|||||||
249
templates/export/index.html.twig
Normal file
249
templates/export/index.html.twig
Normal file
@@ -0,0 +1,249 @@
|
|||||||
|
{% extends 'base.html.twig' %}
|
||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||||
|
{% import "macros/datatables.html.twig" as tables %}
|
||||||
|
|
||||||
|
{% block page_title %}{{ 'export.title'|trans }}{% endblock %}
|
||||||
|
{% block page_subtitle %}{{ 'export.subtitle'|trans }}{% endblock %}
|
||||||
|
{% block page_actions %}
|
||||||
|
{% set actions = {
|
||||||
|
'filter': '#collapseExport',
|
||||||
|
'fas fa-toggle-off': {'id':'export-toggle-button'}
|
||||||
|
} %}
|
||||||
|
{{ widgets.page_actions(actions) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main_before %}
|
||||||
|
{{ toolbar.toolbar(form, 'collapseExport', true) }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main_after %}
|
||||||
|
<div class="clearfix"></div>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block main %}
|
||||||
|
|
||||||
|
{% if entries is empty %}
|
||||||
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
|
{% else %}
|
||||||
|
{#
|
||||||
|
{% set columns = {'date': ''} %}
|
||||||
|
|
||||||
|
{% if not duration_only %}
|
||||||
|
{% set columns = columns|merge({'starttime': 'hidden-xs', 'endtime': 'hidden-xs'}) %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% set columns = columns|merge({
|
||||||
|
'duration': '',
|
||||||
|
'rate': '',
|
||||||
|
'customer': 'hidden-xs hidden-sm',
|
||||||
|
'project': 'hidden-xs hidden-sm',
|
||||||
|
'activity': 'hidden-xs hidden-sm',
|
||||||
|
'username': 'hidden-xs',
|
||||||
|
'description': 'hidden-xs hidden-sm',
|
||||||
|
'actions': 'alwaysVisible',
|
||||||
|
}) %}
|
||||||
|
#}
|
||||||
|
{% set columns = {
|
||||||
|
'date': '',
|
||||||
|
'user': 'hidden-xs hidden-sm',
|
||||||
|
'customer': 'hidden-xs hidden-sm',
|
||||||
|
'project': 'hidden-xs hidden-sm',
|
||||||
|
'activity': 'hidden-xs hidden-sm',
|
||||||
|
'description': 'hidden-xs hidden-sm',
|
||||||
|
'unit_price': '',
|
||||||
|
'duration': '',
|
||||||
|
'total_rate': '',
|
||||||
|
'exported': 'alwaysVisible',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{% set tableName = 'export' %}
|
||||||
|
|
||||||
|
{{ tables.data_table_header(tableName, columns) }}
|
||||||
|
{% for entry in entries %}
|
||||||
|
{% set currency = entry.project.customer.currency %}
|
||||||
|
{% set duration = entry.duration|duration() %}
|
||||||
|
{% if entry.fixedRate is not null %}
|
||||||
|
{% set rate = entry.fixedRate %}
|
||||||
|
{% set duration = 1 %}
|
||||||
|
{% elseif entry.hourlyRate is not null %}
|
||||||
|
{% set rate = entry.hourlyRate %}
|
||||||
|
{% else %}
|
||||||
|
{% set rate = entry.user.preferenceValue('hourly_rate') %}
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
|
||||||
|
<td class="hidden-xs hidden-sm">{{ widgets.label_user(entry.user) }}</td>
|
||||||
|
<td class="hidden-xs hidden-sm">{{ widgets.label_customer(entry.project.customer) }}</td>
|
||||||
|
<td class="hidden-xs hidden-sm">{{ widgets.label_project(entry.project) }}</td>
|
||||||
|
<td class="hidden-xs hidden-sm">{{ widgets.label_activity(entry.activity) }}</td>
|
||||||
|
<td class="hidden-xs hidden-sm timesheet-description">
|
||||||
|
{{ entry.description }}
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{{ rate|money(currency) }}
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{{ duration }}
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{{ entry.rate|money(currency) }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if is_granted('edit_export', entry) %}
|
||||||
|
{% if entry.exported %}
|
||||||
|
<button type="button" class="btn btn-default exportBtn active" data-toggle="button" aria-pressed="true" autocomplete="off"
|
||||||
|
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
|
||||||
|
{{ 'entryState.exported'|trans }}
|
||||||
|
</button>
|
||||||
|
{% else %}
|
||||||
|
<button type="button" class="btn btn-default exportBtn" data-toggle="button" aria-pressed="false" autocomplete="off"
|
||||||
|
data-exported-text="{{ 'entryState.exported'|trans }}" data-clean-text="{{ 'entryState.not_exported'|trans }}" data-timesheet="{{ entry.id }}">
|
||||||
|
{{ 'entryState.not_exported'|trans }}
|
||||||
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% if entry.exported %}
|
||||||
|
{{ 'entryState.exported'|trans }}
|
||||||
|
{% else %}
|
||||||
|
{{ 'entryState.not_exported'|trans }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{{ tables.data_table_footer(entries) }}
|
||||||
|
|
||||||
|
{% if is_granted('create_export') %}
|
||||||
|
<div class="row no-print">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<div class="btn-group pull-right" id="export-buttons" role="group">
|
||||||
|
{% for button in renderer %}
|
||||||
|
<button type="button" id="export-{{ button.id }}-button" class="btn btn-success startExportBtn" data-type="{{ button.id }}">
|
||||||
|
<i class="{{ button.icon|icon }}"></i> {{ ('button.' ~ button.title)|trans }}
|
||||||
|
</button>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block javascripts %}
|
||||||
|
{{ parent() }}
|
||||||
|
<script type="text/javascript">
|
||||||
|
|
||||||
|
function confirmToggleState()
|
||||||
|
{
|
||||||
|
if ($('#export-toggle-button').hasClass('export-off')) {
|
||||||
|
if (confirm('{{ 'export.clear_all'|trans }}')) {
|
||||||
|
$('.exportBtn').each(function () {
|
||||||
|
if ($(this).hasClass('active')) {
|
||||||
|
$(this).click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (confirm('{{ 'export.mark_all'|trans }}')) {
|
||||||
|
$('.exportBtn').each(function () {
|
||||||
|
if (!$(this).hasClass('active')) {
|
||||||
|
$(this).click();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimesheetExportState(button, id, exported)
|
||||||
|
{
|
||||||
|
$.ajax({
|
||||||
|
url: '{{ path('patch_timesheet', {id: '-s-'}) }}'.replace('-s-', id),
|
||||||
|
headers: {
|
||||||
|
'X-AUTH-SESSION': true,
|
||||||
|
'Content-Type':'application/json'
|
||||||
|
},
|
||||||
|
method: 'PATCH',
|
||||||
|
dataType: 'json',
|
||||||
|
data: JSON.stringify({'exported': exported}),
|
||||||
|
success: function(data) {
|
||||||
|
if (exported) {
|
||||||
|
button.button('exported');
|
||||||
|
} else {
|
||||||
|
button.button('clean');
|
||||||
|
}
|
||||||
|
if ($('select#exported').val() !== '{{ constant('App\\Repository\\Query\\TimesheetQuery::STATE_ALL') }}') {
|
||||||
|
button.closest('tr').hide('ease', function() {
|
||||||
|
$(this).remove();
|
||||||
|
if($(this).closest('table').find('tbody tr:visible').length === 0) {
|
||||||
|
$('#export-buttons button').prop('disabled', true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, error: function(jqXHR, textStatus, errorThrown) {
|
||||||
|
var message = 'An error occured';
|
||||||
|
if (jqXHR.responseJSON !== undefined) {
|
||||||
|
console.log(jqXHR);
|
||||||
|
message = jqXHR.responseJSON.message;
|
||||||
|
if (jqXHR.responseJSON.errors !== undefined)
|
||||||
|
{
|
||||||
|
var errors = jqXHR.responseJSON.errors.errors;
|
||||||
|
for (var i = 0; i < errors.length; i++) {
|
||||||
|
message += ' / ' + errors[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
alert('{{ 'action.update.error'|trans({}, 'flashmessages') }}'.replace('%reason%', message));
|
||||||
|
button.button('reset');
|
||||||
|
if (exported) {
|
||||||
|
button.removeClass('active');
|
||||||
|
} else {
|
||||||
|
button.addClass('active');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(document).ready(function () {
|
||||||
|
$('body').on('click', '.exportBtn', function() {
|
||||||
|
var button = $(this);
|
||||||
|
var id = button.attr('data-timesheet');
|
||||||
|
|
||||||
|
if (button.hasClass('active')) {
|
||||||
|
updateTimesheetExportState(button, id, false);
|
||||||
|
} else {
|
||||||
|
updateTimesheetExportState(button, id, true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('#export-toggle-button').on('click', function () {
|
||||||
|
if (!confirmToggleState()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if ($(this).hasClass('export-off')) {
|
||||||
|
$(this).removeClass('export-off');
|
||||||
|
$(this).html('<i class="fas fa-toggle-off"></i>');
|
||||||
|
} else {
|
||||||
|
$(this).addClass('export-off');
|
||||||
|
$(this).html('<i class="fas fa-toggle-on"></i>');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
$('body').on('click', '#export-buttons .startExportBtn', function() {
|
||||||
|
$('#type').val($(this).attr('data-type'));
|
||||||
|
var $form = $("#export-form");
|
||||||
|
var prevAction = $form.attr('action');
|
||||||
|
$form.attr('target', '_blank').attr('action', '{{ path('export_data') }}');
|
||||||
|
$form.submit();
|
||||||
|
$('#type').val('');
|
||||||
|
$form.removeAttr('target').attr('action', prevAction);
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
{% endblock %}
|
||||||
17
templates/export/layout.html.twig
Normal file
17
templates/export/layout.html.twig
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="{{ app.request.locale }}">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||||
|
<meta content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no" name="viewport">
|
||||||
|
<link rel="stylesheet" href="{{ asset('build/app.css') }}">
|
||||||
|
<script src="{{ asset('build/app.js') }}"></script>
|
||||||
|
</head>
|
||||||
|
<body class="pad export_print">
|
||||||
|
<div class="wrapper">
|
||||||
|
<section class="export">
|
||||||
|
{% block export %}{% endblock %}
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
172
templates/export/renderer/default.html.twig
Normal file
172
templates/export/renderer/default.html.twig
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
|
{% extends 'export/layout.html.twig' %}
|
||||||
|
|
||||||
|
{% block export %}
|
||||||
|
<style>
|
||||||
|
.items tr.summary {
|
||||||
|
}
|
||||||
|
.items td.totals {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
.items td.duration,
|
||||||
|
.items th.duration,
|
||||||
|
.items th.cost,
|
||||||
|
.items td.cost {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<h2>{{ 'export.document_title'|trans }}</h2>
|
||||||
|
<p>
|
||||||
|
{{ 'export.period'|trans }}:
|
||||||
|
{{ query.begin|date_short }} - {{ query.end|date_short }}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<h3>{{ 'export.summary'|trans }}</h3>
|
||||||
|
<table class="items table table-condensed table-bordered dataTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>{{ 'label.customer'|trans }}</th>
|
||||||
|
<th>{{ 'label.project'|trans }}</th>
|
||||||
|
<th class="duration">{{ 'label.duration'|trans }}</th>
|
||||||
|
<th class="cost">{{ 'label.rate'|trans }}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% set customer = null %}
|
||||||
|
{% set customerDuration = 0 %}
|
||||||
|
{% set customerRate = 0 %}
|
||||||
|
{% set customerCurrency = null %}
|
||||||
|
{% for summary in summaries %}
|
||||||
|
{% if customer is same as(null) %}
|
||||||
|
{% set customer = summary.customer %}
|
||||||
|
{% set customerCurrency = summary.currency %}
|
||||||
|
{% endif %}
|
||||||
|
{% if customer is not same as(summary.customer) %}
|
||||||
|
<tr class="summary">
|
||||||
|
<td colspan="2"></td>
|
||||||
|
<td class="totals duration">{{ customerDuration|duration }}</td>
|
||||||
|
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% set customerCurrency = summary.currency %}
|
||||||
|
{% set customer = summary.customer %}
|
||||||
|
{% set customerDuration = 0 %}
|
||||||
|
{% set customerRate = 0 %}
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ summary.customer }}</td>
|
||||||
|
<td>{{ summary.project }}</td>
|
||||||
|
<td class="duration">{{ summary.duration|duration }}</td>
|
||||||
|
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% set customerDuration = customerDuration + summary.duration %}
|
||||||
|
{% set customerRate = customerRate + summary.rate %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if customer is not same as(null) %}
|
||||||
|
<tr class="summary">
|
||||||
|
<td colspan="2"></td>
|
||||||
|
<td class="totals duration">{{ customerDuration|duration }}</td>
|
||||||
|
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% set columns = [
|
||||||
|
'label.date',
|
||||||
|
'label.begin',
|
||||||
|
'label.end',
|
||||||
|
'label.username',
|
||||||
|
'label.customer',
|
||||||
|
'label.project',
|
||||||
|
'label.activity',
|
||||||
|
'label.description',
|
||||||
|
'label.exported',
|
||||||
|
'label.hourly_rate',
|
||||||
|
'label.fixed_rate',
|
||||||
|
'label.duration',
|
||||||
|
'label.rate',
|
||||||
|
] %}
|
||||||
|
|
||||||
|
<div class="row" id="export-records">
|
||||||
|
<div class="col-xs-12">
|
||||||
|
<h3>{{ 'export.full_list'|trans }}</h3>
|
||||||
|
<table class="table table-condensed table-bordered dataTable">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
{% for columnTitle in columns %}
|
||||||
|
<th>{{ columnTitle|trans }}</th>
|
||||||
|
{% endfor %}
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% set timeWorked = 0 %}
|
||||||
|
{% set rateTotal = 0 %}
|
||||||
|
{% set currency = false %}
|
||||||
|
{% for entry in entries %}
|
||||||
|
{% set timeWorked = timeWorked + entry.duration %}
|
||||||
|
{% set rateTotal = rateTotal + entry.rate %}
|
||||||
|
{% if currency is same as(false) %}
|
||||||
|
{% set currency = entry.project.customer.currency %}
|
||||||
|
{% endif %}
|
||||||
|
{% if currency is not same as(entry.project.customer.currency) %}
|
||||||
|
{% set currency = null %}
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td class="text-nowrap">{{ entry.begin|date_short }}</td>
|
||||||
|
<td class="text-nowrap">{{ entry.begin|date("H:i") }}</td>
|
||||||
|
<td class="text-nowrap">{{ entry.end|date("H:i") }}</td>
|
||||||
|
<td>{{ widgets.username(entry.user) }}</td>
|
||||||
|
<td>{{ entry.project.customer.name }}</td>
|
||||||
|
<td>{{ entry.project.name }}</td>
|
||||||
|
<td>{{ entry.activity.name }}</td>
|
||||||
|
<td>
|
||||||
|
{% if entry.description is not empty %}
|
||||||
|
{{ entry.description|desc2html }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{% if entry.exported %}
|
||||||
|
{{ 'entryState.exported'|trans }}
|
||||||
|
{% else %}
|
||||||
|
{{ 'entryState.not_exported'|trans }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-nowrap">{{ entry.hourlyRate|money(entry.project.customer.currency) }}</td>
|
||||||
|
<td class="text-nowrap">{{ entry.fixedRate|money(entry.project.customer.currency) }}</td>
|
||||||
|
<td class="text-nowrap">{{ entry.duration|duration }}</td>
|
||||||
|
<td class="text-nowrap">
|
||||||
|
{{ entry.rate|money(entry.project.customer.currency) }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
<tfoot>
|
||||||
|
<tr>
|
||||||
|
<th colspan="{{ ( columns|length ) - 2 }}" class="text-right"></th>
|
||||||
|
<th class="text-nowrap">
|
||||||
|
{{ timeWorked|duration }}
|
||||||
|
</th>
|
||||||
|
<th class="text-nowrap">
|
||||||
|
{% if currency is not null and currency is not same as(false) %}
|
||||||
|
{{ rateTotal|money(currency) }}
|
||||||
|
{% else %}
|
||||||
|
{{ rateTotal|money }}
|
||||||
|
{% endif %}
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% endblock %}
|
||||||
169
templates/export/renderer/pdf.html.twig
Normal file
169
templates/export/renderer/pdf.html.twig
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<style>
|
||||||
|
body {font-family: sans-serif;
|
||||||
|
font-size: 10pt;
|
||||||
|
}
|
||||||
|
p { margin: 0pt; }
|
||||||
|
table.items {
|
||||||
|
border: 0.1mm solid #000000;
|
||||||
|
}
|
||||||
|
td { vertical-align: top; }
|
||||||
|
.items td {
|
||||||
|
border-left: 0.1mm solid #000000;
|
||||||
|
border-right: 0.1mm solid #000000;
|
||||||
|
}
|
||||||
|
.items tr.even {
|
||||||
|
background-color: #e0ebff;
|
||||||
|
}
|
||||||
|
.items tr.summary {
|
||||||
|
background-color: #efefef;
|
||||||
|
}
|
||||||
|
.items tr.summary td {
|
||||||
|
font-weight: bold;
|
||||||
|
border-top: 0.1mm solid #000000;
|
||||||
|
border-bottom: 0.1mm solid #000000;
|
||||||
|
}
|
||||||
|
table thead td {
|
||||||
|
background-color: #ececec;
|
||||||
|
text-align: center;
|
||||||
|
border: 0.1mm solid #000000;
|
||||||
|
font-variant: small-caps;
|
||||||
|
}
|
||||||
|
.items td.totals {
|
||||||
|
font-weight: bold;
|
||||||
|
text-align: right;
|
||||||
|
border: 0.1mm solid #000000;
|
||||||
|
}
|
||||||
|
.items td.duration,
|
||||||
|
.items td.cost {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<!--mpdf
|
||||||
|
<htmlpagefooter name="myfooter">
|
||||||
|
<table style="border-top: 1px solid #000000; font-size: 9pt; padding-top: 3mm; width: 100%">
|
||||||
|
<tr>
|
||||||
|
<td align="left">
|
||||||
|
{{ 'export.page_of'|trans({'%page%': '{PAGENO}', '%pages%': '{nb}'}) }}
|
||||||
|
</td>
|
||||||
|
<td align="right">
|
||||||
|
{{ 'export.date_copyright'|trans({'%date%': now|date_short ~ ' ' ~ now|date('H:i'), '%kimai%': '<a href="' ~ constant('App\\Constants::HOMEPAGE') ~ '">' ~ constant('App\\Constants::SOFTWARE') ~ '</a>'})|raw }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</table>
|
||||||
|
</htmlpagefooter>
|
||||||
|
<sethtmlpagefooter name="myfooter" value="on" />
|
||||||
|
mpdf-->
|
||||||
|
<h2 style="margin-bottom: 0; padding-bottom: 0">{{ 'export.document_title'|trans }}</h2>
|
||||||
|
<p>
|
||||||
|
{{ 'export.period'|trans }}:
|
||||||
|
{{ query.begin|date_short }} - {{ query.end|date_short }}
|
||||||
|
</p>
|
||||||
|
<h3>{{ 'export.summary'|trans }}</h3>
|
||||||
|
<table class="items" width="100%" style="font-size: 9pt; border-collapse: collapse; " cellpadding="8">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>{{ 'label.customer'|trans }}</td>
|
||||||
|
<td>{{ 'label.project'|trans }}</td>
|
||||||
|
<td>{{ 'label.duration'|trans }}</td>
|
||||||
|
<td>{{ 'label.rate'|trans }}</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% set customer = null %}
|
||||||
|
{% set customerDuration = 0 %}
|
||||||
|
{% set customerRate = 0 %}
|
||||||
|
{% set customerCurrency = null %}
|
||||||
|
{% set customerCount = 0 %}
|
||||||
|
{% for summary in summaries %}
|
||||||
|
{% if customer is same as(null) %}
|
||||||
|
{% set customer = summary.customer %}
|
||||||
|
{% set customerCurrency = summary.currency %}
|
||||||
|
{% endif %}
|
||||||
|
{% if customer is not same as(summary.customer) %}
|
||||||
|
<tr class="summary">
|
||||||
|
<td colspan="2"></td>
|
||||||
|
<td class="totals duration">{{ customerDuration|duration }}</td>
|
||||||
|
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% set customerCurrency = summary.currency %}
|
||||||
|
{% set customer = summary.customer %}
|
||||||
|
{% set customerDuration = 0 %}
|
||||||
|
{% set customerRate = 0 %}
|
||||||
|
{% set customerCount = 0 %}
|
||||||
|
{% endif %}
|
||||||
|
<tr class="{{ cycle(['odd', 'even'], customerCount) }}">
|
||||||
|
<td>{{ summary.customer }}</td>
|
||||||
|
<td>{{ summary.project }}</td>
|
||||||
|
<td class="duration">{{ summary.duration|duration }}</td>
|
||||||
|
<td class="cost">{{ summary.rate|money(summary.currency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% set customerDuration = customerDuration + summary.duration %}
|
||||||
|
{% set customerRate = customerRate + summary.rate %}
|
||||||
|
{% set customerCount = customerCount + 1 %}
|
||||||
|
{% endfor %}
|
||||||
|
{% if customer is not same as(null) %}
|
||||||
|
<tr class="summary">
|
||||||
|
<td colspan="2"></td>
|
||||||
|
<td class="totals duration">{{ customerDuration|duration }}</td>
|
||||||
|
<td class="totals cost">{{ customerRate|money(customerCurrency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endif %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<pagebreak>
|
||||||
|
|
||||||
|
<h3>{{ 'export.full_list'|trans }}</h3>
|
||||||
|
|
||||||
|
{% set duration = 0 %}
|
||||||
|
{% set rate = 0 %}
|
||||||
|
{% set currency = false %}
|
||||||
|
<table class="items" width="100%" style="font-size: 9pt; border-collapse: collapse; " cellpadding="8">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<td>{{ 'label.date'|trans }}</td>
|
||||||
|
<td width="">{{ 'label.description'|trans }}</td>
|
||||||
|
<td>{{ 'label.duration'|trans }}</td>
|
||||||
|
<td>{{ 'label.rate'|trans }}</td>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for entry in entries %}
|
||||||
|
{% set duration = duration + entry.duration %}
|
||||||
|
{% set rate = rate + entry.rate %}
|
||||||
|
{% if currency is same as(false) %}
|
||||||
|
{% set currency = entry.project.customer.currency %}
|
||||||
|
{% endif %}
|
||||||
|
{% if currency is not same as(entry.project.customer.currency) %}
|
||||||
|
{% set currency = null %}
|
||||||
|
{% endif %}
|
||||||
|
<tr class="{{ cycle(['odd', 'even'], loop.index0) }}">
|
||||||
|
<td>
|
||||||
|
{{ entry.begin|date_time }}
|
||||||
|
<br>
|
||||||
|
{{ entry.end|date_time }}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
{{ entry.project.customer.name }} - {{ entry.project.name }} - {{ entry.activity.name }}
|
||||||
|
{% if entry.description is not empty %}
|
||||||
|
<br>
|
||||||
|
<i>{{ entry.description|desc2html }}</i>
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="duration">{{ entry.duration|duration }}</td>
|
||||||
|
<td class="cost">{{ entry.rate|money(entry.project.customer.currency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
<tr class="summary">
|
||||||
|
<td colspan="2"></td>
|
||||||
|
<td class="totals duration">{{ duration|duration }}</td>
|
||||||
|
<td class="totals cost">{{ rate|money(currency) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
{% extends 'base.html.twig' %}
|
{% extends 'base.html.twig' %}
|
||||||
{% import "macros/widgets.html.twig" as widgets %}
|
{% import "macros/widgets.html.twig" as widgets %}
|
||||||
{% import "macros/toolbar.html.twig" as toolbar %}
|
{% import "macros/toolbar.html.twig" as toolbar %}
|
||||||
|
{% import "macros/datatables.html.twig" as tables %}
|
||||||
|
|
||||||
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
|
{% block page_title %}{{ 'invoice.title'|trans }}{% endblock %}
|
||||||
{% block page_subtitle %}{{ 'invoice.subtitle'|trans }}{% endblock %}
|
{% block page_subtitle %}{{ 'invoice.subtitle'|trans }}{% endblock %}
|
||||||
@@ -15,8 +16,6 @@
|
|||||||
{{ widgets.page_actions(actions) }}
|
{{ widgets.page_actions(actions) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block page_content_class %}{{ parent() }} invoice{% endblock %}
|
|
||||||
|
|
||||||
{% block main_before %}
|
{% block main_before %}
|
||||||
{{ toolbar.toolbar(form, 'collapseInvoice', true) }}
|
{{ toolbar.toolbar(form, 'collapseInvoice', true) }}
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
@@ -27,10 +26,54 @@
|
|||||||
|
|
||||||
{% block main %}
|
{% block main %}
|
||||||
|
|
||||||
{% if model.entries is empty %}
|
{% set entries = {} %}
|
||||||
{{ widgets.callout('warning', 'invoice.select_filter') }}
|
{% if model.calculator is not empty and model.calculator.entries is not empty %}
|
||||||
|
{% set entries = model.calculator.entries %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
{% if entries is empty %}
|
||||||
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
{% else %}
|
{% else %}
|
||||||
{{ include('invoice/preview.html.twig') }}
|
{{ widgets.callout('success', 'invoice.preview') }}
|
||||||
|
|
||||||
|
{% set columns = {
|
||||||
|
'date': '',
|
||||||
|
'user': 'hidden-xs hidden-sm',
|
||||||
|
'activity': 'hidden-xs hidden-sm',
|
||||||
|
'unit_price': 'text-center',
|
||||||
|
'amount': 'text-center',
|
||||||
|
'total_rate': 'text-right',
|
||||||
|
} %}
|
||||||
|
|
||||||
|
{% set tableName = 'invoice' %}
|
||||||
|
|
||||||
|
{{ tables.data_table_header(tableName, columns) }}
|
||||||
|
{% for entry in entries %}
|
||||||
|
{% set duration = entry.duration|duration() %}
|
||||||
|
{% if entry.fixedRate is not null %}
|
||||||
|
{% set rate = entry.fixedRate %}
|
||||||
|
{% set duration = 1 %}
|
||||||
|
{% elseif entry.hourlyRate is not null %}
|
||||||
|
{% set rate = entry.hourlyRate %}
|
||||||
|
{% else %}
|
||||||
|
{% set rate = entry.user.preferenceValue('hourly_rate') %}
|
||||||
|
{% endif %}
|
||||||
|
<tr>
|
||||||
|
<td>{{ entry.begin|date_short }}</td>
|
||||||
|
<td class="hidden-xs hidden-sm">{{ widgets.username(entry.user) }}</td>
|
||||||
|
<td class="hidden-xs hidden-sm timesheet-description">
|
||||||
|
{% if entry.description is not empty %}
|
||||||
|
{{ entry.description|desc2html }}
|
||||||
|
{% else %}
|
||||||
|
{{ entry.activity.name }} / {{ entry.project.name }}
|
||||||
|
{% endif %}
|
||||||
|
</td>
|
||||||
|
<td class="text-center">{{ rate|money(model.calculator.currency) }}</td>
|
||||||
|
<td class="text-center">{{ duration }}</td>
|
||||||
|
<td class="text-right">{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
{{ tables.data_table_footer(entries) }}
|
||||||
|
|
||||||
{% if is_granted('create_invoice') %}
|
{% if is_granted('create_invoice') %}
|
||||||
<div class="row no-print">
|
<div class="row no-print">
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
{% import "macros/widgets.html.twig" as widgets %}
|
|
||||||
{{ widgets.callout('success', 'invoice.preview') }}
|
|
||||||
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-md-12">
|
|
||||||
<div class="table-responsive">
|
|
||||||
<table class="table table-striped">
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>{{ 'label.date'|trans }}</th>
|
|
||||||
<th class="hidden-xs hidden-sm">{{ 'label.user'|trans }}</th>
|
|
||||||
<th class="hidden-xs hidden-sm">{{ 'label.activity'|trans }}</th>
|
|
||||||
<th class="text-center">{{ 'invoice.unit_price'|trans }}</th>
|
|
||||||
<th class="text-center">{{ 'invoice.amount'|trans }}</th>
|
|
||||||
<th class="text-right">{{ 'invoice.total_rate'|trans }}</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{% for entry in model.calculator.entries %}
|
|
||||||
{% set duration = entry.duration|duration() %}
|
|
||||||
{% if entry.fixedRate is not null %}
|
|
||||||
{% set rate = entry.fixedRate %}
|
|
||||||
{% set duration = 1 %}
|
|
||||||
{% elseif entry.hourlyRate is not null %}
|
|
||||||
{% set rate = entry.hourlyRate %}
|
|
||||||
{% else %}
|
|
||||||
{% set rate = app.user.getPreferenceValue('hourly_rate') %}
|
|
||||||
{% endif %}
|
|
||||||
<tr>
|
|
||||||
<td>{{ entry.begin|date_short }}</td>
|
|
||||||
<td class="hidden-xs hidden-sm">{{ widgets.username(entry.user) }}</td>
|
|
||||||
<td class="hidden-xs hidden-sm timesheet-description">
|
|
||||||
{% if entry.description is not empty %}
|
|
||||||
{{ entry.description|desc2html }}
|
|
||||||
{% else %}
|
|
||||||
{{ entry.activity.name }} / {{ entry.project.name }}
|
|
||||||
{% endif %}
|
|
||||||
</td>
|
|
||||||
<td class="text-center" contenteditable="true">
|
|
||||||
{{ rate|money(model.calculator.currency) }}
|
|
||||||
</td>
|
|
||||||
<td class="text-center">
|
|
||||||
{{ duration }}
|
|
||||||
</td>
|
|
||||||
<td class="text-right">{{ entry.rate|money(model.calculator.currency) }}</td>
|
|
||||||
</tr>
|
|
||||||
{% endfor %}
|
|
||||||
</tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
@@ -57,9 +57,9 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<th>{{ 'label.date'|trans }}</th>
|
<th>{{ 'label.date'|trans }}</th>
|
||||||
<th>{{ 'label.activity'|trans }}</th>
|
<th>{{ 'label.activity'|trans }}</th>
|
||||||
<th>{{ 'invoice.unit_price'|trans }}</th>
|
<th>{{ 'label.unit_price'|trans }}</th>
|
||||||
<th>{{ 'label.hours'|trans }}</th>
|
<th>{{ 'label.hours'|trans }}</th>
|
||||||
<th>{{ 'invoice.total_rate'|trans }}</th>
|
<th>{{ 'label.total_rate'|trans }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -70,9 +70,9 @@
|
|||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>{{ 'label.description'|trans }}</th>
|
<th>{{ 'label.description'|trans }}</th>
|
||||||
<th>{{ 'invoice.unit_price'|trans }}</th>
|
<th>{{ 'label.unit_price'|trans }}</th>
|
||||||
<th>{{ 'invoice.amount'|trans }}</th>
|
<th>{{ 'label.amount'|trans }}</th>
|
||||||
<th>{{ 'invoice.total_rate'|trans }}</th>
|
<th>{{ 'label.total_rate'|trans }}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@@ -89,11 +89,9 @@
|
|||||||
<table class="table table-striped table-hover dataTable" role="grid">
|
<table class="table table-striped table-hover dataTable" role="grid">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
{% for title, class in entries %}
|
{%- for title, class in entries -%}
|
||||||
<th class="{{ macro.data_table_column_class(name, entries, title) }}">
|
<th class="{{ macro.data_table_column_class(name, entries, title) }}">{{ ('label.' ~ title)|trans }}</th>
|
||||||
{{ ('label.' ~ title)|trans }}
|
{%- endfor -%}
|
||||||
</th>
|
|
||||||
{% endfor %}
|
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -104,23 +102,12 @@
|
|||||||
</table>
|
</table>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{#<div class="row">
|
|
||||||
<div class="col-sm-5">
|
|
||||||
{#<div class="dataTables_info" id="example2_info" role="status" aria-live="polite">
|
|
||||||
{{ 'datatables.entry_counter'|trans({'%from%': '1', '%to%': '10', '%total%': entries.count}) }}
|
|
||||||
</div># }
|
|
||||||
</div>
|
|
||||||
<div class="col-sm-7">
|
|
||||||
<div class="dataTables_paginate paging_simple_numbers">
|
|
||||||
{{ pagerfanta(entries, 'twitter_bootstrap3_translated', { routeName: route }) }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>#}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{% if route is not empty %}
|
||||||
<div class="navigation text-center no-print">
|
<div class="navigation text-center no-print">
|
||||||
{{ pagerfanta(entries, 'twitter_bootstrap3_translated', { proximity: 1, routeName: route }) }}
|
{{ pagerfanta(entries, 'twitter_bootstrap3_translated', { proximity: 1, routeName: route }) }}
|
||||||
</div>
|
</div>
|
||||||
|
{% endif %}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|||||||
@@ -2,26 +2,58 @@
|
|||||||
<i class="{{ icon|icon(icon) }}"></i>
|
<i class="{{ icon|icon(icon) }}"></i>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro page_actions(tools) %}
|
{%- macro page_actions(tools) -%}
|
||||||
<div class="breadcrumb">
|
<div class="breadcrumb">
|
||||||
<div class="box-tools">
|
<div class="box-tools">
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
{%- for icon,url in tools %}
|
{%- for icon,values in tools %}
|
||||||
<a class="btn btn-default" href="{{ url }}"
|
{% spaceless %}
|
||||||
{% if '#collapse' in url %}
|
{% set id = null %}
|
||||||
data-toggle="collapse"
|
{% set onclick = null %}
|
||||||
{% elseif '#modal' in url %}
|
{% set modal = null %}
|
||||||
data-toggle="modal" data-target="{{ url }}"
|
{% set toggle = null %}
|
||||||
{% endif %}
|
{% set url = null %}
|
||||||
|
|
||||||
|
{% if not values is iterable %}
|
||||||
|
{% set url = values %}
|
||||||
{% if 'onclick:' in url %}
|
{% if 'onclick:' in url %}
|
||||||
onclick="{{ url|replace({'onclick:': ''}) }}"
|
{% set onclick = url|replace({'onclick:': ''}) %}
|
||||||
|
{% set url = '#' %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
{% if '#collapse' in url %}
|
||||||
|
{% set toggle = 'collapse' %}
|
||||||
|
{% endif %}
|
||||||
|
{% if '#modal' in url %}
|
||||||
|
{% set modal = url %}
|
||||||
|
{% set url = '#' %}
|
||||||
|
{% endif %}
|
||||||
|
{% else %}
|
||||||
|
{% set url = values.url ?? '#' %}
|
||||||
|
{% set onclick = values.onclick ?? null %}
|
||||||
|
{% set modal = values.modal ?? null %}
|
||||||
|
{% set toggle = values.toggle ?? null %}
|
||||||
|
{% set id = values.id ?? null %}
|
||||||
|
{% endif %}
|
||||||
|
{% endspaceless %}
|
||||||
|
<a class="btn btn-default" href="{{ url }}"
|
||||||
|
{%- if id is not empty -%}
|
||||||
|
id="{{ id }}"
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if toggle is not empty -%}
|
||||||
|
data-toggle="{{ toggle }}"
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if modal is not empty -%}
|
||||||
|
data-toggle="modal" data-target="{{ modal }}"
|
||||||
|
{%- endif -%}
|
||||||
|
{%- if onclick is not empty -%}
|
||||||
|
onclick="{{ onclick }}"
|
||||||
|
{%- endif -%}
|
||||||
><i class="{{ icon|icon(icon) }}"></i></a>
|
><i class="{{ icon|icon(icon) }}"></i></a>
|
||||||
{% endfor -%}
|
{% endfor -%}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{%- endmacro -%}
|
||||||
|
|
||||||
{% macro page_header(title) %}
|
{% macro page_header(title) %}
|
||||||
<h2 class="page-header">{{ title|trans }}</h2>
|
<h2 class="page-header">{{ title|trans }}</h2>
|
||||||
|
|||||||
@@ -18,7 +18,12 @@
|
|||||||
<i class="{{ 'stop-small'|icon }} fa-2x"></i>
|
<i class="{{ 'stop-small'|icon }} fa-2x"></i>
|
||||||
</div>
|
</div>
|
||||||
<h4>
|
<h4>
|
||||||
|
{# TODO find a CSS solution for this #}
|
||||||
|
{% if entry.activity.name|length > 23 %}
|
||||||
|
{{ entry.activity.name|slice(0,20) }}...
|
||||||
|
{% else %}
|
||||||
{{ entry.activity.name }}
|
{{ entry.activity.name }}
|
||||||
|
{% endif %}
|
||||||
<small><i class="{{ 'timesheet'|icon }}"></i> {{ entry|duration }}</small>
|
<small><i class="{{ 'timesheet'|icon }}"></i> {{ entry|duration }}</small>
|
||||||
</h4>
|
</h4>
|
||||||
<p>{{ entry.project.name }} ({{ entry.project.customer.name }})</p>
|
<p>{{ entry.project.name }} ({{ entry.project.customer.name }})</p>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
{% if is_granted('ROLE_SUPER_ADMIN') %}
|
{% if is_granted('system_information') %}
|
||||||
<li>
|
<li>
|
||||||
<a href="{{ path('about') }}">
|
<a href="{{ path('about') }}">
|
||||||
<i class="menu-icon far fa-copyright bg-aqua"></i>
|
<i class="menu-icon far fa-copyright bg-aqua"></i>
|
||||||
|
|||||||
@@ -26,8 +26,7 @@
|
|||||||
|
|
||||||
{% if entries.count == 0 %}
|
{% if entries.count == 0 %}
|
||||||
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
{{ widgets.callout('warning', 'error.no_entries_found') }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
|
|
||||||
{% set columns = {'date': ''} %}
|
{% set columns = {'date': ''} %}
|
||||||
|
|
||||||
{% if not duration_only %}
|
{% if not duration_only %}
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
|
|
||||||
{% for entry in entries %}
|
{% for entry in entries %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
|
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'date') }}">{{ entry.begin|date_short }}</td>
|
||||||
|
|
||||||
{% if not duration_only %}
|
{% if not duration_only %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'starttime') }}">{{ entry.begin|date("H:i") }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'starttime') }}">{{ entry.begin|date("H:i") }}</td>
|
||||||
@@ -64,15 +63,15 @@
|
|||||||
{% if not duration_only %}
|
{% if not duration_only %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">{{ entry.end|date("H:i") }}</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">{{ entry.end|date("H:i") }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}">{{ entry.duration|duration }}</td>
|
||||||
{% if is_granted('view_rate', entry) %}
|
{% if is_granted('view_rate', entry) %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.project.customer.currency) }}</td>
|
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'rate') }}">{{ entry.rate|money(entry.project.customer.currency) }}</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{% if not duration_only %}
|
{% if not duration_only %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">‐</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'endtime') }}">‐</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'duration') }}"><i>{{ entry|duration }}</i></td>
|
<td class="text-nowrap {{ tables.data_table_column_class(tableName, columns, 'duration') }}"><i>{{ entry|duration }}</i></td>
|
||||||
{% if is_granted('view_rate', entry) %}
|
{% if is_granted('view_rate', entry) %}
|
||||||
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">‐</td>
|
<td class="{{ tables.data_table_column_class(tableName, columns, 'rate') }}">‐</td>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -107,6 +106,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|
||||||
{{ tables.data_table_footer(entries, 'timesheet_paginated') }}
|
{{ tables.data_table_footer(entries, 'timesheet_paginated') }}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
|
|||||||
@@ -54,6 +54,19 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
|||||||
$this->assertDefaultStructure($result[0], false);
|
$this->assertDefaultStructure($result[0], false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGetCollectionWithQuery()
|
||||||
|
{
|
||||||
|
$query = ['customer' => 1, 'project' => 1, 'page' => 2, 'size' => 5, 'order' => 'DESC', 'orderBy' => 'rate'];
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
|
$this->assertAccessIsGranted($client, '/api/timesheets', 'GET', $query);
|
||||||
|
$result = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
|
||||||
|
$this->assertInternalType('array', $result);
|
||||||
|
$this->assertNotEmpty($result);
|
||||||
|
$this->assertEquals(5, count($result));
|
||||||
|
$this->assertDefaultStructure($result[0], false);
|
||||||
|
}
|
||||||
|
|
||||||
public function testGetEntity()
|
public function testGetEntity()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
@@ -166,6 +179,45 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
|||||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/20');
|
$this->assertEntityNotFound(User::ROLE_USER, '/api/timesheets/20');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testPatchAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
|
$data = [
|
||||||
|
'activity' => 1,
|
||||||
|
'project' => 1,
|
||||||
|
'begin' => (new \DateTime('- 7 hours'))->format('Y-m-d H:m'),
|
||||||
|
'end' => (new \DateTime())->format('Y-m-d H:m'),
|
||||||
|
'description' => 'foo',
|
||||||
|
'exported' => true,
|
||||||
|
];
|
||||||
|
$this->request($client, '/api/timesheets/1', 'PATCH', [], json_encode($data));
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$result = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertInternalType('array', $result);
|
||||||
|
$this->assertDefaultStructure($result);
|
||||||
|
$this->assertNotEmpty($result['id']);
|
||||||
|
$this->assertEquals(25200, $result['duration']);
|
||||||
|
$this->assertEquals(1, $result['exported']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testInvalidPatchAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
|
$data = [
|
||||||
|
'activity' => 10,
|
||||||
|
'project' => 1,
|
||||||
|
'begin' => (new \DateTime())->format('Y-m-d H:m'),
|
||||||
|
'end' => (new \DateTime('- 7 hours'))->format('Y-m-d H:m'),
|
||||||
|
'description' => 'foo',
|
||||||
|
];
|
||||||
|
$this->request($client, '/api/timesheets/1', 'PATCH', [], json_encode($data));
|
||||||
|
|
||||||
|
$response = $client->getResponse();
|
||||||
|
$this->assertEquals(400, $response->getStatusCode());
|
||||||
|
$this->assertApiCallValidationError($response, ['end', 'activity']);
|
||||||
|
}
|
||||||
|
|
||||||
protected function assertDefaultStructure(array $result, $full = true)
|
protected function assertDefaultStructure(array $result, $full = true)
|
||||||
{
|
{
|
||||||
$expectedKeys = [
|
$expectedKeys = [
|
||||||
@@ -174,7 +226,7 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
|||||||
|
|
||||||
if ($full) {
|
if ($full) {
|
||||||
$expectedKeys = array_merge($expectedKeys, [
|
$expectedKeys = array_merge($expectedKeys, [
|
||||||
'description', 'fixed_rate', 'hourly_rate'
|
'exported', 'description', 'fixed_rate', 'hourly_rate'
|
||||||
]);
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -123,8 +123,8 @@ class ActivityControllerTest extends ControllerBaseTest
|
|||||||
$this->request($client, '/admin/activity/1/delete');
|
$this->request($client, '/admin/activity/1/delete');
|
||||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||||
$client->followRedirect();
|
$client->followRedirect();
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasFlashDeleteSuccess($client);
|
||||||
$this->assertHasFlashSuccess($client);
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
|
|
||||||
$this->request($client, '/admin/activity/1/edit');
|
$this->request($client, '/admin/activity/1/edit');
|
||||||
$this->assertFalse($client->getResponse()->isSuccessful());
|
$this->assertFalse($client->getResponse()->isSuccessful());
|
||||||
@@ -157,8 +157,8 @@ class ActivityControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
|
||||||
$client->followRedirect();
|
$client->followRedirect();
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasFlashDeleteSuccess($client);
|
||||||
$this->assertHasFlashSuccess($client);
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
|
|
||||||
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
||||||
// $em->clear();
|
// $em->clear();
|
||||||
|
|||||||
@@ -130,8 +130,8 @@ class CustomerControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/'));
|
$this->assertIsRedirect($client, $this->createUrl('/admin/customer/'));
|
||||||
$client->followRedirect();
|
$client->followRedirect();
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasFlashDeleteSuccess($client);
|
||||||
$this->assertHasFlashSuccess($client);
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
|
|
||||||
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
||||||
// $em->clear();
|
// $em->clear();
|
||||||
|
|||||||
@@ -153,8 +153,8 @@ class ProjectControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$this->assertIsRedirect($client, $this->createUrl('/admin/project/'));
|
$this->assertIsRedirect($client, $this->createUrl('/admin/project/'));
|
||||||
$client->followRedirect();
|
$client->followRedirect();
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasFlashDeleteSuccess($client);
|
||||||
$this->assertHasFlashSuccess($client);
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
|
|
||||||
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
// SQLIte does not necessarly support onCascade delete, so these timesheet will stay after deletion
|
||||||
// $em->clear();
|
// $em->clear();
|
||||||
|
|||||||
@@ -31,7 +31,10 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
$this->assertAccessIsGranted($client, '/team/timesheet/');
|
$this->assertAccessIsGranted($client, '/team/timesheet/');
|
||||||
$this->assertHasDataTable($client);
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
// there are no records by default in the test database
|
||||||
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
|
|
||||||
$result = $client->getCrawler()->filter('div.breadcrumb div.box-tools div.btn-group a.btn');
|
$result = $client->getCrawler()->filter('div.breadcrumb div.box-tools div.btn-group a.btn');
|
||||||
$this->assertEquals(4, count($result));
|
$this->assertEquals(4, count($result));
|
||||||
@@ -47,9 +50,10 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
|
|
||||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
$user = $this->getUserByRole($em, User::ROLE_USER);
|
||||||
$fixture = new TimesheetFixtures();
|
$fixture = new TimesheetFixtures();
|
||||||
$fixture->setAmount(10);
|
$fixture->setAmount(10);
|
||||||
$fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
|
$fixture->setUser($user);
|
||||||
$fixture->setStartDate(new \DateTime('-10 days'));
|
$fixture->setStartDate(new \DateTime('-10 days'));
|
||||||
$this->importFixture($em, $fixture);
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
@@ -61,7 +65,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$form = $client->getCrawler()->filter('form.navbar-form')->form();
|
$form = $client->getCrawler()->filter('form.navbar-form')->form();
|
||||||
$client->submit($form, [
|
$client->submit($form, [
|
||||||
'state' => 1,
|
'state' => 1,
|
||||||
'user' => 1,
|
'user' => $user->getId(),
|
||||||
'pageSize' => 25,
|
'pageSize' => 25,
|
||||||
'daterange' => $dateRange,
|
'daterange' => $dateRange,
|
||||||
'customer' => null,
|
'customer' => null,
|
||||||
@@ -69,8 +73,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasDataTable($client);
|
||||||
|
$this->assertDataTableRowCount($client, 'datatable_timesheet_admin', 10);
|
||||||
// TODO more assertions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testExportAction()
|
public function testExportAction()
|
||||||
|
|||||||
@@ -178,6 +178,17 @@ abstract class ControllerBaseTest extends WebTestCase
|
|||||||
$this->assertContains('<table class="table table-striped table-hover dataTable" role="grid">', $client->getResponse()->getContent());
|
$this->assertContains('<table class="table table-striped table-hover dataTable" role="grid">', $client->getResponse()->getContent());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Client $client
|
||||||
|
* @param string $id
|
||||||
|
* @param int $count
|
||||||
|
*/
|
||||||
|
protected function assertDataTableRowCount(Client $client, string $id, int $count)
|
||||||
|
{
|
||||||
|
$node = $client->getCrawler()->filter('section.content div#' . $id . ' table.table-striped tbody tr');
|
||||||
|
$this->assertEquals($count, $node->count());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param string $role the USER role to use for the request
|
* @param string $role the USER role to use for the request
|
||||||
* @param string $url the URL of the page displaying the initial form to submit
|
* @param string $url the URL of the page displaying the initial form to submit
|
||||||
@@ -221,13 +232,32 @@ abstract class ControllerBaseTest extends WebTestCase
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function assertHasNoEntriesWithFilter(Client $client)
|
||||||
|
{
|
||||||
|
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
||||||
|
$this->assertContains('No entries were found based on your selected filters.', $node->text());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param Client $client
|
* @param Client $client
|
||||||
|
* @param string|null $message
|
||||||
*/
|
*/
|
||||||
protected function assertHasFlashSuccess(Client $client)
|
protected function assertHasFlashDeleteSuccess(Client $client)
|
||||||
|
{
|
||||||
|
$this->assertHasFlashSuccess($client, 'Entry was deleted successful');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param Client $client
|
||||||
|
* @param string|null $message
|
||||||
|
*/
|
||||||
|
protected function assertHasFlashSuccess(Client $client, string $message = null)
|
||||||
{
|
{
|
||||||
$node = $client->getCrawler()->filter('div.alert.alert-success.alert-dismissible');
|
$node = $client->getCrawler()->filter('div.alert.alert-success.alert-dismissible');
|
||||||
$this->assertNotEmpty($node->text());
|
$this->assertNotEmpty($node->text());
|
||||||
|
if (null !== $message) {
|
||||||
|
$this->assertContains($message, $node->text());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
134
tests/Controller/ExportControllerTest.php
Normal file
134
tests/Controller/ExportControllerTest.php
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
<?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\Controller;
|
||||||
|
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Tests\DataFixtures\TimesheetFixtures;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @coversDefaultClass \App\Controller\ExportController
|
||||||
|
* @group integration
|
||||||
|
*/
|
||||||
|
class ExportControllerTest extends ControllerBaseTest
|
||||||
|
{
|
||||||
|
public function testIsSecure()
|
||||||
|
{
|
||||||
|
$this->assertUrlIsSecured('/export/');
|
||||||
|
$this->assertUrlIsSecuredForRole(User::ROLE_USER, '/export/');
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIndexActionHasErrorMessageOnEmptyQuery()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
|
|
||||||
|
$this->request($client, '/export/');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testIndexActionWithEntries()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
$begin = new \DateTime('first day of this month');
|
||||||
|
$end = new \DateTime('last day of this month');
|
||||||
|
$fixture = new TimesheetFixtures();
|
||||||
|
$fixture
|
||||||
|
->setUser($this->getUserByRole($em, User::ROLE_USER))
|
||||||
|
->setAmount(20)
|
||||||
|
->setStartDate($begin)
|
||||||
|
;
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$this->request($client, '/export/');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
// make sure all existing records are displayed
|
||||||
|
$this->assertHasDataTable($client);
|
||||||
|
$this->assertDataTableRowCount($client, 'datatable_export', 20);
|
||||||
|
|
||||||
|
// assert export type buttons are available
|
||||||
|
$expected = ['csv', 'html', 'pdf', 'ods', 'xlsx'];
|
||||||
|
$node = $client->getCrawler()->filter('#export-buttons button');
|
||||||
|
$this->assertEquals(count($expected), $node->count());
|
||||||
|
foreach ($node->getIterator() as $button) {
|
||||||
|
$type = $button->getAttribute('data-type');
|
||||||
|
$this->assertContains($type, $expected);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExportActionWithMissingRenderer()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
|
$this->request($client, '/export/data');
|
||||||
|
|
||||||
|
$response = $client->getResponse();
|
||||||
|
$this->assertFalse($response->isSuccessful());
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExportActionWithInvalidRenderer()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
|
|
||||||
|
$this->request($client, '/export/');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('#export-form')->form();
|
||||||
|
$form->getFormNode()->setAttribute('action', $this->createUrl('/export/data'));
|
||||||
|
$client->submit($form, [
|
||||||
|
'type' => 'default'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $client->getResponse();
|
||||||
|
$this->assertFalse($response->isSuccessful());
|
||||||
|
$this->assertEquals(404, $response->getStatusCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testExportAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
|
||||||
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
|
|
||||||
|
$begin = new \DateTime('first day of this month');
|
||||||
|
$fixture = new TimesheetFixtures();
|
||||||
|
$fixture
|
||||||
|
->setUser($this->getUserByRole($em, User::ROLE_USER))
|
||||||
|
->setAmount(20)
|
||||||
|
->setStartDate($begin)
|
||||||
|
;
|
||||||
|
$this->importFixture($em, $fixture);
|
||||||
|
|
||||||
|
$this->request($client, '/export/');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$form = $client->getCrawler()->filter('#export-form')->form();
|
||||||
|
$form->getFormNode()->setAttribute('action', $this->createUrl('/export/data'));
|
||||||
|
// don't add daterange to make sure the current month is the default range
|
||||||
|
$client->submit($form, [
|
||||||
|
'type' => 'html'
|
||||||
|
]);
|
||||||
|
|
||||||
|
$response = $client->getResponse();
|
||||||
|
$this->assertTrue($response->isSuccessful());
|
||||||
|
$node = $client->getCrawler()->filter('body');
|
||||||
|
$this->assertEquals(1, $node->count());
|
||||||
|
|
||||||
|
// poor mans assertions ;-)
|
||||||
|
$this->assertContains('export_print', $node->getIterator()[0]->getAttribute('class'));
|
||||||
|
$this->assertContains('<h2>List of expenses</h2>', $response->getContent());
|
||||||
|
$this->assertContains('<h3>Summary</h3>', $response->getContent());
|
||||||
|
|
||||||
|
$node = $client->getCrawler()->filter('section.export div#export-records table.dataTable tbody tr');
|
||||||
|
$this->assertEquals(20, $node->count());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -46,9 +46,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
$this->request($client, '/invoice/');
|
$this->request($client, '/invoice/');
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
$this->assertNotEmpty($node->text());
|
|
||||||
$this->assertContains('No invoice entries were found based on your selected filters.', $node->text());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testListTemplateAction()
|
public function testListTemplateAction()
|
||||||
@@ -150,6 +148,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
// no datatable should be displayed
|
||||||
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
$node = $client->getCrawler()->filter('div.callout.callout-warning.lead');
|
||||||
$this->assertEquals(0, $node->count());
|
$this->assertEquals(0, $node->count());
|
||||||
|
|
||||||
@@ -157,8 +156,7 @@ class InvoiceControllerTest extends ControllerBaseTest
|
|||||||
$this->assertNotEmpty($node->text());
|
$this->assertNotEmpty($node->text());
|
||||||
$this->assertContains('This is a preview of the data that will show up in your invoice document.', $node->text());
|
$this->assertContains('This is a preview of the data that will show up in your invoice document.', $node->text());
|
||||||
|
|
||||||
$node = $client->getCrawler()->filter('section.invoice div.table-responsive table.table-striped tbody tr');
|
$this->assertDataTableRowCount($client, 'datatable_invoice', 20);
|
||||||
$this->assertEquals(20, $node->count());
|
|
||||||
|
|
||||||
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
$form = $client->getCrawler()->filter('#invoice-print-form')->form();
|
||||||
$form->getFormNode()->setAttribute('action', $this->createUrl('/invoice/print'));
|
$form->getFormNode()->setAttribute('action', $this->createUrl('/invoice/print'));
|
||||||
|
|||||||
@@ -30,7 +30,9 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
$client = $this->getClientForAuthenticatedUser();
|
$client = $this->getClientForAuthenticatedUser();
|
||||||
$this->request($client, '/timesheet/');
|
$this->request($client, '/timesheet/');
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
$this->assertHasDataTable($client);
|
|
||||||
|
// there are no records by default in the test database
|
||||||
|
$this->assertHasNoEntriesWithFilter($client);
|
||||||
|
|
||||||
$result = $client->getCrawler()->filter('div.breadcrumb div.box-tools div.btn-group a.btn');
|
$result = $client->getCrawler()->filter('div.breadcrumb div.box-tools div.btn-group a.btn');
|
||||||
$this->assertEquals(5, count($result));
|
$this->assertEquals(5, count($result));
|
||||||
@@ -43,7 +45,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
public function testIndexActionWithQuery()
|
public function testIndexActionWithQuery()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser();
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_USER);
|
||||||
|
|
||||||
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
$em = $client->getContainer()->get('doctrine.orm.entity_manager');
|
||||||
$fixture = new TimesheetFixtures();
|
$fixture = new TimesheetFixtures();
|
||||||
@@ -67,8 +69,7 @@ class TimesheetControllerTest extends ControllerBaseTest
|
|||||||
|
|
||||||
$this->assertTrue($client->getResponse()->isSuccessful());
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
$this->assertHasDataTable($client);
|
$this->assertHasDataTable($client);
|
||||||
|
$this->assertDataTableRowCount($client, 'datatable_timesheet', 5);
|
||||||
// TODO more assertions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public function testExportAction()
|
public function testExportAction()
|
||||||
|
|||||||
148
tests/Export/Renderer/AbstractRendererTest.php
Normal file
148
tests/Export/Renderer/AbstractRendererTest.php
Normal file
@@ -0,0 +1,148 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Entity\Activity;
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Entity\Timesheet;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Export\RendererInterface;
|
||||||
|
use App\Repository\Query\TimesheetQuery;
|
||||||
|
use App\Twig\DateExtensions;
|
||||||
|
use App\Twig\Extensions;
|
||||||
|
use App\Utils\LocaleSettings;
|
||||||
|
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Symfony\Component\HttpFoundation\RequestStack;
|
||||||
|
use Symfony\Component\Translation\TranslatorInterface;
|
||||||
|
|
||||||
|
abstract class AbstractRendererTest extends KernelTestCase
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param string $classname
|
||||||
|
* @return RendererInterface
|
||||||
|
*/
|
||||||
|
protected function getAbstractRenderer(string $classname)
|
||||||
|
{
|
||||||
|
$requestStack = new RequestStack();
|
||||||
|
$languages = [
|
||||||
|
'en' => [
|
||||||
|
'date' => 'Y.m.d',
|
||||||
|
'duration' => '%h:%m h'
|
||||||
|
]
|
||||||
|
];
|
||||||
|
|
||||||
|
$request = new Request();
|
||||||
|
$request->setLocale('en');
|
||||||
|
$requestStack->push($request);
|
||||||
|
|
||||||
|
$localeSettings = new LocaleSettings($requestStack, $languages);
|
||||||
|
|
||||||
|
$translator = $this->getMockBuilder(TranslatorInterface::class)->getMock();
|
||||||
|
$dateExtension = new DateExtensions($localeSettings);
|
||||||
|
$extensions = new Extensions($requestStack, $localeSettings);
|
||||||
|
|
||||||
|
return new $classname($translator, $dateExtension, $extensions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param RendererInterface $renderer
|
||||||
|
* @return \Symfony\Component\HttpFoundation\Response
|
||||||
|
*/
|
||||||
|
protected function render(RendererInterface $renderer)
|
||||||
|
{
|
||||||
|
$customer = new Customer();
|
||||||
|
$customer->setName('Customer Name');
|
||||||
|
|
||||||
|
$project = new Project();
|
||||||
|
$project->setName('project name');
|
||||||
|
$project->setCustomer($customer);
|
||||||
|
|
||||||
|
$activity = new Activity();
|
||||||
|
$activity->setName('activity description');
|
||||||
|
$activity->setProject($project);
|
||||||
|
|
||||||
|
$userMethods = ['getId', 'getPreferenceValue', 'getUsername'];
|
||||||
|
$user1 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||||
|
$user1->method('getId')->willReturn(1);
|
||||||
|
$user1->method('getPreferenceValue')->willReturn('50');
|
||||||
|
$user1->method('getUsername')->willReturn('foo-bar');
|
||||||
|
|
||||||
|
$user2 = $this->getMockBuilder(User::class)->setMethods($userMethods)->disableOriginalConstructor()->getMock();
|
||||||
|
$user2->method('getId')->willReturn(2);
|
||||||
|
$user2->method('getUsername')->willReturn('hello-world');
|
||||||
|
|
||||||
|
$timesheet = new Timesheet();
|
||||||
|
$timesheet
|
||||||
|
->setDuration(3600)
|
||||||
|
->setRate(293.27)
|
||||||
|
->setUser($user1)
|
||||||
|
->setActivity($activity)
|
||||||
|
->setProject($project)
|
||||||
|
->setBegin(new \DateTime())
|
||||||
|
->setEnd(new \DateTime())
|
||||||
|
;
|
||||||
|
|
||||||
|
$timesheet2 = new Timesheet();
|
||||||
|
$timesheet2
|
||||||
|
->setDuration(400)
|
||||||
|
->setRate(84.75)
|
||||||
|
->setUser($user2)
|
||||||
|
->setActivity($activity)
|
||||||
|
->setProject($project)
|
||||||
|
->setBegin(new \DateTime())
|
||||||
|
->setEnd(new \DateTime())
|
||||||
|
;
|
||||||
|
|
||||||
|
$timesheet3 = new Timesheet();
|
||||||
|
$timesheet3
|
||||||
|
->setDuration(1800)
|
||||||
|
->setRate(111.11)
|
||||||
|
->setUser($user1)
|
||||||
|
->setActivity($activity)
|
||||||
|
->setProject($project)
|
||||||
|
->setBegin(new \DateTime())
|
||||||
|
->setEnd(new \DateTime())
|
||||||
|
;
|
||||||
|
|
||||||
|
$timesheet4 = new Timesheet();
|
||||||
|
$timesheet4
|
||||||
|
->setDuration(400)
|
||||||
|
->setRate(1947.99)
|
||||||
|
->setUser($user2)
|
||||||
|
->setActivity($activity)
|
||||||
|
->setProject($project)
|
||||||
|
->setBegin(new \DateTime())
|
||||||
|
->setEnd(new \DateTime())
|
||||||
|
;
|
||||||
|
|
||||||
|
$timesheet5 = new Timesheet();
|
||||||
|
$timesheet5
|
||||||
|
->setDuration(400)
|
||||||
|
->setFixedRate(84)
|
||||||
|
->setUser((new User())->setUsername('kevin'))
|
||||||
|
->setActivity($activity)
|
||||||
|
->setProject($project)
|
||||||
|
->setBegin(new \DateTime())
|
||||||
|
->setEnd(new \DateTime())
|
||||||
|
;
|
||||||
|
|
||||||
|
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5];
|
||||||
|
|
||||||
|
$query = new TimesheetQuery();
|
||||||
|
$query->setActivity($activity);
|
||||||
|
$query->setBegin(new \DateTime());
|
||||||
|
$query->setEnd(new \DateTime());
|
||||||
|
$query->setProject($project);
|
||||||
|
|
||||||
|
return $renderer->render($entries, $query);
|
||||||
|
}
|
||||||
|
}
|
||||||
71
tests/Export/Renderer/CsvRendererTest.php
Normal file
71
tests/Export/Renderer/CsvRendererTest.php
Normal file
@@ -0,0 +1,71 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\Renderer\CsvRenderer;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Export\Renderer\CsvRenderer
|
||||||
|
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
|
||||||
|
* @covers \App\Export\Renderer\RendererTrait
|
||||||
|
*/
|
||||||
|
class CsvRendererTest extends AbstractRendererTest
|
||||||
|
{
|
||||||
|
public function testConfiguration()
|
||||||
|
{
|
||||||
|
$sut = $this->getAbstractRenderer(CsvRenderer::class);
|
||||||
|
|
||||||
|
$this->assertEquals('csv', $sut->getId());
|
||||||
|
$this->assertEquals('csv', $sut->getTitle());
|
||||||
|
$this->assertEquals('csv', $sut->getIcon());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getTestModel()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['01:50 h', '2,437.12 €', '1,947.99 €', 7, 5, 1, 2, 2]
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider getTestModel
|
||||||
|
*/
|
||||||
|
public function testRender($totalDuration, $totalRate, $expectedRate, $expectedRows, $expectedDescriptions, $expectedUser1, $expectedUser2, $expectedUser3)
|
||||||
|
{
|
||||||
|
$sut = $this->getAbstractRenderer(CsvRenderer::class);
|
||||||
|
|
||||||
|
/** @var BinaryFileResponse $response */
|
||||||
|
$response = $this->render($sut);
|
||||||
|
|
||||||
|
$file = $response->getFile();
|
||||||
|
$this->assertEquals('text/csv', $response->headers->get('Content-Type'));
|
||||||
|
$this->assertEquals('attachment; filename=kimai-export.csv', $response->headers->get('Content-Disposition'));
|
||||||
|
|
||||||
|
$this->assertTrue(file_exists($file->getRealPath()));
|
||||||
|
$content = file_get_contents($file->getRealPath());
|
||||||
|
|
||||||
|
$this->assertContains('"' . $totalDuration . '"', $content);
|
||||||
|
$this->assertContains('"' . $totalRate . '"', $content);
|
||||||
|
$this->assertContains('"' . $expectedRate . '"', $content);
|
||||||
|
$this->assertEquals($expectedRows, substr_count($content, PHP_EOL));
|
||||||
|
$this->assertEquals($expectedDescriptions, substr_count($content, 'activity description'));
|
||||||
|
$this->assertEquals($expectedUser1, substr_count($content, ',"kevin",'));
|
||||||
|
$this->assertEquals($expectedUser3, substr_count($content, ',"hello-world",'));
|
||||||
|
$this->assertEquals($expectedUser2, substr_count($content, ',"foo-bar",'));
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$response->sendContent();
|
||||||
|
$content2 = ob_get_clean();
|
||||||
|
|
||||||
|
$this->assertEquals($content, $content2);
|
||||||
|
$this->assertFalse(file_exists($file->getRealPath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
62
tests/Export/Renderer/HtmlRendererTest.php
Normal file
62
tests/Export/Renderer/HtmlRendererTest.php
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
/*
|
||||||
|
* This file is part of the Kimai time-tracking app.
|
||||||
|
*
|
||||||
|
* For the full copyright and license information, please view the LICENSE
|
||||||
|
* file that was distributed with this source code.
|
||||||
|
*/
|
||||||
|
|
||||||
|
namespace App\Tests\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\Renderer\HtmlRenderer;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Export\Renderer\HtmlRenderer
|
||||||
|
* @covers \App\Export\Renderer\RendererTrait
|
||||||
|
*/
|
||||||
|
class HtmlRendererTest extends AbstractRendererTest
|
||||||
|
{
|
||||||
|
public function testConfiguration()
|
||||||
|
{
|
||||||
|
$sut = new HtmlRenderer(
|
||||||
|
$this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals('html', $sut->getId());
|
||||||
|
$this->assertEquals('print', $sut->getTitle());
|
||||||
|
$this->assertEquals('print', $sut->getIcon());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRender()
|
||||||
|
{
|
||||||
|
$kernel = self::bootKernel();
|
||||||
|
/** @var \Twig_Environment $twig */
|
||||||
|
$twig = $kernel->getContainer()->get('twig');
|
||||||
|
$stack = $kernel->getContainer()->get('request_stack');
|
||||||
|
$request = new Request();
|
||||||
|
$request->setLocale('en');
|
||||||
|
$stack->push($request);
|
||||||
|
|
||||||
|
/** @var FilesystemLoader $loader */
|
||||||
|
$loader = $twig->getLoader();
|
||||||
|
|
||||||
|
$sut = new HtmlRenderer($twig);
|
||||||
|
|
||||||
|
$response = $this->render($sut);
|
||||||
|
|
||||||
|
$content = $response->getContent();
|
||||||
|
|
||||||
|
$this->assertContains('<h2>List of expenses</h2>', $content);
|
||||||
|
$this->assertContains('<h3>Summary</h3>', $content);
|
||||||
|
|
||||||
|
$this->assertContains('<td>Customer Name</td>', $content);
|
||||||
|
$this->assertContains('<td>project name</td>', $content);
|
||||||
|
$this->assertContains('<td class="duration">01:50 h</td>', $content);
|
||||||
|
$this->assertContains('<td class="cost">2,437.12 €</td>', $content);
|
||||||
|
|
||||||
|
$this->assertEquals(5, substr_count($content, '<td>activity description</td>'));
|
||||||
|
}
|
||||||
|
}
|
||||||
51
tests/Export/Renderer/OdsRendererTest.php
Normal file
51
tests/Export/Renderer/OdsRendererTest.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\Renderer\OdsRenderer;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Export\Renderer\OdsRenderer
|
||||||
|
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
|
||||||
|
* @covers \App\Export\Renderer\RendererTrait
|
||||||
|
*/
|
||||||
|
class OdsRendererTest extends AbstractRendererTest
|
||||||
|
{
|
||||||
|
public function testConfiguration()
|
||||||
|
{
|
||||||
|
$sut = $this->getAbstractRenderer(OdsRenderer::class);
|
||||||
|
|
||||||
|
$this->assertEquals('ods', $sut->getId());
|
||||||
|
$this->assertEquals('ods', $sut->getTitle());
|
||||||
|
$this->assertEquals('ods', $sut->getIcon());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRender()
|
||||||
|
{
|
||||||
|
$sut = $this->getAbstractRenderer(OdsRenderer::class);
|
||||||
|
|
||||||
|
/** @var BinaryFileResponse $response */
|
||||||
|
$response = $this->render($sut);
|
||||||
|
|
||||||
|
$file = $response->getFile();
|
||||||
|
$this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
|
||||||
|
$this->assertEquals('attachment; filename=kimai-export.ods', $response->headers->get('Content-Disposition'));
|
||||||
|
|
||||||
|
$this->assertTrue(file_exists($file->getRealPath()));
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$response->sendContent();
|
||||||
|
$content2 = ob_get_clean();
|
||||||
|
$this->assertNotEmpty($content2);
|
||||||
|
|
||||||
|
$this->assertFalse(file_exists($file->getRealPath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
55
tests/Export/Renderer/PdfRendererTest.php
Normal file
55
tests/Export/Renderer/PdfRendererTest.php
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\Renderer\PDFRenderer;
|
||||||
|
use Symfony\Component\HttpFoundation\Request;
|
||||||
|
use Twig\Loader\FilesystemLoader;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Export\Renderer\PDFRenderer
|
||||||
|
* @covers \App\Export\Renderer\RendererTrait
|
||||||
|
*/
|
||||||
|
class PdfRendererTest extends AbstractRendererTest
|
||||||
|
{
|
||||||
|
public function testConfiguration()
|
||||||
|
{
|
||||||
|
$sut = new PDFRenderer(
|
||||||
|
$this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock()
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->assertEquals('pdf', $sut->getId());
|
||||||
|
$this->assertEquals('pdf', $sut->getTitle());
|
||||||
|
$this->assertEquals('pdf', $sut->getIcon());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRender()
|
||||||
|
{
|
||||||
|
$kernel = self::bootKernel();
|
||||||
|
/** @var \Twig_Environment $twig */
|
||||||
|
$twig = $kernel->getContainer()->get('twig');
|
||||||
|
$stack = $kernel->getContainer()->get('request_stack');
|
||||||
|
$request = new Request();
|
||||||
|
$request->setLocale('en');
|
||||||
|
$stack->push($request);
|
||||||
|
|
||||||
|
/** @var FilesystemLoader $loader */
|
||||||
|
$loader = $twig->getLoader();
|
||||||
|
|
||||||
|
$sut = new PDFRenderer($twig);
|
||||||
|
|
||||||
|
$response = $this->render($sut);
|
||||||
|
|
||||||
|
$this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
|
||||||
|
$this->assertEquals('attachment; filename=kimai-export.pdf', $response->headers->get('Content-Disposition'));
|
||||||
|
|
||||||
|
$this->assertNotEmpty($response->getContent());
|
||||||
|
}
|
||||||
|
}
|
||||||
51
tests/Export/Renderer/XlsxRendererTest.php
Normal file
51
tests/Export/Renderer/XlsxRendererTest.php
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
<?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\Export\Renderer;
|
||||||
|
|
||||||
|
use App\Export\Renderer\XlsxRenderer;
|
||||||
|
use Symfony\Component\HttpFoundation\BinaryFileResponse;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Export\Renderer\XlsxRenderer
|
||||||
|
* @covers \App\Export\Renderer\AbstractSpreadsheetRenderer
|
||||||
|
* @covers \App\Export\Renderer\RendererTrait
|
||||||
|
*/
|
||||||
|
class XlsxRendererTest extends AbstractRendererTest
|
||||||
|
{
|
||||||
|
public function testConfiguration()
|
||||||
|
{
|
||||||
|
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
|
||||||
|
|
||||||
|
$this->assertEquals('xlsx', $sut->getId());
|
||||||
|
$this->assertEquals('xlsx', $sut->getTitle());
|
||||||
|
$this->assertEquals('xlsx', $sut->getIcon());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testRender()
|
||||||
|
{
|
||||||
|
$sut = $this->getAbstractRenderer(XlsxRenderer::class);
|
||||||
|
|
||||||
|
/** @var BinaryFileResponse $response */
|
||||||
|
$response = $this->render($sut);
|
||||||
|
|
||||||
|
$file = $response->getFile();
|
||||||
|
$this->assertEquals('application/vnd.openxmlformats-officedocument.spreadsheetml.sheet', $response->headers->get('Content-Type'));
|
||||||
|
$this->assertEquals('attachment; filename=kimai-export.xlsx', $response->headers->get('Content-Disposition'));
|
||||||
|
|
||||||
|
$this->assertTrue(file_exists($file->getRealPath()));
|
||||||
|
|
||||||
|
ob_start();
|
||||||
|
$response->sendContent();
|
||||||
|
$content2 = ob_get_clean();
|
||||||
|
$this->assertNotEmpty($content2);
|
||||||
|
|
||||||
|
$this->assertFalse(file_exists($file->getRealPath()));
|
||||||
|
}
|
||||||
|
}
|
||||||
43
tests/Export/ServiceExportTest.php
Normal file
43
tests/Export/ServiceExportTest.php
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?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\Export;
|
||||||
|
|
||||||
|
use App\Export\Renderer\HtmlRenderer;
|
||||||
|
use App\Export\ServiceExport;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Export\ServiceExport
|
||||||
|
*/
|
||||||
|
class ServiceExportTest extends TestCase
|
||||||
|
{
|
||||||
|
public function testEmptyObject()
|
||||||
|
{
|
||||||
|
$sut = new ServiceExport();
|
||||||
|
$this->assertEmpty($sut->getRenderer());
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testUnknownRendererReturnsNull()
|
||||||
|
{
|
||||||
|
$sut = new ServiceExport();
|
||||||
|
$this->assertNull($sut->getRendererById('default'));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testAdd()
|
||||||
|
{
|
||||||
|
$sut = new ServiceExport();
|
||||||
|
|
||||||
|
$sut->addRenderer(new HtmlRenderer(
|
||||||
|
$this->getMockBuilder(\Twig_Environment::class)->disableOriginalConstructor()->getMock()
|
||||||
|
));
|
||||||
|
|
||||||
|
$this->assertEquals(1, count($sut->getRenderer()));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -17,6 +17,9 @@ use App\Invoice\ServiceInvoice;
|
|||||||
use App\Repository\InvoiceDocumentRepository;
|
use App\Repository\InvoiceDocumentRepository;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Invoice\ServiceInvoice
|
||||||
|
*/
|
||||||
class ServiceInvoiceTest extends TestCase
|
class ServiceInvoiceTest extends TestCase
|
||||||
{
|
{
|
||||||
public function testEmptyObject()
|
public function testEmptyObject()
|
||||||
|
|||||||
131
tests/Repository/Query/ExportQueryTest.php
Normal file
131
tests/Repository/Query/ExportQueryTest.php
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
<?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\Repository\Query;
|
||||||
|
|
||||||
|
use App\Entity\Activity;
|
||||||
|
use App\Entity\Customer;
|
||||||
|
use App\Entity\Project;
|
||||||
|
use App\Entity\User;
|
||||||
|
use App\Repository\Query\ExportQuery;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @covers \App\Repository\Query\ExportQuery
|
||||||
|
*/
|
||||||
|
class ExportQueryTest extends BaseQueryTest
|
||||||
|
{
|
||||||
|
public function testQuery()
|
||||||
|
{
|
||||||
|
$sut = new ExportQuery();
|
||||||
|
|
||||||
|
$this->assertResultType($sut);
|
||||||
|
$this->assertHiddenEntity($sut);
|
||||||
|
$this->assertPage($sut);
|
||||||
|
$this->assertPageSize($sut);
|
||||||
|
$this->assertOrderBy($sut, 'begin');
|
||||||
|
$this->assertOrder($sut, ExportQuery::ORDER_DESC);
|
||||||
|
|
||||||
|
$this->assertUser($sut);
|
||||||
|
$this->assertCustomer($sut);
|
||||||
|
$this->assertProject($sut);
|
||||||
|
$this->assertActivity($sut);
|
||||||
|
$this->assertState($sut);
|
||||||
|
$this->assertExported($sut);
|
||||||
|
$this->assertType($sut);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertUser(ExportQuery $sut)
|
||||||
|
{
|
||||||
|
$this->assertNull($sut->getUser());
|
||||||
|
|
||||||
|
$expected = new User();
|
||||||
|
$expected->setUsername('foo-bar');
|
||||||
|
$sut->setUser($expected);
|
||||||
|
$this->assertEquals($expected, $sut->getUser());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertCustomer(ExportQuery $sut)
|
||||||
|
{
|
||||||
|
$this->assertNull($sut->getCustomer());
|
||||||
|
|
||||||
|
$expected = new Customer();
|
||||||
|
$expected->setName('foo-bar');
|
||||||
|
$sut->setCustomer($expected);
|
||||||
|
$this->assertEquals($expected, $sut->getCustomer());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertProject(ExportQuery $sut)
|
||||||
|
{
|
||||||
|
$this->assertNull($sut->getProject());
|
||||||
|
|
||||||
|
$expected = new Project();
|
||||||
|
$expected->setName('foo-bar');
|
||||||
|
$sut->setProject($expected);
|
||||||
|
$this->assertEquals($expected, $sut->getProject());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertActivity(ExportQuery $sut)
|
||||||
|
{
|
||||||
|
$this->assertNull($sut->getActivity());
|
||||||
|
|
||||||
|
$expected = new Activity();
|
||||||
|
$expected->setName('foo-bar');
|
||||||
|
$sut->setActivity($expected);
|
||||||
|
$this->assertEquals($expected, $sut->getActivity());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertState(ExportQuery $sut)
|
||||||
|
{
|
||||||
|
$this->assertEquals(ExportQuery::STATE_ALL, $sut->getState());
|
||||||
|
|
||||||
|
$sut->setState(PHP_INT_MAX);
|
||||||
|
$this->assertEquals(ExportQuery::STATE_ALL, $sut->getState());
|
||||||
|
|
||||||
|
$sut->setState(ExportQuery::STATE_STOPPED);
|
||||||
|
$this->assertEquals(ExportQuery::STATE_STOPPED, $sut->getState());
|
||||||
|
|
||||||
|
$sut->setState(ExportQuery::STATE_RUNNING);
|
||||||
|
$this->assertEquals(ExportQuery::STATE_RUNNING, $sut->getState());
|
||||||
|
|
||||||
|
$sut->setState(ExportQuery::STATE_ALL);
|
||||||
|
$this->assertEquals(ExportQuery::STATE_ALL, $sut->getState());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertExported(ExportQuery $sut)
|
||||||
|
{
|
||||||
|
$this->assertEquals(ExportQuery::STATE_ALL, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported(PHP_INT_MAX);
|
||||||
|
$this->assertEquals(ExportQuery::STATE_ALL, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported(ExportQuery::STATE_EXPORTED);
|
||||||
|
$this->assertEquals(ExportQuery::STATE_EXPORTED, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported(ExportQuery::STATE_NOT_EXPORTED);
|
||||||
|
$this->assertEquals(ExportQuery::STATE_NOT_EXPORTED, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported(ExportQuery::STATE_ALL);
|
||||||
|
$this->assertEquals(ExportQuery::STATE_ALL, $sut->getExported());
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function assertType(ExportQuery $sut)
|
||||||
|
{
|
||||||
|
$this->assertNull($sut->getType());
|
||||||
|
|
||||||
|
$allowed = ['html', 'csv', 'pdf', 'xlsx', 'ods'];
|
||||||
|
|
||||||
|
foreach ($allowed as $type) {
|
||||||
|
$sut->setType($type);
|
||||||
|
$this->assertEquals($type, $sut->getType());
|
||||||
|
}
|
||||||
|
|
||||||
|
$sut->setType('foo');
|
||||||
|
$this->assertEquals('ods', $sut->getType());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -36,6 +36,7 @@ class TimesheetQueryTest extends BaseQueryTest
|
|||||||
$this->assertProject($sut);
|
$this->assertProject($sut);
|
||||||
$this->assertActivity($sut);
|
$this->assertActivity($sut);
|
||||||
$this->assertState($sut);
|
$this->assertState($sut);
|
||||||
|
$this->assertExported($sut);
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function assertUser(TimesheetQuery $sut)
|
protected function assertUser(TimesheetQuery $sut)
|
||||||
@@ -94,4 +95,24 @@ class TimesheetQueryTest extends BaseQueryTest
|
|||||||
$sut->setState(TimesheetQuery::STATE_ALL);
|
$sut->setState(TimesheetQuery::STATE_ALL);
|
||||||
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState());
|
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getState());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected function assertExported(TimesheetQuery $sut)
|
||||||
|
{
|
||||||
|
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported(PHP_INT_MAX);
|
||||||
|
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported(TimesheetQuery::STATE_EXPORTED);
|
||||||
|
$this->assertEquals(TimesheetQuery::STATE_EXPORTED, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported(TimesheetQuery::STATE_NOT_EXPORTED);
|
||||||
|
$this->assertEquals(TimesheetQuery::STATE_NOT_EXPORTED, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported(TimesheetQuery::STATE_ALL);
|
||||||
|
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported());
|
||||||
|
|
||||||
|
$sut->setExported('02');
|
||||||
|
$this->assertEquals(TimesheetQuery::STATE_ALL, $sut->getExported());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ class DateExtensionsTest extends TestCase
|
|||||||
|
|
||||||
public function testGetFilters()
|
public function testGetFilters()
|
||||||
{
|
{
|
||||||
$filters = ['month_name', 'date_short'];
|
$filters = ['month_name', 'date_short', 'date_time'];
|
||||||
$sut = $this->getSut('de', []);
|
$sut = $this->getSut('de', []);
|
||||||
$twigFilters = $sut->getFilters();
|
$twigFilters = $sut->getFilters();
|
||||||
$this->assertCount(count($filters), $twigFilters);
|
$this->assertCount(count($filters), $twigFilters);
|
||||||
@@ -77,6 +77,29 @@ class DateExtensionsTest extends TestCase
|
|||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $locale
|
||||||
|
* @param \DateTime $date
|
||||||
|
* @param string $result
|
||||||
|
* @dataProvider getDateTimeData
|
||||||
|
*/
|
||||||
|
public function testDateTime($locale, \DateTime $date, $result)
|
||||||
|
{
|
||||||
|
$sut = $this->getSut($locale, [
|
||||||
|
'de' => ['date_time' => 'd.m.Y H:i:s'],
|
||||||
|
'en' => ['date_time' => 'Y-m-d h:m A'],
|
||||||
|
]);
|
||||||
|
$this->assertEquals($result, $sut->dateTime($date));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function getDateTimeData()
|
||||||
|
{
|
||||||
|
return [
|
||||||
|
['en', new \DateTime('7 January 2010'), '2010-01-07 12:01 AM'],
|
||||||
|
['de', (new \DateTime('1980-12-14'))->setTime(13, 27, 55), '14.12.1980 13:27:55'],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param \DateTime $date
|
* @param \DateTime $date
|
||||||
* @param string $result
|
* @param string $result
|
||||||
|
|||||||
@@ -19,7 +19,6 @@ use Symfony\Component\HttpFoundation\RequestStack;
|
|||||||
*/
|
*/
|
||||||
class LocaleSettingsTest extends TestCase
|
class LocaleSettingsTest extends TestCase
|
||||||
{
|
{
|
||||||
|
|
||||||
protected function getRequestStack(string $locale)
|
protected function getRequestStack(string $locale)
|
||||||
{
|
{
|
||||||
$request = new Request();
|
$request = new Request();
|
||||||
@@ -44,6 +43,7 @@ class LocaleSettingsTest extends TestCase
|
|||||||
'date_type' => 'dd.MM.yyyy',
|
'date_type' => 'dd.MM.yyyy',
|
||||||
'date_picker' => 'DD.MM.YYYY',
|
'date_picker' => 'DD.MM.YYYY',
|
||||||
'date' => 'd.m.Y',
|
'date' => 'd.m.Y',
|
||||||
|
'date_time' => 'd.m. H:i',
|
||||||
'duration' => '%h:%m h',
|
'duration' => '%h:%m h',
|
||||||
],
|
],
|
||||||
'en' => [
|
'en' => [
|
||||||
@@ -52,6 +52,7 @@ class LocaleSettingsTest extends TestCase
|
|||||||
'date_type' => 'yyyy-MM-dd',
|
'date_type' => 'yyyy-MM-dd',
|
||||||
'date_picker' => 'YYYY-MM-DD',
|
'date_picker' => 'YYYY-MM-DD',
|
||||||
'date' => 'Y-m-d',
|
'date' => 'Y-m-d',
|
||||||
|
'date_time' => 'm-d H:i',
|
||||||
'duration' => '%h:%m h',
|
'duration' => '%h:%m h',
|
||||||
],
|
],
|
||||||
'pt_BR' => [
|
'pt_BR' => [
|
||||||
@@ -162,6 +163,13 @@ class LocaleSettingsTest extends TestCase
|
|||||||
$this->assertEquals('d.m.Y', $sut->getDateFormat('de'));
|
$this->assertEquals('d.m.Y', $sut->getDateFormat('de'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testGetDateTimeFormat()
|
||||||
|
{
|
||||||
|
$sut = $this->getSut('en', $this->getDefaultSettings());
|
||||||
|
$this->assertEquals('m-d H:i', $sut->getDateTimeFormat());
|
||||||
|
$this->assertEquals('d.m. H:i', $sut->getDateTimeFormat('de'));
|
||||||
|
}
|
||||||
|
|
||||||
public function testGetDateTypeFormat()
|
public function testGetDateTypeFormat()
|
||||||
{
|
{
|
||||||
$sut = $this->getSut('en', $this->getDefaultSettings());
|
$sut = $this->getSut('en', $this->getDefaultSettings());
|
||||||
|
|||||||
@@ -681,10 +681,6 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>م بإنشاء فواتير من بيانات ورقة التوقيت الخاصة بك.</target>
|
<target>م بإنشاء فواتير من بيانات ورقة التوقيت الخاصة بك.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>لم يتم العثور على بيانات الفواتير بناءً على الفلاتر المحددة.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="invoice.preview">
|
<trans-unit id="invoice.preview">
|
||||||
<source>invoice.preview</source>
|
<source>invoice.preview</source>
|
||||||
<target>هذه معاينة للبيانات التي ستظهر في مستند الفاتورة.</target>
|
<target>هذه معاينة للبيانات التي ستظهر في مستند الفاتورة.</target>
|
||||||
@@ -737,16 +733,16 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>تاريخ الخدمة</target>
|
<target>تاريخ الخدمة</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>كمية</target>
|
<target>كمية</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>السعر الكلي</target>
|
<target>السعر الكلي</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.unit_price">
|
<trans-unit id="label.unit_price">
|
||||||
<source>invoice.unit_price</source>
|
<source>label.unit_price</source>
|
||||||
<target>سعر الوحدة</target>
|
<target>سعر الوحدة</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
|
|||||||
@@ -95,6 +95,10 @@
|
|||||||
<source>menu.invoice</source>
|
<source>menu.invoice</source>
|
||||||
<target>Rechnungen</target>
|
<target>Rechnungen</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="menu.export">
|
||||||
|
<source>menu.export</source>
|
||||||
|
<target>Export</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="menu.admin_timesheet">
|
<trans-unit id="menu.admin_timesheet">
|
||||||
<source>menu.admin_timesheet</source>
|
<source>menu.admin_timesheet</source>
|
||||||
<target>Zeiterfassung</target>
|
<target>Zeiterfassung</target>
|
||||||
@@ -121,7 +125,7 @@
|
|||||||
-->
|
-->
|
||||||
<trans-unit id="error.no_entries_found">
|
<trans-unit id="error.no_entries_found">
|
||||||
<source>error.no_entries_found</source>
|
<source>error.no_entries_found</source>
|
||||||
<target>Keine Einträge vorhanden</target>
|
<target>Anhand ihrer ausgewählten Filter wurden keine Einträge gefunden.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -722,10 +726,6 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>Hier erstellen Sie Rechnungen für ihre aufgezeichneten Zeiten.</target>
|
<target>Hier erstellen Sie Rechnungen für ihre aufgezeichneten Zeiten.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>Anhand ihrer ausgewählten Filter wurden keine Rechnungseinträge gefunden.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="invoice.preview">
|
<trans-unit id="invoice.preview">
|
||||||
<source>invoice.preview</source>
|
<source>invoice.preview</source>
|
||||||
<target>Dies ist eine Vorschau der Daten, die in Ihrer Rechnung enthalten sein werden.</target>
|
<target>Dies ist eine Vorschau der Daten, die in Ihrer Rechnung enthalten sein werden.</target>
|
||||||
@@ -734,6 +734,22 @@
|
|||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
<target>Drucken</target>
|
<target>Drucken</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="button.csv">
|
||||||
|
<source>button.csv</source>
|
||||||
|
<target>CSV</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="button.xlsx">
|
||||||
|
<source>button.xlsx</source>
|
||||||
|
<target>Excel</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="button.pdf">
|
||||||
|
<source>button.pdf</source>
|
||||||
|
<target>PDF</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="button.ods">
|
||||||
|
<source>button.ods</source>
|
||||||
|
<target>ODS</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="invoice_print">
|
<trans-unit id="invoice_print">
|
||||||
<source>invoice_print</source>
|
<source>invoice_print</source>
|
||||||
<target>Rechnung</target>
|
<target>Rechnung</target>
|
||||||
@@ -778,16 +794,16 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>Leistungsdatum</target>
|
<target>Leistungsdatum</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>Anzahl</target>
|
<target>Anzahl</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>Gesamtpreis</target>
|
<target>Gesamtpreis</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.unit_price">
|
<trans-unit id="label.unit_price">
|
||||||
<source>invoice.unit_price</source>
|
<source>label.unit_price</source>
|
||||||
<target>Einzelbetrag</target>
|
<target>Einzelbetrag</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
@@ -827,6 +843,42 @@
|
|||||||
</target>
|
</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Export
|
||||||
|
-->
|
||||||
|
<trans-unit id="export.title">
|
||||||
|
<source>export.title</source>
|
||||||
|
<target>Export</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.subtitle">
|
||||||
|
<source>export.subtitle</source>
|
||||||
|
<target>Exportieren Sie geloggte Zeiten in veschiedene Zielformate</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.period">
|
||||||
|
<source>export.period</source>
|
||||||
|
<target>Zeitraum</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.document_title">
|
||||||
|
<source>export.document_title</source>
|
||||||
|
<target>Aufstellung zu Aufwänden</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.full_list">
|
||||||
|
<source>export.full_list</source>
|
||||||
|
<target>Vollständige Auflistung</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.summary">
|
||||||
|
<source>export.summary</source>
|
||||||
|
<target>Zusammenfassung</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.page_of">
|
||||||
|
<source>export.page_of</source>
|
||||||
|
<target>Seite %page% von %pages%</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.date_copyright">
|
||||||
|
<source>export.date_copyright</source>
|
||||||
|
<target>Erstellt %date% mit %kimai%</target>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Month names
|
Month names
|
||||||
-->
|
-->
|
||||||
@@ -916,6 +968,18 @@
|
|||||||
<source>label.entryState</source>
|
<source>label.entryState</source>
|
||||||
<target>Zeiten</target>
|
<target>Zeiten</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="label.exported">
|
||||||
|
<source>label.exported</source>
|
||||||
|
<target>Exportiert</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="entryState.exported">
|
||||||
|
<source>entryState.exported</source>
|
||||||
|
<target>Abgerechnet</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="entryState.not_exported">
|
||||||
|
<source>entryState.not_exported</source>
|
||||||
|
<target>Offen</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="entryState.all">
|
<trans-unit id="entryState.all">
|
||||||
<source>entryState.all</source>
|
<source>entryState.all</source>
|
||||||
<target>Alle</target>
|
<target>Alle</target>
|
||||||
@@ -928,6 +992,14 @@
|
|||||||
<source>entryState.stopped</source>
|
<source>entryState.stopped</source>
|
||||||
<target>Beendete</target>
|
<target>Beendete</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="export.clear_all">
|
||||||
|
<source>export.clear_all</source>
|
||||||
|
<target>Alle angezeigten Einträge als offen markieren?</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.mark_all">
|
||||||
|
<source>export.mark_all</source>
|
||||||
|
<target>Alle angezeigten Einträge als abgerechnet markieren?</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -95,6 +95,10 @@
|
|||||||
<source>menu.invoice</source>
|
<source>menu.invoice</source>
|
||||||
<target>Invoices</target>
|
<target>Invoices</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="menu.export">
|
||||||
|
<source>menu.export</source>
|
||||||
|
<target>Export</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="menu.admin_timesheet">
|
<trans-unit id="menu.admin_timesheet">
|
||||||
<source>menu.admin_timesheet</source>
|
<source>menu.admin_timesheet</source>
|
||||||
<target>Timesheets</target>
|
<target>Timesheets</target>
|
||||||
@@ -121,7 +125,7 @@
|
|||||||
-->
|
-->
|
||||||
<trans-unit id="error.no_entries_found">
|
<trans-unit id="error.no_entries_found">
|
||||||
<source>error.no_entries_found</source>
|
<source>error.no_entries_found</source>
|
||||||
<target>No entries existing</target>
|
<target>No entries were found based on your selected filters.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -722,10 +726,6 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>Create invoices from your recorded timesheet entries.</target>
|
<target>Create invoices from your recorded timesheet entries.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>No invoice entries were found based on your selected filters.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="invoice.preview">
|
<trans-unit id="invoice.preview">
|
||||||
<source>invoice.preview</source>
|
<source>invoice.preview</source>
|
||||||
<target>This is a preview of the data that will show up in your invoice document.</target>
|
<target>This is a preview of the data that will show up in your invoice document.</target>
|
||||||
@@ -734,6 +734,22 @@
|
|||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
<target>Print</target>
|
<target>Print</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="button.csv">
|
||||||
|
<source>button.csv</source>
|
||||||
|
<target>CSV</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="button.xlsx">
|
||||||
|
<source>button.xlsx</source>
|
||||||
|
<target>Excel</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="button.pdf">
|
||||||
|
<source>button.pdf</source>
|
||||||
|
<target>PDF</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="button.ods">
|
||||||
|
<source>button.ods</source>
|
||||||
|
<target>ODS</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="invoice_print">
|
<trans-unit id="invoice_print">
|
||||||
<source>invoice_print</source>
|
<source>invoice_print</source>
|
||||||
<target>Invoice</target>
|
<target>Invoice</target>
|
||||||
@@ -778,16 +794,16 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>Service date</target>
|
<target>Service date</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>Quantity</target>
|
<target>Quantity</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>Total price</target>
|
<target>Total price</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.unit_price">
|
<trans-unit id="label.unit_price">
|
||||||
<source>invoice.unit_price</source>
|
<source>label.unit_price</source>
|
||||||
<target>Unit price</target>
|
<target>Unit price</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
@@ -827,6 +843,42 @@
|
|||||||
</target>
|
</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
|
||||||
|
<!--
|
||||||
|
Export
|
||||||
|
-->
|
||||||
|
<trans-unit id="export.title">
|
||||||
|
<source>export.title</source>
|
||||||
|
<target>Export</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.subtitle">
|
||||||
|
<source>export.subtitle</source>
|
||||||
|
<target>Export timesheet data to various formats</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.period">
|
||||||
|
<source>export.period</source>
|
||||||
|
<target>Period</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.document_title">
|
||||||
|
<source>export.document_title</source>
|
||||||
|
<target>List of expenses</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.full_list">
|
||||||
|
<source>export.full_list</source>
|
||||||
|
<target>Full list</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.summary">
|
||||||
|
<source>export.summary</source>
|
||||||
|
<target>Summary</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.page_of">
|
||||||
|
<source>export.page_of</source>
|
||||||
|
<target>Page %page% of %pages%</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.date_copyright">
|
||||||
|
<source>export.date_copyright</source>
|
||||||
|
<target>Created %date% with %kimai%</target>
|
||||||
|
</trans-unit>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
Month names
|
Month names
|
||||||
-->
|
-->
|
||||||
@@ -916,6 +968,18 @@
|
|||||||
<source>label.entryState</source>
|
<source>label.entryState</source>
|
||||||
<target>Records</target>
|
<target>Records</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="label.exported">
|
||||||
|
<source>label.exported</source>
|
||||||
|
<target>Exported</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="entryState.exported">
|
||||||
|
<source>entryState.exported</source>
|
||||||
|
<target>Cleared</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="entryState.not_exported">
|
||||||
|
<source>entryState.not_exported</source>
|
||||||
|
<target>Open</target>
|
||||||
|
</trans-unit>
|
||||||
<trans-unit id="entryState.all">
|
<trans-unit id="entryState.all">
|
||||||
<source>entryState.all</source>
|
<source>entryState.all</source>
|
||||||
<target>All</target>
|
<target>All</target>
|
||||||
@@ -928,6 +992,14 @@
|
|||||||
<source>entryState.stopped</source>
|
<source>entryState.stopped</source>
|
||||||
<target>Stopped</target>
|
<target>Stopped</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
|
<trans-unit id="export.clear_all">
|
||||||
|
<source>export.clear_all</source>
|
||||||
|
<target>Mark all shown records as open?</target>
|
||||||
|
</trans-unit>
|
||||||
|
<trans-unit id="export.mark_all">
|
||||||
|
<source>export.mark_all</source>
|
||||||
|
<target>Mark all shown records as cleared?</target>
|
||||||
|
</trans-unit>
|
||||||
</body>
|
</body>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|||||||
@@ -653,10 +653,6 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>Crear facturas desde los registros de sus hojas de tiempo.</target>
|
<target>Crear facturas desde los registros de sus hojas de tiempo.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>Antes de crear una factura, tiene que seleccionar al menos un filtro de cliente.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="button.print">
|
<trans-unit id="button.print">
|
||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
<target>Imprimir</target>
|
<target>Imprimir</target>
|
||||||
@@ -705,12 +701,12 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>Fecha de servicio</target>
|
<target>Fecha de servicio</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>Cantidad</target>
|
<target>Cantidad</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>Precio total</target>
|
<target>Precio total</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
|
|||||||
@@ -686,10 +686,6 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>Générer des factures à partir des fiches de temps.</target>
|
<target>Générer des factures à partir des fiches de temps.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>Pour générer une facture, vous devez sélectionner au moins un client.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="button.print">
|
<trans-unit id="button.print">
|
||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
<target>Imprimer</target>
|
<target>Imprimer</target>
|
||||||
@@ -738,12 +734,12 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>Service date</target>
|
<target>Service date</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>Montant</target>
|
<target>Montant</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>Total price</target>
|
<target>Total price</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
|
|||||||
@@ -700,10 +700,6 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>Számlák készítése az időbejegyzéseidből</target>
|
<target>Számlák készítése az időbejegyzéseidből</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>Nem található keresési feltételeknek megfelelő számla.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="invoice.preview">
|
<trans-unit id="invoice.preview">
|
||||||
<source>invoice.preview</source>
|
<source>invoice.preview</source>
|
||||||
|
|
||||||
@@ -757,16 +753,16 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>Teljesítés időpontja</target>
|
<target>Teljesítés időpontja</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>Mennyiség</target>
|
<target>Mennyiség</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>Teljes ár</target>
|
<target>Teljes ár</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.unit_price">
|
<trans-unit id="label.unit_price">
|
||||||
<source>invoice.unit_price</source>
|
<source>label.unit_price</source>
|
||||||
<target>Egységár</target>
|
<target>Egységár</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
|
|||||||
@@ -631,10 +631,6 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>Crea fattura dalle registrazioni.</target>
|
<target>Crea fattura dalle registrazioni.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>Prima di creare una fattura, devi selezionare almeno un cliente.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="button.print">
|
<trans-unit id="button.print">
|
||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
<target>Stampa</target>
|
<target>Stampa</target>
|
||||||
@@ -683,12 +679,12 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>Data di servizio</target>
|
<target>Data di servizio</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>Quantità</target>
|
<target>Quantità</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>Prezzo totale</target>
|
<target>Prezzo totale</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
|
|||||||
@@ -702,14 +702,9 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>Criar faturas a partir de suas entradas de quadro de horários registradas.</target>
|
<target>Criar faturas a partir de suas entradas de quadro de horários registradas.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>Nenhuma entrada de fatura foi encontrada com base nos filtros selecionados.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="invoice.preview">
|
<trans-unit id="invoice.preview">
|
||||||
<source>invoice.preview</source>
|
<source>invoice.preview</source>
|
||||||
<target>Esta é uma prévia dos dados que serão exibidos no documento da fatura
|
<target>Esta é uma prévia dos dados que serão exibidos no documento da fatura.</target>
|
||||||
.</target>
|
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="button.print">
|
<trans-unit id="button.print">
|
||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
@@ -759,16 +754,16 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>Data de serviço</target>
|
<target>Data de serviço</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>Quantidade</target>
|
<target>Quantidade</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>Preço Total</target>
|
<target>Preço Total</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.unit_price">
|
<trans-unit id="label.unit_price">
|
||||||
<source>invoice.unit_price</source>
|
<source>label.unit_price</source>
|
||||||
<target>Preço unitário</target>
|
<target>Preço unitário</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
|
|||||||
@@ -677,10 +677,6 @@
|
|||||||
<source>invoice.subtitle</source>
|
<source>invoice.subtitle</source>
|
||||||
<target>Создайте счета для записанных периодов времени.</target>
|
<target>Создайте счета для записанных периодов времени.</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.select_filter">
|
|
||||||
<source>invoice.select_filter</source>
|
|
||||||
<target>Прежде чем создать счет, необходимо выбрать фильтр сортировки клиентов.</target>
|
|
||||||
</trans-unit>
|
|
||||||
<trans-unit id="button.print">
|
<trans-unit id="button.print">
|
||||||
<source>button.print</source>
|
<source>button.print</source>
|
||||||
<target>Печать</target>
|
<target>Печать</target>
|
||||||
@@ -729,12 +725,12 @@
|
|||||||
<source>invoice.service_date</source>
|
<source>invoice.service_date</source>
|
||||||
<target>Дата выполненной работы</target>
|
<target>Дата выполненной работы</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.amount">
|
<trans-unit id="label.amount">
|
||||||
<source>invoice.amount</source>
|
<source>label.amount</source>
|
||||||
<target>Количество</target>
|
<target>Количество</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="invoice.total_rate">
|
<trans-unit id="label.total_rate">
|
||||||
<source>invoice.total_rate</source>
|
<source>label.total_rate</source>
|
||||||
<target>Общая стоимость</target>
|
<target>Общая стоимость</target>
|
||||||
</trans-unit>
|
</trans-unit>
|
||||||
<trans-unit id="label.payment_terms">
|
<trans-unit id="label.payment_terms">
|
||||||
|
|||||||
Binary file not shown.
@@ -7,8 +7,9 @@ and we will add it as soon as possible.
|
|||||||
|
|
||||||
## User manual
|
## User manual
|
||||||
|
|
||||||
- [Invoices](invoices.md) - export your data to different formats with customizable templates
|
|
||||||
- [Timesheets](timesheet.md) - information about timesheets
|
- [Timesheets](timesheet.md) - information about timesheets
|
||||||
|
- [Export](export.md) - export filtered timesheet data
|
||||||
|
- [Invoices](invoices.md) - create invoices from timesheet data in different formats with customizable templates
|
||||||
- [User and Security](users.md) - docu for user and security topics, like authentication, registration and roles
|
- [User and Security](users.md) - docu for user and security topics, like authentication, registration and roles
|
||||||
|
|
||||||
## Admins and Developers
|
## Admins and Developers
|
||||||
|
|||||||
@@ -188,10 +188,10 @@ In the config `kimai.invoice.documents`, you can add a list of directories with
|
|||||||
|
|
||||||
## Adding invoice calculator
|
## Adding invoice calculator
|
||||||
|
|
||||||
An invoice calculator is a class extending `App\Invoice\CalculatorInterface` and is responsible for calculating
|
An invoice calculator is a class implementing `App\Invoice\CalculatorInterface` and it is responsible for calculating
|
||||||
invoice rates, taxes and taking care of all timesheet entries that should be displayed.
|
invoice rates, taxes and taking care of all timesheet entries that should be displayed.
|
||||||
|
|
||||||
Every invoice calculator class will be picked up when refreshing the application cache by the [InvoiceServiceCompilerPass](https://github.com/kevinpapst/kimai2/blob/master/src/DependencyInjection/Compiler/InvoiceServiceCompilerPass.php):
|
Every invoice calculator class will be automatically available when refreshing the application cache by the [InvoiceServiceCompilerPass](../../src/DependencyInjection/Compiler/InvoiceServiceCompilerPass.php):
|
||||||
|
|
||||||
The ID of the calculator must be unique, please prefix it with your vendor or bundle name and make sure it only contains
|
The ID of the calculator must be unique, please prefix it with your vendor or bundle name and make sure it only contains
|
||||||
character as it will be stored in a database column.
|
character as it will be stored in a database column.
|
||||||
@@ -200,16 +200,30 @@ Translations are stored in the `invoice-calculator.xx.xliff`.
|
|||||||
|
|
||||||
## Adding invoice-number generator
|
## Adding invoice-number generator
|
||||||
|
|
||||||
An invoice-number generator is a class extending `App\Invoice\NumberGeneratorInterface` and its only task is to generate
|
An invoice-number generator is a class implementing `App\Invoice\NumberGeneratorInterface` and its only task is to generate
|
||||||
a number for the invoice. In most cases you do not want to mix multiple invoice-number generators throughout your invoices.
|
a number for the invoice. In most cases you do not want to mix multiple invoice-number generators throughout your invoices.
|
||||||
|
|
||||||
Every invoice number-generator class will be picked up when refreshing the application cache by the [InvoiceServiceCompilerPass](https://github.com/kevinpapst/kimai2/blob/master/src/DependencyInjection/Compiler/InvoiceServiceCompilerPass.php):
|
Every invoice number-generator class will be automatically available up when refreshing the application cache by the [InvoiceServiceCompilerPass](../../src/DependencyInjection/Compiler/InvoiceServiceCompilerPass.php):
|
||||||
|
|
||||||
The ID of the number generator must be unique, please prefix it with your vendor or bundle name and make sure it only contains
|
The ID of the number generator must be unique, please prefix it with your vendor or bundle name and make sure it only contains
|
||||||
character as it will be stored in a database column.
|
character as it will be stored in a database column.
|
||||||
|
|
||||||
Translations are stored in the `invoice-numbergenerator.xx.xliff`.
|
Translations are stored in the `invoice-numbergenerator.xx.xliff`.
|
||||||
|
|
||||||
|
## Adding invoice renderer
|
||||||
|
|
||||||
|
An invoice renderer is a class implementing `App\Invoice\RendererInterface` and it is responsible to convert an `InvoiceModel` (the actual data)
|
||||||
|
with the use of an `InvoiceDocument` (the template file) into a downloadable/printable document.
|
||||||
|
|
||||||
|
Every invoice renderer class will be automatically available when refreshing the application cache by the [InvoiceServiceCompilerPass](../../src/DependencyInjection/Compiler/InvoiceServiceCompilerPass.php):
|
||||||
|
|
||||||
|
## Adding export renderer
|
||||||
|
|
||||||
|
An export renderer is a class implementing `App\Export\RendererInterface` and it is responsible to convert ar array of `Timesheet` objects
|
||||||
|
into a downloadable/printable document.
|
||||||
|
|
||||||
|
Every export renderer class will be automatically available when refreshing the application cache by the [ExportServiceCompilerPass](../../src/DependencyInjection/Compiler/ExportServiceCompilerPass.php):
|
||||||
|
|
||||||
## Adding timesheet calculator
|
## Adding timesheet calculator
|
||||||
|
|
||||||
A timesheet calculator will be called on stopped timesheet records. It can rewrite all values but will normally take care
|
A timesheet calculator will be called on stopped timesheet records. It can rewrite all values but will normally take care
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user