"]; private array $tempPositions = []; protected array $columns = [ ['key' => 'id', 'text' => 'ID', 'table' => ['visible' => false], 'modal' => false], ['key' => 'invoice_number', 'text' => 'Rechnungsnr.', 'table' => ['sortable' => true, 'filter' => 'search']], ['key' => 'invoice_date', 'text' => 'Datum', 'type' => 'timestamp', 'table' => ['sortable' => true, 'filter' => 'date', 'formatter' => 'formatDate']], ['key' => 'company', 'text' => 'Firma', 'table' => ['sortable' => true, 'filter' => 'search']], ['key' => 'firstname', 'text' => 'Vorname', 'table' => ['visible' => false], 'modal' => false], ['key' => 'lastname', 'text' => 'Nachname', 'table' => ['visible' => false], 'modal' => false], ['key' => 'customer_number', 'text' => 'Kundennr.', 'table' => ['sortable' => true, 'filter' => 'search']], ['key' => 'total', 'text' => 'Netto', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']], ['key' => 'total_gross', 'text' => 'Brutto', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']], ['key' => 'status', 'text' => 'Status', 'table' => ['filter' => 'select', 'filterOptions' => [ ['value' => 'erstellt', 'text' => 'Erstellt'], ['value' => 'gesendet', 'text' => 'Gesendet'], ['value' => 'exportiert', 'text' => 'Exportiert'], ]]], ['key' => 'billing_type', 'text' => 'Zahlungsart', 'table' => ['filter' => 'select', 'filterOptions' => [ ['value' => 'invoice', 'text' => 'Rechnung'], ['value' => 'sepa', 'text' => 'SEPA'], ]]], ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]], ]; protected array $additionalActions = [ ['key' => 'createGutschrift', 'title' => 'Gutschrift erstellen', 'class' => 'fas fa-file-invoice text-warning'], ['key' => 'pdfPreview', 'title' => 'PDF Vorschau', 'class' => 'fas fa-file-pdf text-danger'], ['key' => 'sendInvoice', 'title' => 'Rechnung aussenden', 'class' => 'fas fa-paper-plane text-success'] ]; protected function createPDFAction($returnFilename = false) { $post = json_decode(file_get_contents('php://input'), true); $invoice = (object)[]; $positions = []; if (isset($post['preview']) && $post['preview'] === true) { $invoice = (object) array_merge([ 'id' => 0, 'invoice_number' => null, 'invoice_date' => time(), 'customer_number' => 0, 'fibu_account_number' => 0, 'company' => '', 'firstname' => '', 'lastname' => '', 'street' => '', 'zip' => '', 'city' => '', 'country' => 'Österreich', 'email' => '', 'uid' => '', 'tax_text' => '', 'billing_type' => 'invoice', 'leistungszeitraum' => '', 'einleitender_text' => '', 'externe_referenz' => '', 'gesamtrabatt' => 0, 'total' => 0, 'total_gross' => 0 ], $post); // Convert invoice_date from string to timestamp if needed if (isset($invoice->invoice_date) && is_string($invoice->invoice_date)) { $invoice->invoice_date = strtotime($invoice->invoice_date); } $positions = array_map(function($p) { $obj = (object)$p; // Map _group to position_group for preview if (isset($p['_group'])) { $obj->position_group = $p['_group']; } return $obj; }, $post['positions'] ?? []); } else { $id = $this->request->id ?? $post['id'] ?? null; if (!$id || !($invoice = ManualInvoiceModel::get($id))) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'Rechnung wurde nicht gefunden']); return; } $positions = $invoice->getProperty('positions'); } $vat = []; foreach ($positions as $p) { $pObj = (object)$p; $vat[$pObj->vatrate] = ($vat[$pObj->vatrate] ?? 0) + ($pObj->price_gross ?? 0) - ($pObj->price_total ?? 0); } $invoice->positions = $positions; $pdf_vars = [ "invoice" => $invoice, "vat" => $vat, "bank_iban" => TT_INVOICE_BANK_IBAN, "bank_bic" => TT_INVOICE_BANK_BIC, "bank_bank" => TT_INVOICE_BANK_BANK, "bank_owner" => TT_INVOICE_BANK_OWNER ]; $replacements = [ "{{ basedir }}" => BASEDIR, "{{ addressLine_1 }}" => $invoice->company ?: "", "{{ addressLine_2 }}" => trim($invoice->firstname . " " . $invoice->lastname), "{{ addressLine_3 }}" => $invoice->street ?? '', "{{ addressLine_4 }}" => ($invoice->zip ?? '') . " " . ($invoice->city ?? ''), "{{ addressLine_5 }}" => ($invoice->country ?? '') != "Österreich" ? ($invoice->country ?? '') : "", "{{ customerNumber }}" => $invoice->customer_number ?? '', "{{ billingAccount }}" => $invoice->fibu_account_number ?? '', "{{ invoiceNumber }}" => $invoice->invoice_number ?? "VORSCHAU", "{{ invoiceDate }}" => date("d.m.Y", $invoice->invoice_date ?? time()), "{{ leistungszeitraumHtml }}" => ($invoice->leistungszeitraum ?? '') ? "Leistungszeitraum:" . htmlspecialchars($invoice->leistungszeitraum) . "" : "", "{{ externeReferenzHtml }}" => ($invoice->externe_referenz ?? '') ? "Externe Referenz:" . htmlspecialchars($invoice->externe_referenz) . "" : "", "{{ vatHtml }}" => ($invoice->uid ?? '') ? "Ihre UID:" . $invoice->uid . "" : "", "{{ qrCodeSrc }}" => $this->generateSepaQRCode($invoice->invoice_number ?? "VORSCHAU", round($invoice->total_gross ?? 0, 2)) ]; $headerHtml = str_replace(array_keys($replacements), array_values($replacements), file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_HEADER.html")); $headerFile = BASEDIR . "/var/temp/manualinvoice_header-" . date("U") . "-" . rand(1000, 9999) . ".html"; file_put_contents($headerFile, $headerHtml); $footerReplacements = [ "{{ bank_iban }}" => TT_INVOICE_BANK_IBAN_FORMATTED, "{{ bank_bic }}" => TT_INVOICE_BANK_BIC, "{{ bank_bank }}" => TT_INVOICE_BANK_BANK, "{{ bank_owner }}" => TT_INVOICE_BANK_OWNER ]; $footerHtml = str_replace(array_keys($footerReplacements), array_values($footerReplacements), file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_FOOTER.html")); $footerFile = BASEDIR . "/var/temp/manualinvoice_footer-" . date("U") . "-" . rand(1000, 9999) . ".html"; file_put_contents($footerFile, $footerHtml); $pdf = new PdfForm("ManualInvoice/PDF_MAIN", $pdf_vars); $filename = $pdf->render("--header-html $headerFile --footer-html $footerFile"); if ($returnFilename === true) return $filename; header('Content-Type: application/pdf'); header('Content-Disposition: inline; filename="' . ($invoice->invoice_number ?? 'preview') . '.pdf"'); readfile($filename); die(); } protected function downloadInvoicePdfAction() { $id = $this->request->id; if (!is_numeric($id) || !$id || !($invoice = ManualInvoiceModel::get($id))) { $this->layout()->setFlash("Rechnung nicht gefunden", "error"); $this->redirect("ManualInvoice"); } if(!($pdf_filename = $this->createPDFAction(true)) || !file_exists($pdf_filename)) { $this->layout()->setFlash("PDF-Datei konnte nicht erstellt werden"); $this->redirect("ManualInvoice"); } header('Content-Type: application/pdf'); header('Content-disposition: attachment; filename="'.$invoice->invoice_number.'.pdf"'); header('Content-Length: ' . filesize($pdf_filename)); readfile($pdf_filename); exit; } protected function pdfPreviewAction() { $post = json_decode(file_get_contents('php://input'), true); $id = $post['id'] ?? null; if (!$id || !($invoice = ManualInvoiceModel::get($id))) { self::returnJson(['success' => false, 'message' => 'Rechnung nicht gefunden']); return; } // Log PDF preview in journal $me = new User(); $me->loadMe(); ManualInvoiceJournalModel::create([ 'manualinvoiceId' => $id, 'text' => 'PDF Vorschau geöffnet', 'createBy' => $me->id, 'create' => time() ]); // Return URL to open in new tab $url = "?action=ManualInvoice_createPDF&id=" . $id; self::returnJson(['success' => true, 'url' => $url]); } protected function getInvoiceEmailAction() { $post = json_decode(file_get_contents('php://input'), true); $id = $post['id'] ?? null; if (!$id || !($invoice = ManualInvoiceModel::get($id))) { self::returnJson(['success' => false, 'message' => 'Rechnung nicht gefunden']); return; } self::returnJson([ 'success' => true, 'invoice' => [ 'id' => $invoice->id, 'invoice_number' => $invoice->invoice_number, 'email' => $invoice->email, 'customerName' => trim(($invoice->company ?: '') . ' ' . $invoice->firstname . ' ' . $invoice->lastname) ] ]); } protected function sendInvoiceEmailAction() { // Enable error reporting for debugging error_reporting(E_ALL); ini_set('display_errors', 1); ini_set('display_startup_errors', 1); $post = json_decode(file_get_contents('php://input'), true); $id = $post['id'] ?? null; $recipientEmail = $post['email'] ?? null; if (!$id || !$recipientEmail) { self::returnJson(['success' => false, 'message' => 'ID oder E-Mail-Adresse fehlt']); return; } $invoice = ManualInvoiceModel::get($id); if (!$invoice) { self::returnJson(['success' => false, 'message' => 'Rechnung nicht gefunden']); return; } // Format invoice date for display $invoiceDateFormatted = date('d.m.Y', $invoice->invoice_date); // Set default subject and body with invoice number and date $defaultSubject = "Ihre Rechnung {$invoice->invoice_number} vom {$invoiceDateFormatted}"; $defaultBody = "Sehr geehrte Damen und Herren,\n\nanbei erhalten Sie Ihre Rechnung Nr. {$invoice->invoice_number} vom {$invoiceDateFormatted}.\n\nMit freundlichen Grüßen\nIhr XINON Team"; $subject = $post['subject'] ?? $defaultSubject; $bodyText = $post['body'] ?? $defaultBody; // Convert literal \n strings to actual newlines (in case frontend sends escaped strings) $bodyText = str_replace('\n', "\n", $bodyText); // Generate PDF $pdf_filename = $this->createPDFAction(true); if (!$pdf_filename || !file_exists($pdf_filename)) { self::returnJson(['success' => false, 'message' => 'PDF konnte nicht erstellt werden']); return; } $pdfContent = file_get_contents($pdf_filename); // --- HTML Email Generation --- $logoXinonPath = BASEDIR . '/public/assets/images/xinon-full.png'; $logoXinonExists = file_exists($logoXinonPath); // Construct HTML Body with Outlook compatibility $html = ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= ''; $html .= 'Rechnung'; $html .= ''; $html .= ''; $html .= ''; $html .= ''; // Outlook-safe container table $html .= ''; $html .= '
'; // Logo with Outlook-safe sizing $html .= '
'; if ($logoXinonExists) { $html .= ''; $html .= 'XINON GmbH'; $html .= ''; } $html .= '
'; $html .= '

' . htmlspecialchars($subject) . '

'; $html .= '
'; $html .= nl2br(htmlspecialchars($bodyText)); $html .= '
'; $html .= '
'; $html .= 'XINON GmbH | www.xinon.at'; $html .= '
'; $html .= ''; $html .= ''; $mail = new PHPMailer(true); try { // Server settings $mail->isSMTP(); $mail->Host = TT_PIPEWORK_SMTP_HOST; $mail->SMTPAuth = true; $mail->Username = TT_PIPEWORK_SMTP_USER; $mail->Password = TT_PIPEWORK_SMTP_PASS; $mail->CharSet = PHPMailer::CHARSET_UTF8; $mail->Encoding = 'base64'; $mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS; $mail->Port = 587; // Logo embedding if ($logoXinonExists) $mail->addEmbeddedImage($logoXinonPath, 'logo_xinon'); $mail->addReplyTo('backoffice@xinon.at', 'XINON Backoffice'); $mail->setFrom('thetool@xinon.at', 'XINON GmbH - Rechnungswesen'); $customerName = trim(($invoice->company ?: '') . ' ' . $invoice->firstname . ' ' . $invoice->lastname); $mail->addAddress($recipientEmail, $customerName); $mail->Subject = $subject; $mail->isHTML(true); $mail->Body = $html; $mail->AltBody = strip_tags($bodyText); // Attachment filename: YYYY-MM-DD_InvoiceNumber_Rechnung.pdf $invoiceDateFile = date('Y-m-d', $invoice->invoice_date); $attachmentFilename = "{$invoiceDateFile}_{$invoice->invoice_number}_Rechnung.pdf"; $mail->addStringAttachment($pdfContent, $attachmentFilename, 'base64', 'application/pdf'); $mail->send(); // Update invoice status $invoice->status = 'gesendet'; $invoice->save(); // Add Journal Entry $me = new User(); $me->loadMe(); ManualInvoiceJournalModel::create([ 'manualinvoiceId' => $id, 'text' => "Rechnung per E-Mail an $recipientEmail gesendet.", 'statusChange' => 'gesendet', 'createBy' => $me->id, 'create' => time() ]); self::returnJson(['success' => true, 'message' => 'E-Mail erfolgreich versendet an ' . $recipientEmail]); } catch (Exception $e) { self::returnJson(['success' => false, 'message' => 'E-Mail konnte nicht gesendet werden. Fehler: ' . $mail->ErrorInfo]); } } protected function downloadInvoiceAction() { $post = json_decode(file_get_contents('php://input'), true); $id = $post['id'] ?? null; if (!$id || !($invoice = ManualInvoiceModel::get($id))) { self::returnJson(['success' => false, 'message' => 'Rechnung nicht gefunden']); return; } $me = new User(); $me->loadMe(); // Log download in journal ManualInvoiceJournalModel::create([ 'manualinvoiceId' => $id, 'text' => 'Rechnung heruntergeladen', 'createBy' => $me->id, 'create' => time() ]); $downloadUrl = "?action=ManualInvoice_downloadInvoicePdf&id=" . $id; self::returnJson([ 'success' => true, 'url' => $downloadUrl ]); } protected function beforeCreate(&$data): bool { if (isset($data['positions']) && is_array($data['positions'])) { $this->tempPositions = $data['positions']; unset($data['positions']); } $me = new User(); $me->loadMe(); // Convert invoice_date from string to timestamp if needed if (isset($data['invoice_date']) && is_string($data['invoice_date'])) { $data['invoice_date'] = strtotime($data['invoice_date']); } // Always generate invoice number (override any null from frontend) $data['invoice_number'] = ManualInvoiceModel::getNextInvoiceNumber(); $data['invoice_date'] = $data['invoice_date'] ?? time(); $data['status'] = 'erstellt'; $data['fibu_payment_skonto'] = $data['fibu_payment_skonto'] ?? 0; $data['fibu_payment_skonto_rate'] = $data['fibu_payment_skonto_rate'] ?? 0; $data['gesamtrabatt'] = $data['gesamtrabatt'] ?? 0; $data['total'] = $data['total'] ?? 0; $data['total_gross'] = $data['total_gross'] ?? 0; $data['lock'] = 0; $data['exported'] = 0; $data['create_by'] = $me->id; $data['edit_by'] = $me->id; $data['create'] = time(); $data['edit'] = time(); return true; } protected function afterCreate($data) { $this->savePositions($data['id']); $this->recalculateTotals($data['id']); // Log creation in journal $me = new User(); $me->loadMe(); ManualInvoiceJournalModel::create([ 'manualinvoiceId' => $data['id'], 'text' => 'Rechnung erstellt', 'statusChange' => 'erstellt', 'createBy' => $me->id, 'create' => time() ]); } protected function beforeUpdate(&$data): bool { if (isset($data['positions']) && is_array($data['positions'])) { $this->tempPositions = $data['positions']; unset($data['positions']); } if (isset($data['id']) && ($invoice = ManualInvoiceModel::get($data['id']))) { if ($invoice->lock == 1) { $this->infoMessages['update'] = 'Rechnung ist gesperrt und kann nicht bearbeitet werden'; return false; } if ($invoice->status === 'exportiert') { $this->infoMessages['update'] = 'Rechnung wurde bereits exportiert und kann nicht mehr bearbeitet werden'; return false; } } // Convert invoice_date from string to timestamp if needed if (isset($data['invoice_date']) && is_string($data['invoice_date'])) { $data['invoice_date'] = strtotime($data['invoice_date']); } $me = new User(); $me->loadMe(); $data['edit_by'] = $me->id; $data['edit'] = time(); return true; } protected function afterUpdate($data) { $existingPositions = ManualInvoicepositionModel::search(['manualinvoice_id' => $data['id']]); foreach ($existingPositions as $pos) ManualInvoicepositionModel::delete($pos->id); $this->savePositions($data['id']); $this->recalculateTotals($data['id']); // Log update in journal $me = new User(); $me->loadMe(); ManualInvoiceJournalModel::create([ 'manualinvoiceId' => $data['id'], 'text' => 'Rechnung aktualisiert', 'createBy' => $me->id, 'create' => time() ]); } private function savePositions($invoiceId) { if (empty($this->tempPositions)) return; $me = new User(); $me->loadMe(); foreach ($this->tempPositions as $position) { // Skip empty positions if (empty($position['product_name']) || ($position['amount'] ?? 0) == 0) continue; // Map _group to position_group $groupName = $position['_group'] ?? null; unset($position['_group']); ManualInvoicepositionModel::create(array_merge([ 'manualinvoice_id' => $invoiceId, 'position_group' => $groupName, 'unit' => 'Stk.', 'discount' => 0, 'create_by' => $me->id, 'edit_by' => $me->id, 'create' => time(), 'edit' => time() ], $position)); } $this->tempPositions = []; } protected function recalculateTotals($invoiceId) { if (!($invoice = ManualInvoiceModel::get($invoiceId))) return; $positions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]); $subtotal = array_sum(array_column($positions, 'price_total')); // Apply gesamtrabatt (total discount) if exists $gesamtrabatt = $invoice->gesamtrabatt ?? 0; $discountAmount = $subtotal * ($gesamtrabatt / 100); $netTotal = $subtotal - $discountAmount; // Calculate gross total with VAT applied after discount $grossTotal = 0; foreach ($positions as $pos) { $positionNet = $pos->price_total; $positionAfterDiscount = $positionNet * (1 - $gesamtrabatt / 100); $grossTotal += $positionAfterDiscount * (1 + $pos->vatrate / 100); } $invoice->total = $netTotal; $invoice->total_gross = $grossTotal; $invoice->save(); } protected function getAction() { $filter = $this->postData['filters'] ?? []; $order = $this->postData['order']['key'] ? $this->postData['order'] : ($this->defaultOrder ?? ['key' => null, 'order' => 'ASC']); $page = $this->postData['pagination']['page'] ?? 1; $perPage = $this->postData['pagination']['per_page'] ?? 10; $rows = ManualInvoiceModel::getAll($filter, $perPage, ($page - 1) * $perPage, $order); $filteredAvailable = ManualInvoiceModel::count($filter); $totalRows = ManualInvoiceModel::count(); foreach ($rows as &$row) { $row->customerName = trim(($row->company ?: '') . ' ' . $row->firstname . ' ' . $row->lastname); $row->positions = array_map([$this, 'formatPosition'], $row->getProperty('positions')); } self::returnJson([ "rows" => $rows, "autoCompleteData" => [], "pagination" => [ "page" => $page, "total_pages" => ceil($filteredAvailable / $perPage), "per_page" => $perPage, "filtered_available" => intval($filteredAvailable), "total_rows" => intval($totalRows) ] ]); } protected function getByIdAction() { $id = $_GET['id'] ?? null; if (!$id || !is_numeric($id)) { http_response_code(500); self::returnJson(['success' => false, 'message' => 'No ID provided.']); die(); } if (!($invoice = ManualInvoiceModel::get($id))) { http_response_code(404); self::returnJson(['success' => false, 'message' => 'Invoice not found.']); die(); } $data = (array) $invoice; $data['positions'] = array_map([$this, 'formatPosition'], $invoice->getProperty('positions')); self::returnJson($data); } private function formatPosition($pos) { return [ 'id' => $pos->id, 'manualinvoice_id' => $pos->manualinvoice_id, '_group' => $pos->position_group ?? '', 'billing_id' => $pos->billing_id, 'contract_id' => $pos->contract_id, 'matchcode' => $pos->matchcode, 'product_id' => $pos->product_id, 'product_name' => $pos->product_name, 'product_info' => $pos->product_info, 'amount' => $pos->amount, 'unit' => $pos->unit ?? 'Stk.', 'price' => $pos->price, 'discount' => $pos->discount ?? 0, 'price_total' => $pos->price_total, 'price_gross' => $pos->price_gross, 'vatrate' => $pos->vatrate, 'fibu_cost_account' => $pos->fibu_cost_account, 'fibu_cost_account_legacy' => $pos->fibu_cost_account_legacy, 'fibu_taxcode' => $pos->fibu_taxcode, 'options' => $pos->options ]; } protected function customRowsHandler($rows) { foreach ($rows as &$row) { $row->customerName = trim(($row->company ?: '') . ' ' . $row->firstname . ' ' . $row->lastname); } return $rows; } protected function generateSepaQRCode($paymentReference, $amount) { $xinonIBAN = TT_INVOICE_BANK_IBAN; $xinonBIC = TT_INVOICE_BANK_BIC; $xinonOwner = TT_INVOICE_BANK_OWNER; $epc = "BCD\n001\n1\nSCT\n$xinonBIC\n$xinonOwner\n$xinonIBAN\nEUR$amount\nXINO\n$paymentReference\n\nXINON GmbH"; return (new \chillerlan\QRCode\QRCode)->render($epc); } protected function getInvoiceForGutschriftAction() { if (!($id = $_GET['id'] ?? null) || !($invoice = ManualInvoiceModel::get($id))) { self::returnJson(['success' => false, 'message' => 'Invoice not found']); } if ($invoice->total < 0) { self::returnJson(['success' => false, 'message' => 'Kann keine Gutschrift für eine Gutschrift erstellen']); } $positions = $invoice->getProperty('positions'); $existingCredits = ManualInvoiceModel::getAll(['credit_for_invoice_id' => $id]); $creditedAmounts = []; foreach ($existingCredits as $credit) { foreach ($credit->getProperty('positions') as $creditPos) { $key = $creditPos->product_id . '_' . $creditPos->matchcode; $creditedAmounts[$key] = ($creditedAmounts[$key] ?? 0) + abs($creditPos->amount); } } $availablePositions = []; foreach ($positions as $pos) { $key = $pos->product_id . '_' . $pos->matchcode; $availableAmount = $pos->amount - ($creditedAmounts[$key] ?? 0); if ($availableAmount > 0) { $availablePositions[] = [ 'id' => $pos->id, 'product_name' => $pos->product_name, 'product_info' => $pos->product_info, 'original_amount' => $pos->amount, 'credited_amount' => $creditedAmounts[$key] ?? 0, 'available_amount' => $availableAmount, 'unit' => $pos->unit ?? 'Stk.', 'price' => $pos->price, 'vatrate' => $pos->vatrate, 'product_id' => $pos->product_id, 'matchcode' => $pos->matchcode, 'fibu_cost_account' => $pos->fibu_cost_account, 'fibu_taxcode' => $pos->fibu_taxcode ]; } } self::returnJson([ 'success' => true, 'invoice' => [ 'id' => $invoice->id, 'invoice_number' => $invoice->invoice_number, 'customer_name' => trim(($invoice->company ?? '') . ' ' . $invoice->firstname . ' ' . $invoice->lastname), 'positions' => $availablePositions ] ]); } protected function createGutschriftAction() { $post = json_decode(file_get_contents('php://input'), true); $originalInvoiceId = $post['original_invoice_id'] ?? null; $positions = $post['positions'] ?? []; if (!$originalInvoiceId || empty($positions) || !($originalInvoice = ManualInvoiceModel::get($originalInvoiceId))) { self::returnJson(['success' => false, 'message' => 'Ungültige Anfrage']); return; } if ($originalInvoice->lock == 1) { self::returnJson(['success' => false, 'message' => 'Originalrechnung ist gesperrt und kann nicht gutgeschrieben werden']); return; } $me = new User(); $me->loadMe(); $invoiceData = [ 'invoice_number' => ManualInvoiceModel::getNextInvoiceNumber(), 'invoice_date' => time(), 'leistungszeitraum' => $originalInvoice->leistungszeitraum ?? null, 'einleitender_text' => 'Gutschrift zur Rechnung ' . $originalInvoice->invoice_number, 'externe_referenz' => $originalInvoice->externe_referenz ?? null, 'gesamtrabatt' => 0, 'owner_id' => $originalInvoice->owner_id, 'billingaddress_id' => $originalInvoice->billingaddress_id, 'customer_number' => $originalInvoice->customer_number, 'fibu_account_number' => $originalInvoice->fibu_account_number, 'fibu_payment_due' => $originalInvoice->fibu_payment_due, 'fibu_payment_skonto' => $originalInvoice->fibu_payment_skonto, 'fibu_payment_skonto_rate' => $originalInvoice->fibu_payment_skonto_rate, 'sepa_date' => $originalInvoice->sepa_date, 'sepa_id' => $originalInvoice->sepa_id, 'sepa_last_date' => $originalInvoice->sepa_last_date, 'fibu_cost_area' => $originalInvoice->fibu_cost_area, 'fibu_cost_account' => $originalInvoice->fibu_cost_account, 'fibu_cost_account_legacy' => $originalInvoice->fibu_cost_account_legacy, 'fibu_taxcode' => $originalInvoice->fibu_taxcode, 'tax_text' => $originalInvoice->tax_text, 'company' => $originalInvoice->company, 'firstname' => $originalInvoice->firstname, 'lastname' => $originalInvoice->lastname, 'street' => $originalInvoice->street, 'zip' => $originalInvoice->zip, 'city' => $originalInvoice->city, 'country' => $originalInvoice->country, 'email' => $originalInvoice->email, 'uid' => $originalInvoice->uid, 'billing_type' => $originalInvoice->billing_type, 'billing_delivery' => $originalInvoice->billing_delivery, 'bank_account_bank' => $originalInvoice->bank_account_bank, 'bank_account_owner' => $originalInvoice->bank_account_owner, 'bank_account_iban' => $originalInvoice->bank_account_iban, 'bank_account_bic' => $originalInvoice->bank_account_bic, 'total' => 0, 'total_gross' => 0, 'vatgroup_id' => $originalInvoice->vatgroup_id, 'credit_for_invoice_id' => $originalInvoiceId, 'status' => 'erstellt', 'lock' => 0, 'exported' => 0, 'create' => time(), 'edit' => time(), 'create_by' => $me->id, 'edit_by' => $me->id ]; if (!($creditInvoiceId = ManualInvoiceModel::create($invoiceData))) { self::returnJson(['success' => false, 'message' => 'Fehler beim Erstellen der Gutschrift']); return; } foreach ($positions as $pos) { $priceTotal = (-abs($pos['amount'])) * $pos['price']; ManualInvoicepositionModel::create([ 'manualinvoice_id' => $creditInvoiceId, 'position_group' => null, 'product_id' => $pos['product_id'], 'product_name' => $pos['product_name'], 'product_info' => $pos['product_info'] ?? '', 'amount' => -abs($pos['amount']), 'unit' => $pos['unit'] ?? 'Stk.', 'price' => $pos['price'], 'discount' => 0, 'vatrate' => $pos['vatrate'], 'price_total' => $priceTotal, 'price_gross' => $priceTotal * (1 + $pos['vatrate'] / 100), 'matchcode' => $pos['matchcode'] ?? null, 'fibu_cost_account' => $pos['fibu_cost_account'] ?? null, 'fibu_taxcode' => $pos['fibu_taxcode'] ?? null, 'contract_id' => 0, 'billing_id' => null, 'create_by' => $me->id, 'edit_by' => $me->id, 'create' => time(), 'edit' => time() ]); } $this->recalculateTotals($creditInvoiceId); self::returnJson(['success' => true, 'message' => 'Gutschrift erfolgreich erstellt', 'credit_invoice_id' => $creditInvoiceId]); } protected function beforeDelete(): bool { if ($id = $this->request->id) { $invoice = ManualInvoiceModel::get($id); if ($invoice && $invoice->lock == 1) { $this->infoMessages['delete'] = 'Rechnung ist gesperrt und kann nicht gelöscht werden'; return false; } if ($invoice && ($invoice->status === 'exported' || $invoice->status === 'exportiert')) { $this->infoMessages['delete'] = 'Rechnung wurde bereits exportiert und kann nicht gelöscht werden'; return false; } if (ManualInvoiceModel::count(['credit_for_invoice_id' => $id]) > 0) { $this->infoMessages['delete'] = 'Rechnung kann nicht gelöscht werden, da bereits Gutschriften existieren'; return false; } foreach (ManualInvoicepositionModel::search(['manualinvoice_id' => $id]) as $pos) { ManualInvoicepositionModel::delete($pos->id); } } return true; } protected function getArticleVatInfoAction() { $articleId = $_GET['article_id'] ?? null; $vatarea = $_GET['vatarea'] ?? 'domestic'; if (!$articleId) { self::returnJson(['success' => false, 'message' => 'Article ID required']); return; } $article = WarehouseArticleModel::get($articleId); if (!$article) { self::returnJson(['success' => false, 'message' => 'Article not found']); return; } // Map revenueAccount to vatgroup_id // revenueAccount 0 = Dienstleistungen = vatgroup_id 2 // revenueAccount 1 = Handelswaren = vatgroup_id 3 $vatgroupId = $article->revenueAccount == 0 ? 2 : 3; // Get vatrate for this vatgroup and area $vatrate = VatrateModel::getFirst(['vatgroup_id' => $vatgroupId, 'area' => $vatarea]); if (!$vatrate) { self::returnJson(['success' => false, 'message' => 'Vatrate not found for vatgroup ' . $vatgroupId . ' and area ' . $vatarea]); return; } self::returnJson([ 'success' => true, 'article' => [ 'id' => $article->id, 'title' => $article->title, 'articleNumber' => $article->articleNumber, 'description' => $article->description, 'revenueAccount' => $article->revenueAccount ], 'vatgroup_id' => $vatgroupId, 'fibu_cost_account' => $vatrate->account, 'fibu_cost_account_legacy' => $vatrate->legacy_account, 'fibu_taxcode' => $vatrate->taxcode, 'vatrate' => $vatrate->rate ]); } protected function getCustomerBillingInfoAction() { $addressId = $_GET['address_id'] ?? null; $vatgroupId = $_GET['vatgroup_id'] ?? 2; if (!$addressId) { self::returnJson(['success' => false, 'message' => 'Address ID required']); return; } $address = new Address($addressId); if (!$address->id) { self::returnJson(['success' => false, 'message' => 'Address not found']); return; } $vatarea = 'domestic'; if ($address->country_id) { $country = new Country($address->country_id); if ($country->id && $country->isocode != TT_HOMECOUNTRY_ISOCODE) { $vatarea = $country->is_eu ? 'eu' : 'other'; } } if ($address->uid && substr(strtolower(preg_replace('/[^a-z0-9]/i', '', $address->uid)), 0, 3) == 'atu') { $vatarea = 'domestic'; } $vatrate = VatrateModel::getFirst(['vatgroup_id' => $vatgroupId, 'area' => $vatarea]); $taxText = $vatrate ? $vatrate->invoice_text : ''; $db = $this->db(); $sepaLimit = null; $res = $db->query("SELECT manual_invoice_sepa_limit FROM Address WHERE id = " . intval($addressId)); if ($res && $row = $res->fetch_assoc()) { $sepaLimit = $row['manual_invoice_sepa_limit'] ? floatval($row['manual_invoice_sepa_limit']) : null; } self::returnJson([ 'success' => true, 'billing_type' => $address->billing_type ?: 'invoice', 'manual_invoice_sepa_limit' => $sepaLimit, 'vatarea' => $vatarea, 'tax_text' => $taxText, 'bank_account_bank' => $address->bank_account_bank, 'bank_account_owner' => $address->bank_account_owner, 'bank_account_iban' => $address->bank_account_iban, 'bank_account_bic' => $address->bank_account_bic, 'sepa_date' => $address->sepa_date, 'sepa_id' => $address->sepa_id ]); } protected function getTaxTextAction() { $vatgroupId = $_GET['vatgroup_id'] ?? 2; $vatarea = $_GET['vatarea'] ?? 'domestic'; $vatrate = VatrateModel::getFirst(['vatgroup_id' => $vatgroupId, 'area' => $vatarea]); self::returnJson([ 'success' => true, 'tax_text' => $vatrate ? $vatrate->invoice_text : '', 'vatrate' => $vatrate ? $vatrate->rate : 20 ]); } }