Compare commits
3 Commits
2.35.0
...
download-i
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
821000f143 | ||
|
|
e5ab1f9e78 | ||
|
|
12190141d7 |
@@ -144,6 +144,10 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
|
||||
_parseBeginTime()
|
||||
{
|
||||
if (this._beginTime.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
let newBeginTime = this._formatTimeForParsing(this._beginTime.value, this._beginTime.dataset['format']);
|
||||
if (newBeginTime !== this._beginTime.value) {
|
||||
this._beginTime.value = newBeginTime;
|
||||
@@ -153,6 +157,10 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
|
||||
_parseEndTime()
|
||||
{
|
||||
if (this._endTime.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
let newEndTime = this._formatTimeForParsing(this._endTime.value, this._endTime.dataset['format']);
|
||||
if (newEndTime !== this._endTime.value) {
|
||||
this._endTime.value = newEndTime;
|
||||
@@ -162,6 +170,10 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
|
||||
_parseDuration()
|
||||
{
|
||||
if (this._duration.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
this._setDurationAsString(this._getParsedDuration());
|
||||
}
|
||||
|
||||
@@ -446,14 +458,14 @@ export default class KimaiTimesheetForm extends KimaiFormPlugin {
|
||||
|
||||
/**
|
||||
* Ruleset:
|
||||
* - invalid duration => skip
|
||||
* - invalid or empty duration => skip
|
||||
* - if begin and end are empty: set begin to now and end to duration
|
||||
* - if begin is empty and end is not empty: set begin to end minus duration
|
||||
* - if begin is not empty and end is empty and duration is > 0 (running records = 0): set end to begin plus duration
|
||||
*/
|
||||
_changedDuration()
|
||||
{
|
||||
if (!this._isDurationConnected()) {
|
||||
if (!this._isDurationConnected() || this._duration.value === '') {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -3,7 +3,7 @@
|
||||
"app": {
|
||||
"js": [
|
||||
"/build/runtime.6c399d29.js",
|
||||
"/build/app.f98f56cc.js"
|
||||
"/build/app.6bb53337.js"
|
||||
],
|
||||
"css": [
|
||||
"/build/app.0416ea92.css"
|
||||
@@ -72,7 +72,7 @@
|
||||
},
|
||||
"integrity": {
|
||||
"/build/runtime.6c399d29.js": "sha384-/rm616f12czi8l/27GvWXtb3g608vJZf2XTUKxqCRI4tsa2vUHP+BW90edTok5zC",
|
||||
"/build/app.f98f56cc.js": "sha384-YWrfaZO6uvTBk94Q/zlqG56vaP68xEWYD1YunerAX1rMJcRMFSzi8fp7TVGeJm78",
|
||||
"/build/app.6bb53337.js": "sha384-ZcwEys2VjhW4jwH/pU+/W06vG7rBounyirdkC+TCA4PyMn2QsENR6FIcNoJEpF5X",
|
||||
"/build/app.0416ea92.css": "sha384-JAIO6+B/vmV8IlpsmQ+zkfO4JsvdKTMsZZaDg6EMlZxkMZ5K3cpEK0mo24D/Wgkh",
|
||||
"/build/app-rtl.7a875ca7.js": "sha384-T7gLI61h9dGeMgzo63vKu4GiDOeLPct9zSUHrceNbhSwIdUmSSNoZ1+d7fKhJJ4/",
|
||||
"/build/app-rtl.0848906b.css": "sha384-O26Xw3P/NSea5iT6lt5v2RaZ6+jP06hf9vYD2pUJbkFSCLGU1iGXnobP25dWjUs/",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"build/app.css": "/build/app.0416ea92.css",
|
||||
"build/app.js": "/build/app.f98f56cc.js",
|
||||
"build/app.js": "/build/app.6bb53337.js",
|
||||
"build/app-rtl.css": "/build/app-rtl.0848906b.css",
|
||||
"build/app-rtl.js": "/build/app-rtl.7a875ca7.js",
|
||||
"build/export-pdf.css": "/build/export-pdf.d8a6c23b.css",
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
namespace App\API;
|
||||
|
||||
use App\Entity\Invoice;
|
||||
use App\Invoice\ServiceInvoice;
|
||||
use App\Repository\CustomerRepository;
|
||||
use App\Repository\InvoiceRepository;
|
||||
use App\Repository\Query\InvoiceArchiveQuery;
|
||||
@@ -18,6 +19,7 @@ use FOS\RestBundle\Request\ParamFetcherInterface;
|
||||
use FOS\RestBundle\View\View;
|
||||
use FOS\RestBundle\View\ViewHandlerInterface;
|
||||
use OpenApi\Attributes as OA;
|
||||
use Symfony\Component\ExpressionLanguage\Expression;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\Routing\Attribute\Route;
|
||||
use Symfony\Component\Security\Http\Attribute\IsGranted;
|
||||
@@ -25,6 +27,7 @@ use Symfony\Component\Validator\Constraints;
|
||||
|
||||
#[Route(path: '/invoices')]
|
||||
#[IsGranted('API')]
|
||||
#[IsGranted('view_invoice')]
|
||||
#[OA\Tag(name: 'Invoice')]
|
||||
final class InvoiceController extends BaseApiController
|
||||
{
|
||||
@@ -40,7 +43,6 @@ final class InvoiceController extends BaseApiController
|
||||
/**
|
||||
* Fetch invoices
|
||||
*/
|
||||
#[IsGranted('view_invoice')]
|
||||
#[OA\Response(response: 200, description: 'Returns a collection of invoices', content: new OA\JsonContent(type: 'array', items: new OA\Items(ref: '#/components/schemas/InvoiceCollection')))]
|
||||
#[Route(methods: ['GET'], path: '', name: 'get_invoices')]
|
||||
#[Rest\QueryParam(name: 'begin', requirements: [new Constraints\DateTime(format: 'Y-m-d\TH:i:s')], strict: true, nullable: true, description: 'Only records after this date will be included (format: HTML5 datetime-local, e.g. YYYY-MM-DDThh:mm:ss)')]
|
||||
@@ -89,7 +91,6 @@ final class InvoiceController extends BaseApiController
|
||||
/**
|
||||
* Fetch invoice
|
||||
*/
|
||||
#[IsGranted('view_invoice')]
|
||||
#[OA\Response(response: 200, description: 'Returns one invoice', content: new OA\JsonContent(ref: '#/components/schemas/Invoice'))]
|
||||
#[Route(methods: ['GET'], path: '/{id}', name: 'get_invoice', requirements: ['id' => '\d+'])]
|
||||
public function getAction(Invoice $invoice): Response
|
||||
@@ -99,4 +100,28 @@ final class InvoiceController extends BaseApiController
|
||||
|
||||
return $this->viewHandler->handle($view);
|
||||
}
|
||||
|
||||
/**
|
||||
* Download invoice
|
||||
*
|
||||
* The returned `content-type` depends on the type of invoice document.
|
||||
* Could be anything from `application/pdf` to `application/vnd.openxmlformats-officedocument.wordprocessingml.document`.
|
||||
*
|
||||
* Use the response header to detect the filename, e.g. `content-disposition: attachment; filename=2025-001-acmme_company.pdf`.
|
||||
*/
|
||||
#[OA\Response(response: 200, description: 'Returns the (binary) invoice document', content: new OA\MediaType(mediaType: 'application/octet-stream', schema: new OA\Schema(type: 'string', format: 'binary')))]
|
||||
#[Route(path: '/{id}/download', requirements: ['id' => '\d+'], methods: ['GET'])]
|
||||
#[IsGranted(new Expression("is_granted('access', subject.getCustomer())"), 'invoice')]
|
||||
public function download(Invoice $invoice, ServiceInvoice $service): Response
|
||||
{
|
||||
$file = $service->getInvoiceFile($invoice);
|
||||
|
||||
if (null === $file) {
|
||||
throw $this->createNotFoundException(
|
||||
\sprintf('Invoice file "%s" could not be found for invoice ID "%s"', $invoice->getInvoiceFilename(), $invoice->getId())
|
||||
);
|
||||
}
|
||||
|
||||
return $this->file($file->getRealPath(), $file->getBasename());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,11 +17,11 @@ final class Constants
|
||||
/**
|
||||
* The current release version
|
||||
*/
|
||||
public const VERSION = '2.35.0';
|
||||
public const VERSION = '2.35.1';
|
||||
/**
|
||||
* The current release: major * 10000 + minor * 100 + patch
|
||||
*/
|
||||
public const VERSION_ID = 23500;
|
||||
public const VERSION_ID = 23501;
|
||||
/**
|
||||
* The software name
|
||||
*/
|
||||
|
||||
@@ -126,6 +126,8 @@ class Invoice implements EntityWithMetaFields
|
||||
#[ORM\Column(name: 'invoice_filename', type: Types::STRING, length: 150, nullable: false)]
|
||||
#[Assert\NotNull]
|
||||
#[Assert\Length(min: 1, max: 150)]
|
||||
#[Serializer\Expose]
|
||||
#[Serializer\Groups(['Default'])]
|
||||
#[Exporter\Expose(label: 'file', type: 'string')]
|
||||
private ?string $invoiceFilename = null;
|
||||
private bool $localized = false;
|
||||
|
||||
@@ -303,6 +303,7 @@ abstract class APIControllerBaseTestCase extends AbstractControllerBaseTestCase
|
||||
'user' => ['result' => 'object', 'type' => '@User'],
|
||||
'dueDays' => 'int',
|
||||
'invoiceNumber' => 'string',
|
||||
'invoiceFilename' => 'string',
|
||||
'metaFields' => 'array',
|
||||
'paymentDate' => '@datetime',
|
||||
'status' => 'string',
|
||||
|
||||
@@ -75,6 +75,7 @@ class ApiDocControllerTest extends AbstractControllerBaseTestCase
|
||||
'/api/customers/{id}/rates/{rateId}',
|
||||
'/api/invoices',
|
||||
'/api/invoices/{id}',
|
||||
'/api/invoices/{id}/download',
|
||||
'/api/projects',
|
||||
'/api/projects/{id}',
|
||||
'/api/projects/{id}/meta',
|
||||
|
||||
@@ -120,6 +120,11 @@ class InvoiceControllerTest extends APIControllerBaseTestCase
|
||||
self::assertApiResponseTypeStructure('Invoice', $result);
|
||||
}
|
||||
|
||||
public function testDownloadInvoice(): void
|
||||
{
|
||||
$this->fail('Missing test');
|
||||
}
|
||||
|
||||
public function testNotFound(): void
|
||||
{
|
||||
$this->assertEntityNotFound(User::ROLE_USER, '/api/invoices/' . PHP_INT_MAX);
|
||||
|
||||
Reference in New Issue
Block a user