@@ -109,8 +109,8 @@
</td>
<td class="w-min text-center">
{{ widgets.action_button('show', {'url': '#invoice_preview_details_' ~ model.customer.id, 'title': 'timesheet.all'|trans, 'class': 'btn btn-sm hidden-xs hidden-sm'}, 'link') }}
{{ widgets.action_button('print', {'url': '#', 'onclick': 'return singleInvoice(this)', 'title': 'button.preview'|trans, 'target': '_blank', 'class': 'btn btn-sm', 'attr': {'data-href': path('invoice_preview', {'customer': model.customer.id, 'template': model.template.id})}}) }}
{{ widgets.action_button('save', {'url': '#', 'onclick': 'return singleInvoice(this)', 'title': 'action.save'|trans, 'class': 'btn btn-sm', 'attr': {'data-href': path('invoice_create', {'customer': model.customer.id, 'template': model.template.id})}}, 'success') }}
{{ widgets.action_button('print', {'url': '#', 'onclick': 'return singleInvoice(this)', 'title': 'button.preview'|trans, 'target': '_blank', 'class': 'btn btn-sm', 'attr': {'data-customer': model.customer.id, 'data-template': model.template.id, 'data-href': path('invoice_preview', {'customer': model.customer.id})}}) }}
{{ widgets.action_button('save', {'url': '#', 'onclick': 'return singleInvoice(this)', 'title': 'action.save'|trans, 'class': 'btn btn-sm', 'attr': {'data-customer': model.customer.id, 'data-template': model.template.id, 'data-href': path('invoice_create', {'customer': model.customer.id, 'template': model.template.id})}}, 'success') }}
</td>
<td class="w-min text-right hidden-xs">
{{ model.calculator.timeWorked|duration(isDecimal) }}
@@ -214,20 +214,20 @@
function singleInvoice(link)
{
let formPlugin = kimai.getPlugin('form');
let data = formPlugin.getFormData(document.getElementById('{{ formId }}'));
let uri = formPlugin.convertFormDataToQueryString(data);
let baseUrl = link.getAttribute('data-href');
link.href = baseUrl + '?' + uri;
const formPlugin = kimai.getPlugin('form');
const overwrites = {'customers[]': link.dataset['customer'], 'template': link.dataset['template']};
const uri = formPlugin.convertFormDataToQueryString(document.getElementById('{{ formId }}'), overwrites);
link.href = link.dataset['href'] + '?' + uri;
return true;
}
function saveAllInvoices(link)
{
let formPlugin = kimai.getPlugin('form');
let data = formPlugin.getFormData(document.getElementById('{{ formId }}'));
let uri = formPlugin.convertFormDataToQueryString(data);
const formPlugin = kimai.getPlugin('form');
const uri = formPlugin.convertFormDataToQueryString(document.getElementById('{{ formId }}'));
link.href = '{{ path('invoice') }}?createInvoice=true&' + uri;
return true;
@@ -207,7 +207,7 @@ public function testCreateAction()
}
}

public function testPrintAction()
public function testPreviewAction()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_TEAMLEAD);

@@ -233,17 +233,19 @@ public function testPrintAction()
$params = [
'daterange' => $dateRange,
'projects' => [1],
'template' => $id,
'customers[]' => 1
];

$action = '/invoice/preview/1/' . $id . '?' . http_build_query($params);
$action = '/invoice/preview/1?' . http_build_query($params);
$this->request($client, $action);
$this->assertTrue($client->getResponse()->isSuccessful());
$node = $client->getCrawler()->filter('body');
$this->assertEquals(1, $node->count());
$this->assertEquals('invoice_print', $node->getIterator()[0]->getAttribute('class'));
}

public function testCreateActionAsAdminWithDownloadAndStatusChangeAndDelete()
public function testCreateActionAsAdminWithDownloadAndStatusChange()
{
$client = $this->getClientForAuthenticatedUser(User::ROLE_ADMIN);

@@ -352,12 +354,6 @@ public function testCreateActionAsAdminWithDownloadAndStatusChangeAndDelete()
$this->assertIsRedirect($client, '/invoice/show');
$client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());

// this does not delete the invoice, because the token is wrong
$this->request($client, '/invoice/delete/' . $id . '/fghfkjhgkjhg');
$this->assertIsRedirect($client, '/invoice/show');
$client->followRedirect();
$this->assertTrue($client->getResponse()->isSuccessful());
}

public function testEditTemplateAction()
@@ -25,6 +25,9 @@ public function testQuery()
$this->assertBaseQuery($sut);

$customer = new Customer();
self::assertFalse($sut->isAllowCustomerPreselect());
$sut->setAllowCustomerPreselect(true);
self::assertTrue($sut->isAllowCustomerPreselect());
self::assertNull($sut->getCustomerToIgnore());
self::assertInstanceOf(CustomerFormTypeQuery::class, $sut->setCustomerToIgnore($customer));
self::assertSame($customer, $sut->getCustomerToIgnore());
@@ -118,4 +118,50 @@ public function testTeamMember()

$this->assertVote($user, $customer, 'edit', VoterInterface::ACCESS_GRANTED);
}

public function testAccess()
{
// ALLOW: customer has no teams
$this->assertVote(new User(), new Customer(), 'access', VoterInterface::ACCESS_GRANTED);

// ALLOW: customer has no teams
$user = new User();
$user->addTeam(new Team());
$this->assertVote($user, new Customer(), 'access', VoterInterface::ACCESS_GRANTED);

// ALLOW: user and customer are in the same team (as teamlead)
$team = new Team();
$user = new User();
$team->addTeamlead($user);

$customer = new Customer();
$customer->addTeam($team);

$this->assertVote($user, $customer, 'access', VoterInterface::ACCESS_GRANTED);

// ALLOW: user and customer are in the same team (as member)
$team = new Team();
$user = new User();
$user->addTeam(new Team());
$user->addTeam($team);

$customer = new Customer();
$customer->addTeam($team);

$this->assertVote($user, $customer, 'access', VoterInterface::ACCESS_GRANTED);

// DENY: customer has a team, user not
$customer = new Customer();
$customer->addTeam(new Team());

$this->assertVote(new User(), $customer, 'access', VoterInterface::ACCESS_DENIED);

// DENY: user and customer has a team are not in the same team
$user = new User();
$user->addTeam(new Team());
$customer = new Customer();
$customer->addTeam(new Team());

$this->assertVote($user, $customer, 'access', VoterInterface::ACCESS_DENIED);
}
}