action to duplicate timesheet record (#1478)
This commit is contained in:
@@ -656,6 +656,47 @@ class TimesheetController extends BaseApiController
|
|||||||
return $this->viewHandler->handle($view);
|
return $this->viewHandler->handle($view);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Duplicates an existing timesheet record
|
||||||
|
*
|
||||||
|
* @SWG\Response(
|
||||||
|
* response=200,
|
||||||
|
* description="Duplicates a timesheet record, resetting the export state only.",
|
||||||
|
* @SWG\Schema(ref="#/definitions/TimesheetEntity")
|
||||||
|
* )
|
||||||
|
* @SWG\Parameter(
|
||||||
|
* name="id",
|
||||||
|
* in="path",
|
||||||
|
* type="integer",
|
||||||
|
* description="Timesheet record ID to duplicate",
|
||||||
|
* required=true,
|
||||||
|
* )
|
||||||
|
*
|
||||||
|
* @ApiSecurity(name="apiUser")
|
||||||
|
* @ApiSecurity(name="apiToken")
|
||||||
|
*/
|
||||||
|
public function duplicateAction(int $id): Response
|
||||||
|
{
|
||||||
|
$timesheet = $this->repository->find($id);
|
||||||
|
|
||||||
|
if (null === $timesheet) {
|
||||||
|
throw new NotFoundException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!$this->isGranted('duplicate', $timesheet)) {
|
||||||
|
throw new AccessDeniedHttpException('You are not allowed to duplicate this timesheet');
|
||||||
|
}
|
||||||
|
|
||||||
|
$copyTimesheet = clone $timesheet;
|
||||||
|
|
||||||
|
$this->service->saveNewTimesheet($copyTimesheet);
|
||||||
|
|
||||||
|
$view = new View($copyTimesheet, 200);
|
||||||
|
$view->getContext()->setGroups(['Default', 'Entity', 'Timesheet']);
|
||||||
|
|
||||||
|
return $this->viewHandler->handle($view);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Switch the export state of a timesheet record to (un-)lock it
|
* Switch the export state of a timesheet record to (un-)lock it
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
|||||||
public const CATEGORY_WORK = 'work';
|
public const CATEGORY_WORK = 'work';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @var int
|
* @var int|null
|
||||||
*
|
*
|
||||||
* @ORM\Column(name="id", type="integer")
|
* @ORM\Column(name="id", type="integer")
|
||||||
* @ORM\Id
|
* @ORM\Id
|
||||||
@@ -537,4 +537,12 @@ class Timesheet implements EntityWithMetaFields, ExportItemInterface
|
|||||||
|
|
||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function __clone()
|
||||||
|
{
|
||||||
|
if ($this->id) {
|
||||||
|
$this->id = null;
|
||||||
|
$this->exported = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
self::VIEW_RATE,
|
self::VIEW_RATE,
|
||||||
self::EDIT_RATE,
|
self::EDIT_RATE,
|
||||||
self::EDIT_EXPORT,
|
self::EDIT_EXPORT,
|
||||||
|
'duplicate'
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -99,6 +100,10 @@ class TimesheetVoter extends AbstractVoter
|
|||||||
$permission .= $attribute;
|
$permission .= $attribute;
|
||||||
break;
|
break;
|
||||||
|
|
||||||
|
case 'duplicate':
|
||||||
|
$permission = self::EDIT;
|
||||||
|
break;
|
||||||
|
|
||||||
case self::VIEW_RATE:
|
case self::VIEW_RATE:
|
||||||
case self::EDIT_RATE:
|
case self::EDIT_RATE:
|
||||||
case self::STOP:
|
case self::STOP:
|
||||||
|
|||||||
@@ -39,6 +39,10 @@
|
|||||||
{% set actions = actions|merge({'repeat': {'url': path('restart_timesheet', {'id' : timesheet.id}), 'class': 'api-link', 'attr': {'data-payload': '{"copy": "all"}', 'data-event': 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method': 'PATCH', 'data-msg-error': 'timesheet.start.error', 'data-msg-success': 'timesheet.start.success'}}}) %}
|
{% set actions = actions|merge({'repeat': {'url': path('restart_timesheet', {'id' : timesheet.id}), 'class': 'api-link', 'attr': {'data-payload': '{"copy": "all"}', 'data-event': 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method': 'PATCH', 'data-msg-error': 'timesheet.start.error', 'data-msg-success': 'timesheet.start.success'}}}) %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if is_granted('duplicate', timesheet) %}
|
||||||
|
{% set actions = actions|merge({'copy': {'url': path('duplicate_timesheet', {'id' : timesheet.id}), 'class': 'api-link', 'attr': {'data-payload': '{"copy": "all"}', 'data-event': 'kimai.timesheetStart kimai.timesheetUpdate', 'data-method': 'PATCH', 'data-msg-error': 'timesheet.update.error', 'data-msg-success': 'timesheet.update.success'}}}) %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{% if is_granted('edit', timesheet) %}
|
{% if is_granted('edit', timesheet) %}
|
||||||
{% set class = '' %}
|
{% set class = '' %}
|
||||||
{% if view != 'edit' %}
|
{% if view != 'edit' %}
|
||||||
|
|||||||
@@ -786,6 +786,44 @@ class TimesheetControllerTest extends APIControllerBaseTest
|
|||||||
$this->assertEntityNotFoundForPatch(User::ROLE_USER, '/api/timesheets/42/restart', []);
|
$this->assertEntityNotFoundForPatch(User::ROLE_USER, '/api/timesheets/42/restart', []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function testDuplicateAction()
|
||||||
|
{
|
||||||
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
$data = [
|
||||||
|
'activity' => 1,
|
||||||
|
'project' => 1,
|
||||||
|
'begin' => ($this->dateTime->createDateTime('- 16 hours'))->format('Y-m-d H:m:0'),
|
||||||
|
'end' => ($this->dateTime->createDateTime())->format('Y-m-d H:m:0'),
|
||||||
|
'description' => 'foo',
|
||||||
|
'fixedRate' => 2016,
|
||||||
|
'hourlyRate' => 127
|
||||||
|
];
|
||||||
|
$this->request($client, '/api/timesheets', 'POST', [], json_encode($data));
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$result = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertDefaultStructure($result);
|
||||||
|
$this->assertNotEmpty($result['id']);
|
||||||
|
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
|
||||||
|
$this->assertEquals(2016, $result['rate']);
|
||||||
|
|
||||||
|
$this->request($client, '/api/timesheets/' . $result['id'] . '/duplicate', 'PATCH');
|
||||||
|
$this->assertTrue($client->getResponse()->isSuccessful());
|
||||||
|
|
||||||
|
$result = json_decode($client->getResponse()->getContent(), true);
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertDefaultStructure($result);
|
||||||
|
$this->assertNotEmpty($result['id']);
|
||||||
|
$this->assertTrue($result['duration'] == 57600 || $result['duration'] == 57660); // 1 minute rounding might be applied
|
||||||
|
$this->assertEquals(2016, $result['rate']);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testDuplicateThrowsNotFound()
|
||||||
|
{
|
||||||
|
$this->assertEntityNotFoundForPatch(User::ROLE_ADMIN, '/api/timesheets/11/duplicate', []);
|
||||||
|
}
|
||||||
|
|
||||||
public function testExportAction()
|
public function testExportAction()
|
||||||
{
|
{
|
||||||
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);
|
||||||
|
|||||||
Reference in New Issue
Block a user