"]; 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' => 'draft', 'text' => 'Entwurf'], ['value' => 'finalized', 'text' => 'Finalisiert'], ['value' => 'exported', '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'] ]; 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', 'total' => 0, 'total_gross' => 0 ], $post); $positions = array_map(fn($p) => (object)$p, $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()), "{{ 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 beforeCreate(&$data): bool { if (isset($data['positions']) && is_array($data['positions'])) { $this->tempPositions = $data['positions']; unset($data['positions']); } $me = new User(); $me->loadMe(); $data = array_merge([ 'invoice_number' => ManualInvoiceModel::getNextInvoiceNumber(), 'invoice_date' => time(), 'status' => 'draft', 'fibu_payment_skonto' => 0, 'fibu_payment_skonto_rate' => 0, 'total' => 0, 'total_gross' => 0, 'create_by' => $me->id, 'edit_by' => $me->id, 'create' => time(), 'edit' => time() ], $data); return true; } protected function afterCreate($data) { $this->savePositions($data['id']); $this->recalculateTotals($data['id']); } 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'])) && $invoice->status === 'exported') { $this->infoMessages['update'] = 'Rechnung wurde bereits exportiert und kann nicht mehr bearbeitet werden'; return false; } $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']); } private function savePositions($invoiceId) { if (empty($this->tempPositions)) return; $me = new User(); $me->loadMe(); foreach ($this->tempPositions as $position) { ManualInvoicepositionModel::create(array_merge([ 'manualinvoice_id' => $invoiceId, 'start_date' => date('Y-m-d'), '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]); $invoice->total = array_sum(array_column($positions, 'price_total')); $invoice->total_gross = array_sum(array_column($positions, 'price_gross')); $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, 'billing_id' => $pos->billing_id, 'contract_id' => $pos->contract_id, 'start_date' => $pos->start_date, 'end_date' => $pos->end_date, 'matchcode' => $pos->matchcode, 'product_id' => $pos->product_id, 'product_name' => $pos->product_name, 'product_info' => $pos->product_info, 'amount' => $pos->amount, 'price' => $pos->price, '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, 'billing_period' => $pos->billing_period, '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, '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']); } $me = new User(); $me->loadMe(); $invoiceData = [ 'invoice_number' => ManualInvoiceModel::getNextInvoiceNumber(), 'invoice_date' => time(), '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' => 'finalized', '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']); } foreach ($positions as $pos) { $priceTotal = (-abs($pos['amount'])) * $pos['price']; ManualInvoicepositionModel::create([ 'manualinvoice_id' => $creditInvoiceId, 'product_id' => $pos['product_id'], 'product_name' => $pos['product_name'], 'product_info' => $pos['product_info'] ?? '', 'amount' => -abs($pos['amount']), 'price' => $pos['price'], '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, 'start_date' => date('Y-m-d'), 'billing_period' => 0, '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->status === 'exported') { $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; } }