added invoice archive & configurable invoice numbers (#1541)
This commit is contained in:
@@ -33,33 +33,6 @@ class ActivityInvoiceCalculatorTest extends AbstractCalculatorTest
|
||||
$this->assertEmptyModel(new ActivityInvoiceCalculator());
|
||||
}
|
||||
|
||||
public function testExceptionNoActivity()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('Cannot work with invoice items that do not have an activity');
|
||||
$timesheet = new Timesheet();
|
||||
|
||||
$sut = new ActivityInvoiceCalculator();
|
||||
$model = $this->getEmptyModel();
|
||||
$model->addEntries([$timesheet]);
|
||||
$sut->setModel($model);
|
||||
$sut->getEntries();
|
||||
}
|
||||
|
||||
public function testExceptionNoId()
|
||||
{
|
||||
$this->expectException('Exception');
|
||||
$this->expectExceptionMessage('Cannot handle un-persisted activities');
|
||||
$timesheet = new Timesheet();
|
||||
$timesheet->setActivity(new Activity());
|
||||
|
||||
$sut = new ActivityInvoiceCalculator();
|
||||
$model = $this->getEmptyModel();
|
||||
$model->addEntries([$timesheet]);
|
||||
$sut->setModel($model);
|
||||
$sut->getEntries();
|
||||
}
|
||||
|
||||
public function testWithMultipleEntries()
|
||||
{
|
||||
$customer = new Customer();
|
||||
@@ -128,7 +101,35 @@ class ActivityInvoiceCalculatorTest extends AbstractCalculatorTest
|
||||
->setActivity($activity3)
|
||||
->setProject((new Project())->setName('bar'));
|
||||
|
||||
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5];
|
||||
$timesheet6 = new Timesheet();
|
||||
$timesheet6
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
->setDuration(0)
|
||||
->setRate(0)
|
||||
->setUser(new User())
|
||||
->setProject((new Project())->setName('bar'));
|
||||
|
||||
$timesheet7 = new Timesheet();
|
||||
$timesheet7
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
->setDuration(0)
|
||||
->setRate(0)
|
||||
->setUser(new User())
|
||||
->setActivity(new Activity())
|
||||
->setProject((new Project())->setName('bar'));
|
||||
|
||||
$timesheet8 = new Timesheet();
|
||||
$timesheet8
|
||||
->setBegin(new \DateTime())
|
||||
->setEnd(new \DateTime())
|
||||
->setDuration(0)
|
||||
->setRate(0)
|
||||
->setUser(new User())
|
||||
->setProject((new Project())->setName('bar'));
|
||||
|
||||
$entries = [$timesheet, $timesheet2, $timesheet3, $timesheet4, $timesheet5, $timesheet6, $timesheet7, $timesheet8];
|
||||
|
||||
$query = new InvoiceQuery();
|
||||
$query->setActivity($activity1);
|
||||
@@ -148,7 +149,7 @@ class ActivityInvoiceCalculatorTest extends AbstractCalculatorTest
|
||||
$this->assertEquals('EUR', $model->getCurrency());
|
||||
$this->assertEquals(2521.12, $sut->getSubtotal());
|
||||
$this->assertEquals(6600, $sut->getTimeWorked());
|
||||
$this->assertEquals(3, count($sut->getEntries()));
|
||||
$this->assertEquals(5, count($sut->getEntries()));
|
||||
|
||||
$entries = $sut->getEntries();
|
||||
$this->assertEquals(404.38, $entries[0]->getRate());
|
||||
|
||||
53
tests/Invoice/InvoiceFilenameTest.php
Normal file
53
tests/Invoice/InvoiceFilenameTest.php
Normal file
@@ -0,0 +1,53 @@
|
||||
<?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\Invoice;
|
||||
|
||||
use App\Entity\Customer;
|
||||
use App\Entity\InvoiceTemplate;
|
||||
use App\Invoice\InvoiceFilename;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\NumberGenerator\DateNumberGenerator;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\InvoiceFilename
|
||||
*/
|
||||
class InvoiceFilenameTest extends TestCase
|
||||
{
|
||||
public function testInvoiceFilename()
|
||||
{
|
||||
$customer = new Customer();
|
||||
$template = new InvoiceTemplate();
|
||||
|
||||
$model = new InvoiceModel(new DebugFormatter());
|
||||
$model->setNumberGenerator(new DateNumberGenerator());
|
||||
$model->setTemplate($template);
|
||||
$model->setCustomer($customer);
|
||||
|
||||
$datePrefix = date('ymd');
|
||||
|
||||
$sut = new InvoiceFilename($model);
|
||||
|
||||
self::assertEquals($datePrefix, $sut->getFilename());
|
||||
self::assertEquals($datePrefix, (string) $sut);
|
||||
|
||||
$customer->setName('foo');
|
||||
$sut = new InvoiceFilename($model);
|
||||
|
||||
self::assertEquals($datePrefix . '-foo', $sut->getFilename());
|
||||
self::assertEquals($datePrefix . '-foo', (string) $sut);
|
||||
|
||||
$customer->setCompany('barß / laölala # ldksjf 123');
|
||||
$sut = new InvoiceFilename($model);
|
||||
|
||||
self::assertEquals($datePrefix . '-barß_laölala_ldksjf123', $sut->getFilename());
|
||||
self::assertEquals($datePrefix . '-barß_laölala_ldksjf123', (string) $sut);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?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\Invoice\NumberGenerator;
|
||||
|
||||
use App\Configuration\SystemConfiguration;
|
||||
use App\Invoice\InvoiceModel;
|
||||
use App\Invoice\NumberGenerator\ConfigurableNumberGenerator;
|
||||
use App\Repository\InvoiceRepository;
|
||||
use App\Tests\Invoice\DebugFormatter;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\NumberGenerator\ConfigurableNumberGenerator
|
||||
*/
|
||||
class ConfigurableNumberGeneratorTest extends TestCase
|
||||
{
|
||||
private function getSut(string $format)
|
||||
{
|
||||
$config = $this->createMock(SystemConfiguration::class);
|
||||
$config->expects($this->any())
|
||||
->method('find')
|
||||
->willReturn($format);
|
||||
|
||||
$repository = $this->createMock(InvoiceRepository::class);
|
||||
$repository
|
||||
->expects($this->any())
|
||||
->method('getCounterForAllTime')
|
||||
->willReturn(1);
|
||||
$repository
|
||||
->expects($this->any())
|
||||
->method('getCounterForYear')
|
||||
->willReturn(1);
|
||||
$repository
|
||||
->expects($this->any())
|
||||
->method('getCounterForMonth')
|
||||
->willReturn(1);
|
||||
$repository
|
||||
->expects($this->any())
|
||||
->method('getCounterForDay')
|
||||
->willReturn(1);
|
||||
|
||||
return new ConfigurableNumberGenerator($repository, $config);
|
||||
}
|
||||
|
||||
public function getTestData()
|
||||
{
|
||||
$timestamp = time();
|
||||
|
||||
return [
|
||||
// simple tests for single calls
|
||||
['{date}', date('ymd'), $timestamp],
|
||||
['{Y}', date('Y'), $timestamp],
|
||||
['{y}', date('y'), $timestamp],
|
||||
['{M}', date('m'), $timestamp],
|
||||
['{m}', date('n'), $timestamp],
|
||||
['{D}', date('d'), $timestamp],
|
||||
['{d}', date('j'), $timestamp],
|
||||
['{c}', '2', $timestamp],
|
||||
['{cy}', '2', $timestamp],
|
||||
['{cm}', '2', $timestamp],
|
||||
['{cd}', '2', $timestamp],
|
||||
// number formatting (not testing the lower case versions, as the tests might break depending on the date)
|
||||
['{date,10}', '0000' . date('ymd'), $timestamp],
|
||||
['{Y,6}', '00' . date('Y'), $timestamp],
|
||||
['{M,3}', '0' . date('m'), $timestamp],
|
||||
['{D,3}', '0' . date('d'), $timestamp],
|
||||
['{c,2}', '02', $timestamp],
|
||||
['{cy,2}', '02', $timestamp],
|
||||
['{cm,2}', '02', $timestamp],
|
||||
['{cd,2}', '02', $timestamp],
|
||||
// mixing identifiers
|
||||
['{Y}{cy}', date('Y') . '2', $timestamp],
|
||||
['{Y}{cy}{m}', date('Y') . '2' . date('n'), $timestamp],
|
||||
['{Y}-{cy}/{m}', date('Y') . '-2/' . date('n'), $timestamp],
|
||||
['{Y}-{cy}/{m}', date('Y') . '-2/' . date('n'), $timestamp],
|
||||
['{Y,5}/{cy,5}', '0' . date('Y') . '/00002', $timestamp],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @dataProvider getTestData
|
||||
*/
|
||||
public function testGetInvoiceNumber(string $format, string $expectedInvoiceNumber, int $timestamp)
|
||||
{
|
||||
$sut = $this->getSut($format);
|
||||
$model = new InvoiceModel(new DebugFormatter());
|
||||
$model->setInvoiceDate((new \DateTime())->setTimestamp($timestamp));
|
||||
$sut->setModel($model);
|
||||
|
||||
$this->assertEquals($expectedInvoiceNumber, $sut->getInvoiceNumber());
|
||||
$this->assertEquals('default', $sut->getId());
|
||||
}
|
||||
}
|
||||
@@ -25,6 +25,6 @@ class DateNumberGeneratorTest extends TestCase
|
||||
$sut->setModel(new InvoiceModel(new DebugFormatter()));
|
||||
|
||||
$this->assertEquals(date('ymd'), $sut->getInvoiceNumber());
|
||||
$this->assertEquals('default', $sut->getId());
|
||||
$this->assertEquals('date', $sut->getId());
|
||||
}
|
||||
}
|
||||
|
||||
65
tests/Invoice/Renderer/PdfRendererTest.php
Normal file
65
tests/Invoice/Renderer/PdfRendererTest.php
Normal file
@@ -0,0 +1,65 @@
|
||||
<?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\Invoice\Renderer;
|
||||
|
||||
use App\Invoice\Renderer\PdfRenderer;
|
||||
use App\Utils\MPdfConverter;
|
||||
use Symfony\Bundle\FrameworkBundle\Test\KernelTestCase;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Twig\Environment;
|
||||
use Twig\Loader\FilesystemLoader;
|
||||
|
||||
/**
|
||||
* @covers \App\Invoice\Renderer\PdfRenderer
|
||||
* @group integration
|
||||
*/
|
||||
class PdfRendererTest extends KernelTestCase
|
||||
{
|
||||
use RendererTestTrait;
|
||||
|
||||
public function testSupports()
|
||||
{
|
||||
$loader = new FilesystemLoader();
|
||||
$env = new Environment($loader);
|
||||
$sut = new PdfRenderer($env, $this->createMock(MPdfConverter::class));
|
||||
$this->assertTrue($sut->supports($this->getInvoiceDocument('default.pdf.twig', true)));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('freelancer.html.twig')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('timesheet.html.twig')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('foo.html.twig')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('company.docx')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('export.csv')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('spreadsheet.xlsx')));
|
||||
$this->assertFalse($sut->supports($this->getInvoiceDocument('open-spreadsheet.ods')));
|
||||
}
|
||||
|
||||
public function testRender()
|
||||
{
|
||||
$kernel = self::bootKernel();
|
||||
/** @var Environment $twig */
|
||||
$twig = $kernel->getContainer()->get('twig');
|
||||
$stack = $kernel->getContainer()->get('request_stack');
|
||||
$cacheDir = $kernel->getContainer()->getParameter('kernel.cache_dir');
|
||||
|
||||
$request = new Request();
|
||||
$request->setLocale('en');
|
||||
$stack->push($request);
|
||||
|
||||
/** @var FilesystemLoader $loader */
|
||||
$loader = $twig->getLoader();
|
||||
$loader->addPath(__DIR__ . '/../templates/', 'invoice');
|
||||
|
||||
$sut = new PdfRenderer($twig, new MPdfConverter($cacheDir));
|
||||
$model = $this->getInvoiceModel();
|
||||
$document = $this->getInvoiceDocument('default.pdf.twig', true);
|
||||
|
||||
$response = $sut->render($document, $model);
|
||||
$this->assertEquals('application/pdf', $response->headers->get('Content-Type'));
|
||||
}
|
||||
}
|
||||
@@ -15,6 +15,7 @@ use App\Invoice\NumberGenerator\DateNumberGenerator;
|
||||
use App\Invoice\Renderer\TwigRenderer;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Repository\InvoiceDocumentRepository;
|
||||
use App\Utils\FileHelper;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
use Twig\Environment;
|
||||
|
||||
@@ -23,10 +24,16 @@ use Twig\Environment;
|
||||
*/
|
||||
class ServiceInvoiceTest extends TestCase
|
||||
{
|
||||
private function getSut(array $paths): ServiceInvoice
|
||||
{
|
||||
$repo = new InvoiceDocumentRepository($paths);
|
||||
|
||||
return new ServiceInvoice($repo, new FileHelper(realpath(__DIR__ . '/../../var/data/')));
|
||||
}
|
||||
|
||||
public function testEmptyObject()
|
||||
{
|
||||
$repo = new InvoiceDocumentRepository([]);
|
||||
$sut = new ServiceInvoice($repo);
|
||||
$sut = $this->getSut([]);
|
||||
|
||||
$this->assertEmpty($sut->getCalculator());
|
||||
$this->assertIsArray($sut->getCalculator());
|
||||
@@ -44,8 +51,7 @@ class ServiceInvoiceTest extends TestCase
|
||||
|
||||
public function testWithDocumentDirectory()
|
||||
{
|
||||
$repo = new InvoiceDocumentRepository(['templates/invoice/renderer/']);
|
||||
$sut = new ServiceInvoice($repo);
|
||||
$sut = $this->getSut(['templates/invoice/renderer/']);
|
||||
|
||||
$actual = $sut->getDocuments();
|
||||
$this->assertNotEmpty($actual);
|
||||
@@ -59,8 +65,7 @@ class ServiceInvoiceTest extends TestCase
|
||||
|
||||
public function testAdd()
|
||||
{
|
||||
$repo = new InvoiceDocumentRepository([]);
|
||||
$sut = new ServiceInvoice($repo);
|
||||
$sut = $this->getSut([]);
|
||||
|
||||
$sut->addCalculator(new DefaultCalculator());
|
||||
$sut->addNumberGenerator(new DateNumberGenerator());
|
||||
@@ -74,7 +79,7 @@ class ServiceInvoiceTest extends TestCase
|
||||
$this->assertInstanceOf(DefaultCalculator::class, $sut->getCalculatorByName('default'));
|
||||
|
||||
$this->assertEquals(1, count($sut->getNumberGenerator()));
|
||||
$this->assertInstanceOf(DateNumberGenerator::class, $sut->getNumberGeneratorByName('default'));
|
||||
$this->assertInstanceOf(DateNumberGenerator::class, $sut->getNumberGeneratorByName('date'));
|
||||
|
||||
$this->assertEquals(1, count($sut->getRenderer()));
|
||||
}
|
||||
|
||||
146
tests/Invoice/templates/default.pdf.twig
Normal file
146
tests/Invoice/templates/default.pdf.twig
Normal file
@@ -0,0 +1,146 @@
|
||||
{% extends 'invoice/layout.html.twig' %}
|
||||
{% set language = model.template.language|default(app.request.locale) %}
|
||||
{% set isDecimal = model.template.decimalDuration|default(false) %}
|
||||
|
||||
{% block invoice %}
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
<h2 class="page-header">
|
||||
<span contenteditable="true">{{ model.template.title }}</span>
|
||||
<small class="pull-right">{{ 'label.date'|trans({}, 'messages', language) }}: {{ model.invoiceDate|date_short }}</small>
|
||||
</h2>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
{{ 'invoice.from'|trans({}, 'messages', language) }}
|
||||
<address contenteditable="true">
|
||||
<strong>{{ model.template.company }}</strong><br>
|
||||
{{ model.template.address|trim|nl2br }}
|
||||
{% if model.template.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans({}, 'messages', language) }}:
|
||||
{{ model.template.vatId }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</div>
|
||||
<div class="col-sm-2"></div>
|
||||
<div class="col-sm-5">
|
||||
{{ 'invoice.to'|trans({}, 'messages', language) }}
|
||||
<address contenteditable="true">
|
||||
<strong>{{ model.customer.company|default(model.customer.name) }}</strong><br>
|
||||
{{ model.customer.address|nl2br }}
|
||||
{% if model.customer.vatId is not empty %}
|
||||
<br>
|
||||
{{ 'label.vat_id'|trans({}, 'messages', language) }}: {{ model.customer.vatId }}
|
||||
{% endif %}
|
||||
{% if model.customer.number is not empty %}
|
||||
<br>
|
||||
{{ 'label.number'|trans({}, 'messages', language) }}: {{ model.customer.number }}
|
||||
{% endif %}
|
||||
{% if model.query.project is not empty and model.query.project.orderNumber is not empty %}
|
||||
<br>
|
||||
{{ 'label.orderNumber'|trans({}, 'messages', language) }}: {{ model.query.project.orderNumber }}
|
||||
{% endif %}
|
||||
</address>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-sm-5">
|
||||
<p contenteditable="true">
|
||||
<strong>{{ 'invoice.number'|trans({}, 'messages', language) }}:</strong>
|
||||
{{ model.numberGenerator.invoiceNumber }}
|
||||
|
||||
<br>
|
||||
<strong>{{ 'invoice.due_days'|trans({}, 'messages', language) }}:</strong>
|
||||
{{ model.dueDate|date_short }}
|
||||
</p>
|
||||
</div>
|
||||
<div class="col-sm-7"></div>
|
||||
</div>
|
||||
|
||||
<div class="row invoice-items">
|
||||
<div class="col-xs-12 table-responsive">
|
||||
<table class="table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>{{ 'label.date'|trans({}, 'messages', language) }}</th>
|
||||
<th>{{ 'label.description'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.unit_price'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.amount'|trans({}, 'messages', language) }}</th>
|
||||
<th class="text-right">{{ 'label.total_rate'|trans({}, 'messages', language) }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for entry in model.calculator.entries %}
|
||||
{% set duration = entry.duration|duration(isDecimal) %}
|
||||
{% if entry.fixedRate %}
|
||||
{% set rate = entry.fixedRate %}
|
||||
{% set duration = entry.amount|amount %}
|
||||
{% else %}
|
||||
{% set rate = entry.hourlyRate %}
|
||||
{% endif %}
|
||||
<tr>
|
||||
<td nowrap class="text-nowrap">{{ entry.begin|date_short }}</td>
|
||||
<td contenteditable="true">
|
||||
{% if entry.description is not empty %}
|
||||
{{ entry.description|nl2br }}
|
||||
{% else %}
|
||||
{% if entry.activity is not null %}{{ entry.activity.name }} / {% endif %}{{ entry.project.name }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td nowrap class="text-nowrap text-right">{{ rate|money(model.calculator.currency) }}</td>
|
||||
<td nowrap class="text-nowrap text-right">{{ duration }}</td>
|
||||
<td nowrap class="text-nowrap text-right">{{ entry.rate|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
{{ 'invoice.subtotal'|trans({}, 'messages', language) }}
|
||||
</td>
|
||||
<td class="text-right">{{ model.calculator.subtotal|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right">
|
||||
{{ 'invoice.tax'|trans({}, 'messages', language) }} ({{ model.calculator.vat }}%)
|
||||
</td>
|
||||
<td class="text-right">{{ model.calculator.tax|money(model.calculator.currency) }}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td colspan="4" class="text-right text-nowrap">
|
||||
<strong>{{ 'invoice.total'|trans({}, 'messages', language) }}</strong>
|
||||
</td>
|
||||
<td class="text-right">
|
||||
<strong>{{ model.calculator.total|money(model.calculator.currency) }}</strong>
|
||||
</td>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-xs-12">
|
||||
{% if model.template.paymentTerms is not empty %}
|
||||
<div contenteditable="true" class="paymentTerms">
|
||||
{{ model.template.paymentTerms|nl2br|md2html }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<footer class="footer">
|
||||
<p>
|
||||
<strong>{{ 'label.address'|trans({}, 'messages', language) }}</strong>: {{ model.template.company }} – {{ model.template.address|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<br>
|
||||
<strong>{{ 'label.invoice_bank_account'|trans({}, 'messages', language) }}</strong>: {{ model.template.paymentDetails|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
<br>
|
||||
<strong>{{ 'label.contact'|trans({}, 'messages', language) }}</strong>: {{ model.template.contact|replace({"\n": ' – ', "\r\n": ' – ', "\r": ' – '})|raw }}
|
||||
</p>
|
||||
</footer>
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user