feat: additional receipts as attachments to teamer invoices

addresses #869dv97u9
This commit is contained in:
2026-09-09 11:10:57 +02:00
parent 16d847137d
commit 52926ec6cc
29 changed files with 682 additions and 23 deletions
+34
View File
@@ -21,6 +21,7 @@ class DispositionVoter extends Voter
public const INVOICE = 'INVOICE';
public const FEEDBACK = 'FEEDBACK';
public const CALL_OFF = 'CALL_OFF';
public const MANAGE_RECEIPTS = 'MANAGE_RECEIPTS';
public function __construct(private readonly Security $security)
{
@@ -42,6 +43,7 @@ class DispositionVoter extends Voter
static::INVOICE,
static::FEEDBACK,
static::CALL_OFF,
static::MANAGE_RECEIPTS,
]);
}
@@ -66,10 +68,42 @@ class DispositionVoter extends Voter
static::ADMIN_DOCUMENT_UPLOAD => $this->assertAdminDocumentUploadAllowed($disposition),
static::CALL_OFF => $this->security->isGranted('ROLE_ADMINISTRATIVE')
&& Disposition::STATUS_CALLED_OFF !== $disposition->getStatus(),
static::MANAGE_RECEIPTS => $this->assertManageReceiptsAllowed($token, $disposition),
default => false,
};
}
/**
* Whether Belege backing the Honorarnote may be added, listed or deleted.
*
* This is deliberately a permission and not a workflow transition. The disposition state
* machine offers upload_invoice only from "ended", so a teamer whose invoice is being checked
* has no transition available at all - which is precisely the phase in which receipts must
* stay manageable. Receipts also have to survive the ended -> checking_invoice -> ended
* bouncing that upload_invoice and reject_invoice cause, so the window is expressed as the two
* places themselves rather than as anything workflow_can() could answer. Do not "tidy" this
* into a workflow check.
*
* Once the invoice is accepted the disposition leaves for "completed" and the receipts are
* deleted (see Administrative\Document\CheckController), so the window closes on its own.
*/
private function assertManageReceiptsAllowed(TokenInterface $token, Disposition $disposition): bool
{
if (true === $disposition->isSkipFormalities()) {
return false;
}
if (false === $this->security->isGranted('ROLE_ADMINISTRATIVE')
&& false === $this->assertTeamerAccess($token, $disposition)) {
return false;
}
return in_array($disposition->getStatus(), [
Disposition::STATUS_ENDED,
Disposition::STATUS_CHECKING_INVOICE,
], true);
}
private function assertHouseManagerAccess(TokenInterface $token, Disposition $disposition): bool
{
if (false === $this->security->isGranted('ROLE_HOUSE_MANAGER')) {