diff --git a/application/WarehouseShippingNote/WarehouseShippingNoteController.php b/application/WarehouseShippingNote/WarehouseShippingNoteController.php
index 0d0d90c89..02b07ee85 100644
--- a/application/WarehouseShippingNote/WarehouseShippingNoteController.php
+++ b/application/WarehouseShippingNote/WarehouseShippingNoteController.php
@@ -34,6 +34,14 @@ class WarehouseShippingNoteController extends TTCrud {
'delete' => 'Lieferschein wurde gelöscht',
'noChanges' => 'Keine Änderungen vorgenommen'];
protected array $permissionCheck = ['WarehouseUser'];
+ protected array $additionalActions = [
+ [
+ 'key' => 'createManualInvoice',
+ 'title' => 'Rechnung erstellen',
+ 'class' => 'fas fa-file-invoice text-primary',
+ 'condition' => ['status' => 'accepted']
+ ]
+ ];
//@formatter:on
protected function prepareCrudConfig() {
@@ -109,6 +117,177 @@ class WarehouseShippingNoteController extends TTCrud {
));
}
+ protected function getShippingNoteForInvoiceAction() {
+ $id = $this->request->id;
+
+ // Get shipping note
+ $shippingNote = WarehouseShippingNoteModel::get($id);
+ if (!$shippingNote) {
+ self::returnJson(['success' => false, 'message' => 'Lieferschein nicht gefunden']);
+ return;
+ }
+
+ // Get billing address info
+ $billingAddress = null;
+ if ($shippingNote->billingAddressId) {
+ $billingAddress = Address::getOne($shippingNote->billingAddressId);
+ }
+
+ // Determine price type ONCE (not in loop for performance)
+ $priceType = 'Verkauf';
+ if ($shippingNote->billingAddressId) {
+ $addressPriceType = AddressPriceTypeModel::getFirst(['address_id' => $shippingNote->billingAddressId]);
+ if ($addressPriceType) {
+ $warehousePriceType = WarehouseArticlePriceTypeModel::get($addressPriceType->priceType_id);
+ if ($warehousePriceType) {
+ $priceType = $warehousePriceType->title;
+ }
+ }
+ }
+
+ // Decode and enrich positions
+ $positions = json_decode($shippingNote->positions, true);
+ if (!is_array($positions)) {
+ $positions = [];
+ }
+
+ $enrichedPositions = [];
+
+ foreach ($positions as $position) {
+ if (isset($position['article'])) {
+ // Fetch article details
+ $article = WarehouseArticleModel::get($position['article']);
+ if (!$article) continue;
+
+ // Get price for determined price type
+ $prices = json_decode($article->cheapestSellPrice, true) ?: [];
+ $price = 0;
+ foreach ($prices as $p) {
+ if ($p['title'] === $priceType) {
+ $price = $p['price'];
+ break;
+ }
+ }
+
+ $enrichedPositions[] = [
+ 'type' => 'article',
+ 'articleId' => $article->id,
+ 'product_name' => $article->articleNumber . " | " . $article->title,
+ 'product_info' => $article->description,
+ 'amount' => $position['amount'],
+ 'unit' => $article->unit,
+ 'price' => $price,
+ 'discount' => 0,
+ 'vatrate' => 20
+ ];
+
+ } elseif (isset($position['articlePacket'])) {
+ // Handle article packets
+ $packet = WarehouseArticlePacketModel::get($position['articlePacket']);
+ if (!$packet) continue;
+
+ $enrichedPositions[] = [
+ 'type' => 'packet',
+ 'packetId' => $packet->id,
+ 'product_name' => $packet->title,
+ 'product_info' => $packet->description ?? '',
+ 'amount' => $position['amount'],
+ 'unit' => 'Pau.',
+ 'price' => 0,
+ 'discount' => 0,
+ 'vatrate' => 20
+ ];
+
+ } elseif (isset($position['articleText'])) {
+ // Handle custom text entries
+ $enrichedPositions[] = [
+ 'type' => 'text',
+ 'product_name' => $position['articleText'],
+ 'product_info' => '',
+ 'amount' => $position['amount'] ?? 1,
+ 'unit' => 'Stk.',
+ 'price' => 0,
+ 'discount' => 0,
+ 'vatrate' => 20
+ ];
+ }
+ }
+
+ // Add hours entries as positions
+ $hoursEntries = json_decode($shippingNote->hoursEntries, true);
+ if (!is_array($hoursEntries)) {
+ $hoursEntries = [];
+ }
+
+ foreach ($hoursEntries as $hoursEntry) {
+ if (empty($hoursEntry['hourCount']) || floatval(str_replace(",", ".", $hoursEntry['hourCount'])) <= 0) {
+ continue;
+ }
+
+ $userName = 'Unbekannt';
+ if (!empty($hoursEntry['userId']) && is_numeric($hoursEntry['userId'])) {
+ try {
+ $user = UserModel::getOne($hoursEntry['userId']);
+ $userName = $user ? $user->name : 'Unbekannt';
+ } catch (Exception $e) {
+ $userName = 'Unbekannt';
+ }
+ } elseif (!empty($hoursEntry['userId_text'])) {
+ $userName = $hoursEntry['userId_text'];
+ }
+
+ $enrichedPositions[] = [
+ 'type' => 'hours',
+ 'product_name' => 'Arbeitsstunden - ' . $userName,
+ 'product_info' => 'Datum: ' . (isset($hoursEntry['date']) ? date('d.m.Y', strtotime($hoursEntry['date'])) : ''),
+ 'amount' => str_replace(",", ".", $hoursEntry['hourCount']),
+ 'unit' => 'h',
+ 'price' => 60,
+ 'discount' => 0,
+ 'vatrate' => 20
+ ];
+ }
+
+ self::returnJson([
+ 'success' => true,
+ 'data' => [
+ 'shippingNoteId' => $shippingNote->id,
+ 'billingAddress' => $billingAddress ? [
+ 'id' => $billingAddress->id,
+ 'customer_number' => $billingAddress->customer_number,
+ 'company' => $billingAddress->company,
+ 'firstname' => $billingAddress->firstname,
+ 'lastname' => $billingAddress->lastname,
+ 'street' => $billingAddress->street,
+ 'zip' => $billingAddress->zip,
+ 'city' => $billingAddress->city,
+ 'email' => $billingAddress->email,
+ 'uid' => $billingAddress->uid,
+ 'fibu_account_number' => $billingAddress->fibu_account_number,
+ 'billing_type' => $billingAddress->billing_type,
+ 'billing_delivery' => $billingAddress->billing_delivery,
+ 'bank_account_bank' => $billingAddress->bank_account_bank,
+ 'bank_account_owner' => $billingAddress->bank_account_owner,
+ 'bank_account_iban' => $billingAddress->bank_account_iban,
+ 'bank_account_bic' => $billingAddress->bank_account_bic,
+ 'sepa_date' => $billingAddress->sepa_date,
+ 'fibu_payment_due' => $billingAddress->fibu_payment_due,
+ 'fibu_payment_skonto' => $billingAddress->fibu_payment_skonto,
+ 'fibu_payment_skonto_rate' => $billingAddress->fibu_payment_skonto_rate
+ ] : null,
+ 'deliveryAddress' => [
+ 'name' => $shippingNote->deliveryAddressName,
+ 'line' => $shippingNote->deliveryAddressLine,
+ 'plz' => $shippingNote->deliveryAddressPLZ,
+ 'city' => $shippingNote->deliveryAddressCity,
+ 'email' => $shippingNote->deliveryAddressEMail
+ ],
+ 'note' => $shippingNote->note,
+ 'positions' => $enrichedPositions
+ ]
+ ]);
+ }
+
protected function getArticleAddressPriceAction() {
empty($this->request->articleId) && $this->sendError('Keine Artikel ID gefunden');
empty($this->request->addressId) && $this->sendError('Keine Adress ID gefunden');
diff --git a/public/js/pages/ManualInvoice/ManualInvoice.js b/public/js/pages/ManualInvoice/ManualInvoice.js
index f81021b9c..4ce7a72cd 100644
--- a/public/js/pages/ManualInvoice/ManualInvoice.js
+++ b/public/js/pages/ManualInvoice/ManualInvoice.js
@@ -16,12 +16,29 @@ Vue.component('manual-invoice', {
-