Skip to content

Aus Angeboten heraus, können Standard-, Aktonto- und Schlussrechnunge…#574

Merged
dspangenberg merged 3 commits into
mainfrom
develop
Jul 7, 2026
Merged

Aus Angeboten heraus, können Standard-, Aktonto- und Schlussrechnunge…#574
dspangenberg merged 3 commits into
mainfrom
develop

Conversation

@dspangenberg

@dspangenberg dspangenberg commented Jul 7, 2026

Copy link
Copy Markdown
Owner

…n erstellt werden

Summary by CodeRabbit

  • Neue Funktionen

    • Rechnungen können direkt aus einem Angebot erstellt werden: eigene Vorschau-Ansicht plus separater Speichern-Aktion.
    • Neues Rechnungsformular mit Auswahl für Akonto-, Schluss- und Standardrechnung sowie Option zum Zusammenfassen von Positionen.
    • Angebotsdetails zeigen mehrere zugehörige Rechnungen an.
  • Verbesserungen

    • Umfassendere Datenaufbereitung für die Rechnungsvorschau und angepasste Generierungslogik je Rechnungsart (inkl. Steuern/Positionen).
    • Neue Validierung für Rechnungsanforderungen; Formular-UI mit zusätzlichem Titel und dynamischen Eingaben je Rechnungsart.
    • PDF-Darstellung: Preis wird für den betroffenen Zeilentyp nicht mehr angezeigt.
    • Debug-Logging entfernt; Duplizieren behält den Status wie zuvor.

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: d6faf5fa-328f-4020-9e1c-478da2dfe0b0

📥 Commits

Reviewing files that changed from the base of the PR and between e02a9dd and c3ef70c.

📒 Files selected for processing (3)
  • app/Http/Controllers/App/OfferController.php
  • resources/js/Pages/App/Offer/OfferCreateInvoice.tsx
  • resources/js/Pages/App/Offer/OfferDetailsLayout.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • resources/js/Pages/App/Offer/OfferDetailsLayout.tsx
  • resources/js/Pages/App/Offer/OfferCreateInvoice.tsx
  • app/Http/Controllers/App/OfferController.php

Walkthrough

Der PR führt einen neuen Ablauf zur Rechnungserstellung aus Angeboten ein: Validierung per FormRequest, umgebaute Controller-Logik mit Rechnungstypen und Zugferd-Feldern, neue Route- und UI-Anbindung sowie eine Anpassung der PDF-Zeilen-Ausgabe.

Changes

Rechnung aus Angebot erstellen

Layer / File(s) Summary
Validierung und Datenvertrag
app/Http/Requests/OfferInvoiceStoreRequest.php, app/Models/Offer.php, app/Http/Controllers/App/OfferController.php
Neue FormRequest mit Regeln für invoice_type_id, should_summarize und konditionalem deposit; neue invoices()-Relation; createInvoice lädt mehr Offer-, Summen- und Tax-Daten und storeInvoice initialisiert zusätzliche Vorabdaten.
Rechnungsaufbau im Controller
app/Http/Controllers/App/OfferController.php
storeInvoice setzt Rechnungsmetadaten und Zugferd-Felder, verzweigt nach final/deposit/default, erzeugt zusammengefasste oder kopierte Invoice-Lines, ergänzt Gegen-/Storno-Lines aus verknüpften Rechnungen und entfernt im duplicate-Pfad die Pending-Zuweisung.
Routen und Formularansicht
routes/tenant/offers.php, resources/js/Pages/App/Offer/OfferCreateInvoice.tsx, resources/js/Pages/App/Offer/OfferDetailsLayout.tsx, resources/js/Components/twc-ui/form-card.tsx
GET- und POST-Route für die Invoice-Erstellung, neue Angebots-Rechnungsseite mit Typauswahl und Formularfeldern, GET statt POST für den Aufruf der Erstellungsansicht sowie optionale title-Prop für FormCard.
PDF-Ausgabe
resources/views/pdf/invoice/defaultLine.blade.php
Die Preis-Ausgabe für Zeilen vom Typ 3 wurde entfernt.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • dspangenberg/opsc#87: Beide PRs erweitern den Angebots-zu-Rechnung-Flow rund um OfferController::createInvoice(...) und die zugehörige Route.
  • dspangenberg/opsc#130: Beide PRs greifen in die Angebots-/Rechnungs-Verknüpfung und die Verarbeitung in OfferController ein.
  • dspangenberg/opsc#492: Beide PRs berühren die ZUGFeRD-bezogene Rechnungs-Erzeugung und die dazugehörige Controller-Logik.

Poem

Ein Häschen tippt im Morgenlicht,
Rechnungen formen sich ganz schlicht.
Mal final, mal Anzahlung im Nest,
und jede Zeile sitzt nun fest.
🐇📄

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed Der Titel beschreibt passend die Kernänderung: Rechnungen aller drei Typen können aus Angeboten erstellt werden.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch develop

Warning

There were issues while running some tools. Please review the errors and either fix the tool's configuration or disable the tool if it's a critical failure.

🔧 PHPStan (2.2.2)

PHPStan was skipped because the config uses disallowed bootstrapFiles, bootstrapFile, or includes directives.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 5

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
app/Http/Controllers/App/OfferController.php (1)

189-310: 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

Fehlende Datenbanktransaktion beim Erstellen der Rechnung.

storeInvoice erzeugt eine Invoice und (je nach Zweig) mehrere InvoiceLine-Datensätze über separate save()-Aufrufe/Schleifen. Schlägt ein Schritt mitten in der Verarbeitung fehl (z. B. DB-Fehler in der foreach-Schleife für Gegenbuchungen), bleibt eine unvollständige Rechnung (Invoice ohne/mit nur teilweisen Positionen) in der Datenbank zurück. Der gesamte Block sollte in DB::transaction() gekapselt werden.

🛠️ Vorschlag (Skizze)
-    public function storeInvoice(OfferInvoiceStoreRequest $request, Offer $offer)
+    public function storeInvoice(OfferInvoiceStoreRequest $request, Offer $offer): RedirectResponse
     {
-        $offer->loadSum('lines', 'amount');
-        ...
-        return redirect()->route('app.invoice.details', ['invoice' => $invoice->id]);
+        return DB::transaction(function () use ($request, $offer) {
+            $offer->loadSum('lines', 'amount');
+            // ... unveränderte Logik ...
+            return redirect()->route('app.invoice.details', ['invoice' => $invoice->id]);
+        });
     }
Ebenso fehlt der explizite Rückgabetyp (`RedirectResponse`) an der Methode, den die Guideline für PHP-Methoden verlangt. As per coding guidelines, "In PHP files, use explicit return type declarations and type hints for all method parameters."
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/App/OfferController.php` around lines 189 - 310, Wrap
the entire invoice creation flow in storeInvoice() in a DB::transaction() so the
Invoice, its address update, and all InvoiceLine saves either all succeed or all
roll back together, including the final/deposit/default branches and the foreach
over $offer->invoices. Also update the method signature on storeInvoice() to use
an explicit RedirectResponse return type, keeping the existing parameters and
preserving the redirects to app.invoice.details.

Source: Coding guidelines

🧹 Nitpick comments (1)
app/Http/Controllers/App/OfferController.php (1)

623-639: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Methodenname getOfferLine_QB weicht von PHP-Namenskonventionen ab.

Mischung aus camelCase und Unterstrich sowie das kryptische Suffix _QB erschweren die Lesbarkeit. Ein sprechender camelCase-Name (z. B. copyOfferLinesToInvoice) wäre klarer.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/App/OfferController.php` around lines 623 - 639, The
method name getOfferLine_QB mixes camelCase with an underscore and the _QB
suffix, so rename it to a clear camelCase name that reflects its purpose, such
as copyOfferLinesToInvoice, and update any internal references or callers so the
OfferController method remains discoverable and consistent with PHP naming
conventions.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@app/Http/Controllers/App/OfferController.php`:
- Around line 234-249: Handle the nullable result from
$offer->lines()->whereIn('type_id', [1, 3])->first() before using $firstLine in
the invoice creation flow inside OfferController. Add a guard in the branch that
builds the invoice line so you either skip processing or fail gracefully when no
matching line exists, and only read $firstLine->text, $firstLine->tax_rate,
$firstLine->tax_rate_id, and $firstLine->rate->rate after confirming both
$firstLine and $firstLine->rate are present. Also remove the duplicate
invoice_id assignment while updating the same invoice line setup.
- Around line 163-187: The initial $offer->load('contact', 'lines.rate') call in
createInvoice is redundant and gets overridden by the later load chain, which
reloads lines without rate. Remove the duplicate eager-load pattern and make
sure the final load sequence includes lines.rate alongside the ordered lines
relation so OfferData::from($offer) still has rate available without triggering
extra queries.

In `@app/Http/Requests/OfferInvoiceStoreRequest.php`:
- Around line 24-31: In OfferInvoiceStoreRequest::messages(), the custom
validation messages still reference unrelated fields from a different request,
so they never apply to the rules() in this class. Update the message keys to
match the actual validated attributes in OfferInvoiceStoreRequest (the fields
defined in rules(), such as invoice_type_id, should_summarize, and deposit) and
replace the copied German texts with messages that fit those validations.

In `@resources/js/Pages/App/Offer/OfferCreateInvoice.tsx`:
- Around line 14-28: Rename the copied identifiers in the invoice page so they
reflect the actual screen: update OfferHistory and OfferTermsProps in
OfferCreateInvoice.tsx to invoice-specific names, and keep the component
signature aligned with the new props interface. Make sure the exported/used
React component name and its props type clearly match the invoice creation
context so DevTools, error boundaries, and future maintenance show the correct
component identity.
- Around line 96-102: The JSX in OfferCreateInvoice renders literal “(” and “)”
text nodes around the OfferDetailsSide sidebar, so remove those stray characters
from the returned markup and keep only the wrapping div structure. Use the
OfferCreateInvoice component and the OfferDetailsSide block as the place to
clean up the rendered JSX so the sidebar no longer shows extra parentheses in
the UI.

---

Outside diff comments:
In `@app/Http/Controllers/App/OfferController.php`:
- Around line 189-310: Wrap the entire invoice creation flow in storeInvoice()
in a DB::transaction() so the Invoice, its address update, and all InvoiceLine
saves either all succeed or all roll back together, including the
final/deposit/default branches and the foreach over $offer->invoices. Also
update the method signature on storeInvoice() to use an explicit
RedirectResponse return type, keeping the existing parameters and preserving the
redirects to app.invoice.details.

---

Nitpick comments:
In `@app/Http/Controllers/App/OfferController.php`:
- Around line 623-639: The method name getOfferLine_QB mixes camelCase with an
underscore and the _QB suffix, so rename it to a clear camelCase name that
reflects its purpose, such as copyOfferLinesToInvoice, and update any internal
references or callers so the OfferController method remains discoverable and
consistent with PHP naming conventions.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8ab191ab-6d3e-4681-8a99-100e13938bb3

📥 Commits

Reviewing files that changed from the base of the PR and between 9d4c46d and 88040a9.

⛔ Files ignored due to path filters (3)
  • .DS_Store is excluded by !**/.DS_Store
  • app/.DS_Store is excluded by !**/.DS_Store
  • app/Http/.DS_Store is excluded by !**/.DS_Store
📒 Files selected for processing (7)
  • app/Http/Controllers/App/OfferController.php
  • app/Http/Requests/OfferInvoiceStoreRequest.php
  • app/Models/Offer.php
  • resources/js/Components/twc-ui/form-card.tsx
  • resources/js/Pages/App/Offer/OfferCreateInvoice.tsx
  • resources/js/Pages/App/Offer/OfferDetailsLayout.tsx
  • routes/tenant/offers.php

Comment thread app/Http/Controllers/App/OfferController.php
Comment on lines +234 to +249
$firstLine = $offer->lines()->whereIn('type_id', [1, 3])->first();
$firstLineParts = explode("\n", $firstLine->text);
$text[] = $firstLineParts[0];
$text[] = 'gemäß AG-'.$offer->formated_offer_number;
$invoiceLine->invoice_id = $invoice->id;
$invoiceLine->type_id = 3;
$invoiceLine->invoice_id = $invoice->id;
$invoiceLine->pos = 1;
$invoiceLine->quantity = 1;
$invoiceLine->unit = '*';
$invoiceLine->price = $amount;
$invoiceLine->amount = $amount;
$invoiceLine->text = implode("\n", $text);
$invoiceLine->tax_rate = $firstLine->tax_rate;
$invoiceLine->tax_rate_id = $firstLine->tax_rate_id;
$invoiceLine->tax = $amount / 100 * $firstLine->rate->rate;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🩺 Stability & Availability | 🟠 Major | ⚡ Quick win

Mögliche Null-Pointer-Exception bei $firstLine.

$offer->lines()->whereIn('type_id', [1, 3])->first() kann null zurückgeben, falls das Angebot keine passende Zeile (Typ 1 oder 3) besitzt. Direkt danach wird $firstLine->text (bzw. $firstLine->rate->rate) ohne Null-Prüfung zugegriffen – das führt zu einem Fatal Error und würde die gesamte Rechnungserstellung abbrechen. Zusätzlich ist $firstLine->rate nicht garantiert geladen/gesetzt (z. B. wenn tax_rate_id null ist), was zu einer weiteren NPE führen kann.

🛠️ Vorschlag
             $firstLine = $offer->lines()->whereIn('type_id', [1, 3])->first();
+            if (! $firstLine || ! $firstLine->rate) {
+                abort(422, 'Für die Rechnungserstellung wird mindestens eine gültige Angebotsposition mit Steuersatz benötigt.');
+            }
             $firstLineParts = explode("\n", $firstLine->text);

Also applies to: 281-299

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Controllers/App/OfferController.php` around lines 234 - 249, Handle
the nullable result from $offer->lines()->whereIn('type_id', [1, 3])->first()
before using $firstLine in the invoice creation flow inside OfferController. Add
a guard in the branch that builds the invoice line so you either skip processing
or fail gracefully when no matching line exists, and only read $firstLine->text,
$firstLine->tax_rate, $firstLine->tax_rate_id, and $firstLine->rate->rate after
confirming both $firstLine and $firstLine->rate are present. Also remove the
duplicate invoice_id assignment while updating the same invoice line setup.

Comment on lines +24 to +31
public function messages(): array
{
return [
'group.required' => 'Die Einstellungsgruppe ist erforderlich.',
'group.in' => 'Die Einstellungsgruppe ist ungültig.',
'key.required' => 'Der Einstellungsschlüssel ist erforderlich.',
];
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

messages() referenziert nicht-existente Felder.

Die Schlüssel group.required, group.in und key.required gehören zu keinem der in rules() definierten Felder (invoice_type_id, should_summarize, deposit). Das sieht nach einem Copy-Paste-Rest aus einer anderen Request-Klasse aus – die deutschen Fehlermeldungen für diese Rechnungs-Request greifen dadurch nie.

🛠️ Vorschlag für passende Fehlermeldungen
     public function messages(): array
     {
         return [
-            'group.required' => 'Die Einstellungsgruppe ist erforderlich.',
-            'group.in' => 'Die Einstellungsgruppe ist ungültig.',
-            'key.required' => 'Der Einstellungsschlüssel ist erforderlich.',
+            'invoice_type_id.required' => 'Die Rechnungsart ist erforderlich.',
+            'invoice_type_id.in' => 'Die Rechnungsart ist ungültig.',
+            'should_summarize.required' => 'Bitte gib an, ob die Positionen zusammengefasst werden sollen.',
+            'deposit.required' => 'Die Höhe der Anzahlung ist erforderlich.',
+            'deposit.min' => 'Die Anzahlung muss mindestens :min € betragen.',
         ];
     }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
public function messages(): array
{
return [
'group.required' => 'Die Einstellungsgruppe ist erforderlich.',
'group.in' => 'Die Einstellungsgruppe ist ungültig.',
'key.required' => 'Der Einstellungsschlüssel ist erforderlich.',
];
}
public function messages(): array
{
return [
'invoice_type_id.required' => 'Die Rechnungsart ist erforderlich.',
'invoice_type_id.in' => 'Die Rechnungsart ist ungültig.',
'should_summarize.required' => 'Bitte gib an, ob die Positionen zusammengefasst werden sollen.',
'deposit.required' => 'Die Höhe der Anzahlung ist erforderlich.',
'deposit.min' => 'Die Anzahlung muss mindestens :min € betragen.',
];
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/Http/Requests/OfferInvoiceStoreRequest.php` around lines 24 - 31, In
OfferInvoiceStoreRequest::messages(), the custom validation messages still
reference unrelated fields from a different request, so they never apply to the
rules() in this class. Update the message keys to match the actual validated
attributes in OfferInvoiceStoreRequest (the fields defined in rules(), such as
invoice_type_id, should_summarize, and deposit) and replace the copied German
texts with messages that fit those validations.

Comment thread resources/js/Pages/App/Offer/OfferCreateInvoice.tsx Outdated
Comment thread resources/js/Pages/App/Offer/OfferCreateInvoice.tsx
@dspangenberg

Copy link
Copy Markdown
Owner Author

@coderrabitai kannst du einen neuen review durchführen?

@dspangenberg

Copy link
Copy Markdown
Owner Author

@coderabbitai kannst du einen neuen review durchführen?

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@dspangenberg Ja, gerne! Ich starte einen neuen Review der Pull Request.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@dspangenberg

Copy link
Copy Markdown
Owner Author

@coderabbitai kannst du einen neuen review durchführen?

@coderabbitai

coderabbitai Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

@dspangenberg Ja, gerne! Ich starte einen neuen Review der Pull Request.

✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@dspangenberg dspangenberg merged commit c5cb4a6 into main Jul 7, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant