diff --git a/config/packages/kimai.yaml b/config/packages/kimai.yaml
index d4152ee6..c713587e 100644
--- a/config/packages/kimai.yaml
+++ b/config/packages/kimai.yaml
@@ -62,7 +62,7 @@ kimai:
INVOICE: [view_invoice,create_invoice]
INVOICE_TEMPLATE: [view_invoice_template,create_invoice_template,edit_invoice_template,delete_invoice_template]
TIMESHEET: [view_own_timesheet,start_own_timesheet,stop_own_timesheet,create_own_timesheet,edit_own_timesheet,export_own_timesheet,delete_own_timesheet]
- TIMESHEET_OTHER: [view_other_timesheet,start_other_timesheet,stop_other_timesheet,create_other_timesheet,edit_other_timesheet,delete_other_timesheet]
+ TIMESHEET_OTHER: [view_other_timesheet,start_other_timesheet,stop_other_timesheet,create_other_timesheet,edit_other_timesheet,export_other_timesheet,delete_other_timesheet]
PROFILE: [view_own_profile,edit_own_profile,password_own_profile,preferences_own_profile,api-token_own_profile]
PROFILE_OTHER: [view_other_profile,edit_other_profile,delete_other_profile,password_other_profile,roles_other_profile,preferences_other_profile,api-token_other_profile]
USER: [view_user,create_user,delete_user]
diff --git a/src/Controller/Admin/TimesheetController.php b/src/Controller/Admin/TimesheetController.php
index b35ab296..3e48ccca 100644
--- a/src/Controller/Admin/TimesheetController.php
+++ b/src/Controller/Admin/TimesheetController.php
@@ -78,6 +78,39 @@ class TimesheetController extends AbstractController
]);
}
+ /**
+ * @Route(path="/export", name="admin_timesheet_export", methods={"GET"})
+ *
+ * @param Request $request
+ * @return \Symfony\Component\HttpFoundation\Response
+ */
+ public function exportAction(Request $request)
+ {
+ $query = new TimesheetQuery();
+ $query->setOrder(TimesheetQuery::ORDER_ASC);
+
+ $form = $this->getToolbarForm($query);
+ $form->handleRequest($request);
+ if ($form->isSubmitted() && $form->isValid()) {
+ /** @var TimesheetQuery $query */
+ $query = $form->getData();
+ if (null !== $query->getBegin()) {
+ $query->getBegin()->setTime(0, 0, 0);
+ }
+ if (null !== $query->getEnd()) {
+ $query->getEnd()->setTime(23, 59, 59);
+ }
+ }
+
+ /* @var $entries Pagerfanta */
+ $entries = $this->getRepository()->findByQuery($query);
+
+ return $this->render('admin/timesheet_export.html.twig', [
+ 'entries' => $entries,
+ 'query' => $query,
+ ]);
+ }
+
/**
* @Route(path="/{id}/stop", name="admin_timesheet_stop", methods={"GET"})
* @Security("is_granted('stop', entry)")
diff --git a/src/Controller/TimesheetController.php b/src/Controller/TimesheetController.php
index f7674a77..2a13a62b 100644
--- a/src/Controller/TimesheetController.php
+++ b/src/Controller/TimesheetController.php
@@ -88,6 +88,7 @@ class TimesheetController extends AbstractController
public function exportAction(Request $request)
{
$query = new TimesheetQuery();
+ $query->setOrder(TimesheetQuery::ORDER_ASC);
$form = $this->getToolbarForm($query);
$form->handleRequest($request);
diff --git a/src/Voter/TimesheetVoter.php b/src/Voter/TimesheetVoter.php
index efa21201..c097fea2 100644
--- a/src/Voter/TimesheetVoter.php
+++ b/src/Voter/TimesheetVoter.php
@@ -23,6 +23,7 @@ class TimesheetVoter extends AbstractVoter
public const STOP = 'stop';
public const EDIT = 'edit';
public const DELETE = 'delete';
+ public const EXPORT = 'export';
public const VIEW_RATE = 'view_rate';
public const EDIT_RATE = 'edit_rate';
@@ -34,6 +35,7 @@ class TimesheetVoter extends AbstractVoter
self::STOP,
self::EDIT,
self::DELETE,
+ self::EXPORT,
self::VIEW_RATE,
self::EDIT_RATE,
];
@@ -89,6 +91,7 @@ class TimesheetVoter extends AbstractVoter
case self::STOP:
case self::EDIT:
case self::DELETE:
+ case self::EXPORT:
$permission .= $attribute;
break;
diff --git a/templates/admin/timesheet.html.twig b/templates/admin/timesheet.html.twig
index de8b42d4..0c42936f 100644
--- a/templates/admin/timesheet.html.twig
+++ b/templates/admin/timesheet.html.twig
@@ -6,7 +6,11 @@
{% block page_title %}{{ 'admin_timesheet.title'|trans }}{% endblock %}
{% block page_subtitle %}{{ 'admin_timesheet.subtitle'|trans }}{% endblock %}
{% block page_actions %}
- {% set actions = {'filter': '#collapseTimesheetAdmin', 'visibility': '#modal_timesheet_admin'} %}
+ {% set actions = {'filter': '#collapseTimesheetAdmin'} %}
+ {% if is_granted('export_own_timesheet') %}
+ {% set actions = actions|merge({'download': 'onclick:return exportTimesheet()'}) %}
+ {% endif %}
+ {% set actions = actions|merge({'visibility': '#modal_timesheet_admin'}) %}
{% if is_granted('create_other_timesheet') %}
{% set actions = actions|merge({'create': path('admin_timesheet_create')}) %}
{% endif %}
@@ -98,3 +102,17 @@
{{ tables.data_table_footer(entries, 'admin_timesheet_paginated') }}
{% endblock %}
+
+{% block javascripts %}
+ {{ parent() }}
+
+{% endblock %}
diff --git a/templates/admin/timesheet_export.html.twig b/templates/admin/timesheet_export.html.twig
new file mode 100644
index 00000000..02b90e96
--- /dev/null
+++ b/templates/admin/timesheet_export.html.twig
@@ -0,0 +1,78 @@
+{% import "macros/widgets.html.twig" as widgets %}
+{% extends 'invoice/layout.html.twig' %}
+
+{% block invoice %}
+
+
+
+
+
+
+
+
+ | {{ 'label.date'|trans }} |
+ {% if query.user is empty %}
+ {{ 'label.username'|trans }} |
+ {% endif %}
+ {{ 'label.description'|trans }} |
+ {{ 'label.hours'|trans }} |
+
+
+
+ {% set timeWorked = 0 %}
+ {% for entry in entries %}
+ {% set timeWorked = timeWorked + entry.duration %}
+
+ | {{ entry.begin|date_short }} |
+ {% if query.user is empty %}
+ {{ widgets.username(entry.user) }} |
+ {% endif %}
+
+ {% if entry.description is not empty %}
+
+ {{ entry.description|desc2html }}
+
+ {% endif %}
+
+ {{ 'label.activity'|trans }}: {{ entry.activity.name }} |
+ {{ 'label.project'|trans }}: {{ entry.project.name }} |
+ {{ 'label.customer'|trans }}: {{ entry.project.customer.name }}
+
+ |
+ {{ entry.duration|duration }} |
+
+ {% endfor %}
+
+
+
+ |
+ {{ 'invoice.total_working_time'|trans }} |
+ {{ timeWorked|duration }} |
+
+
+
+
+
+
+{% endblock %}
+
+{% block print_button %}{% endblock %}
\ No newline at end of file
diff --git a/tests/Controller/Admin/ActivityControllerTest.php b/tests/Controller/Admin/ActivityControllerTest.php
index 8065438d..7c16cfa3 100644
--- a/tests/Controller/Admin/ActivityControllerTest.php
+++ b/tests/Controller/Admin/ActivityControllerTest.php
@@ -44,12 +44,20 @@ class ActivityControllerTest extends ControllerBaseTest
$this->assertNull($form->get('activity_edit_form[create_more]')->getValue());
$client->submit($form, [
'activity_edit_form' => [
- 'name' => 'Test 2',
+ 'name' => 'An AcTiVitY Name',
+ 'project' => '1',
]
]);
$this->assertIsRedirect($client, $this->createUrl('/admin/activity/'));
$client->followRedirect();
$this->assertHasDataTable($client);
+
+ $this->request($client, '/admin/activity/2/edit');
+ $editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
+ $this->assertEquals('An AcTiVitY Name', $editForm->get('activity_edit_form[name]')->getValue());
+ // make sure customer and project are pre-selected for none global activities
+ $this->assertEquals('1', $editForm->get('activity_edit_form[project]')->getValue());
+ $this->assertEquals('1', $editForm->get('activity_edit_form[customer]')->getValue());
}
public function testCreateActionWithCreateMore()
@@ -101,6 +109,9 @@ class ActivityControllerTest extends ControllerBaseTest
$this->request($client, '/admin/activity/1/edit');
$editForm = $client->getCrawler()->filter('form[name=activity_edit_form]')->form();
$this->assertEquals('Test 2', $editForm->get('activity_edit_form[name]')->getValue());
+ // make sure no customer or project is pre-selected for global activities
+ $this->assertEquals('', $editForm->get('activity_edit_form[customer]')->getValue());
+ $this->assertEquals('', $editForm->get('activity_edit_form[project]')->getValue());
}
public function testDeleteAction()
diff --git a/tests/Controller/Admin/TimesheetControllerTest.php b/tests/Controller/Admin/TimesheetControllerTest.php
index eebdb40c..17e4626a 100644
--- a/tests/Controller/Admin/TimesheetControllerTest.php
+++ b/tests/Controller/Admin/TimesheetControllerTest.php
@@ -33,7 +33,7 @@ class TimesheetControllerTest extends ControllerBaseTest
$this->assertHasDataTable($client);
$result = $client->getCrawler()->filter('div.breadcrumb div.box-tools div.btn-group a.btn');
- $this->assertEquals(3, count($result));
+ $this->assertEquals(4, count($result));
foreach ($result as $item) {
$this->assertEquals('btn btn-default', $item->getAttribute('class'));
@@ -71,6 +71,44 @@ class TimesheetControllerTest extends ControllerBaseTest
// TODO more assertions
}
+ public function testExportAction()
+ {
+ $client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
+
+ $em = $client->getContainer()->get('doctrine.orm.entity_manager');
+ $fixture = new TimesheetFixtures();
+ $fixture->setAmount(7);
+ $fixture->setUser($this->getUserByRole($em, User::ROLE_USER));
+ $fixture->setStartDate(new \DateTime('-10 days'));
+ $this->importFixture($em, $fixture);
+ $fixture = new TimesheetFixtures();
+ $fixture->setAmount(3);
+ $fixture->setUser($this->getUserByRole($em, User::ROLE_TEAMLEAD));
+ $fixture->setStartDate(new \DateTime('-10 days'));
+ $this->importFixture($em, $fixture);
+
+ $this->request($client, '/team/timesheet/');
+ $this->assertTrue($client->getResponse()->isSuccessful());
+
+ $form = $client->getCrawler()->filter('form.navbar-form')->form();
+ $form->getFormNode()->setAttribute('action', $this->createUrl('/team/timesheet/export'));
+ $client->submit($form, [
+ 'state' => 1,
+ 'pageSize' => 25,
+ 'begin' => (new \DateTime('-10 days'))->format('Y-m-d'),
+ 'end' => (new \DateTime())->format('Y-m-d'),
+ 'customer' => null,
+ ]);
+
+ $this->assertTrue($client->getResponse()->isSuccessful());
+
+ $node = $client->getCrawler()->filter('body');
+ $this->assertEquals('invoice_print', $node->getNode(0)->getAttribute('class'));
+
+ $result = $node->filter('section.invoice table.table tbody tr');
+ $this->assertEquals(10, count($result));
+ }
+
public function testCreateAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);
diff --git a/tests/Voter/AbstractVoterTest.php b/tests/Voter/AbstractVoterTest.php
index 4b052387..207cca30 100644
--- a/tests/Voter/AbstractVoterTest.php
+++ b/tests/Voter/AbstractVoterTest.php
@@ -70,7 +70,7 @@ abstract class AbstractVoterTest extends TestCase
$invoice = ['view_invoice', 'create_invoice'];
$invoiceTemplate = ['view_invoice_template', 'create_invoice_template', 'edit_invoice_template', 'delete_invoice_template'];
$timesheet = ['view_own_timesheet', 'start_own_timesheet', 'stop_own_timesheet', 'create_own_timesheet', 'edit_own_timesheet', 'export_own_timesheet', 'delete_own_timesheet'];
- $timesheetOthers = ['view_other_timesheet', 'start_other_timesheet', 'stop_other_timesheet', 'create_other_timesheet', 'edit_other_timesheet', 'delete_other_timesheet'];
+ $timesheetOthers = ['view_other_timesheet', 'start_other_timesheet', 'stop_other_timesheet', 'create_other_timesheet', 'edit_other_timesheet', 'export_other_timesheet', 'delete_other_timesheet'];
$profile = ['view_own_profile', 'edit_own_profile', 'password_own_profile', 'preferences_own_profile', 'api-token_own_profile'];
$profileOther = ['view_other_profile', 'edit_other_profile', 'delete_other_profile', 'password_other_profile', 'roles_other_profile', 'preferences_other_profile', 'api-token_other_profile'];
$user = ['view_user', 'create_user', 'delete_user'];
diff --git a/tests/Voter/TimesheetVoterTest.php b/tests/Voter/TimesheetVoterTest.php
index 3274b97c..2ed5d6b1 100644
--- a/tests/Voter/TimesheetVoterTest.php
+++ b/tests/Voter/TimesheetVoterTest.php
@@ -62,6 +62,7 @@ class TimesheetVoterTest extends AbstractVoterTest
yield [$timeEntry[0], $timeEntry[1], 'stop', $result];
yield [$timeEntry[0], $timeEntry[1], 'edit', $result];
yield [$timeEntry[0], $timeEntry[1], 'delete', $result];
+ yield [$timeEntry[0], $timeEntry[1], 'export', $result];
}
$result = VoterInterface::ACCESS_DENIED;
@@ -73,6 +74,7 @@ class TimesheetVoterTest extends AbstractVoterTest
yield [$timeEntry[0], $timeEntry[1], 'stop', $result];
yield [$timeEntry[0], $timeEntry[1], 'edit', $result];
yield [$timeEntry[0], $timeEntry[1], 'delete', $result];
+ yield [$timeEntry[0], $timeEntry[1], 'export', $result];
}
}
diff --git a/var/docs/permissions.md b/var/docs/permissions.md
index 4a5123ff..f9be79b2 100644
--- a/var/docs/permissions.md
+++ b/var/docs/permissions.md
@@ -71,30 +71,31 @@ The permission-names were chosen to be self-explanatory. In the hope that it wor
| stop_own_timesheet | TIMESHEET | | - |
| create_own_timesheet | TIMESHEET | X | - |
| edit_own_timesheet | TIMESHEET | | - |
-| export_own_timesheet | TIMESHEET | | - |
+| export_own_timesheet | TIMESHEET | | export your own timesheet in the timesheet panel |
| delete_own_timesheet | TIMESHEET | | - |
| view_other_timesheet | TIMESHEET_OTHER | | allows access to the complete timesheet view |
| start_other_timesheet | TIMESHEET_OTHER | | - |
| stop_other_timesheet | TIMESHEET_OTHER | | - |
| create_other_timesheet | TIMESHEET_OTHER | | - |
| edit_other_timesheet | TIMESHEET_OTHER | | - |
+| export_other_timesheet | TIMESHEET | | export timesheet in the timesheet admin panel |
| delete_other_timesheet | TIMESHEET_OTHER | | - |
| view_rate_own_timesheet | RATE | | - |
| edit_rate_own_timesheet | RATE | | - |
| view_rate_other_timesheet | RATE_OTHER | | - |
| edit_rate_other_timesheet | RATE_OTHER | | - |
-| view_own_profile | PROFILE | | Allows access to the own profile view. Without this permission, users cannot access any of their profile settings or passwords ... |
-| edit_own_profile | PROFILE | | - |
-| delete_own_profile | PROFILE | | - |
-| password_own_profile | PROFILE | | - |
-| roles_own_profile | PROFILE | | - |
-| preferences_own_profile | PROFILE | | - |
-| api-token_own_profile | PROFILE | | - |
+| view_own_profile | PROFILE | | allows access to the own profile view - without this permission, users cannot access any of their profile settings or passwords ... |
+| edit_own_profile | PROFILE | | grants access to edit the own profile |
+| delete_own_profile | PROFILE | | grants access to delete the own profile |
+| password_own_profile | PROFILE | | grants access to change the own password |
+| roles_own_profile | PROFILE | | SECURITY ALERT: grants access to the own roles |
+| preferences_own_profile | PROFILE | | grants access to the own preferences |
+| api-token_own_profile | PROFILE | | grants access to change the own API token |
| view_other_profile | PROFILE_OTHER | | - |
| edit_other_profile | PROFILE_OTHER | | - |
| delete_other_profile | PROFILE_OTHER | | - |
| password_other_profile | PROFILE_OTHER | | allows to change the password for another user |
-| roles_other_profile | PROFILE_OTHER | | allows to change roles for other users |
+| roles_other_profile | PROFILE_OTHER | | SECURITY ALERT: allows to change roles for other users |
| preferences_other_profile | PROFILE_OTHER | | allows to change the preferences for another user |
| api-token_other_profile | PROFILE_OTHER | | allows to set the API login token for other users |
| hourly-rate_own_profile | - | | allows to edit the own user specific hourly rate |