diff --git a/Layout/default/ManualInvoice/PDF_FOOTER.html b/Layout/default/ManualInvoice/PDF_FOOTER.html
new file mode 100644
index 000000000..0a984b7d6
--- /dev/null
+++ b/Layout/default/ManualInvoice/PDF_FOOTER.html
@@ -0,0 +1,43 @@
+
+
+
+
+
+
Ihre Xinon =($is_credit) ? "Gutschrift" : "Rechnung"?> vom =date("d.m.Y",$invoice->invoice_date)?>
+
+
+
+ Leistung / Produkt
+ Zeitraum
+ Preis
+ Menge
+ Netto €
+ Ust. %
+ Brutto €
+
+ positions as $p):
+ $timerange_month_only = method_exists($p, 'getOption') ? $p->getOption('timerange_month_only') : (isset($p->options) ? (json_decode($p->options, true)['timerange_month_only'] ?? false) : false);
+
+ // Handle dates safely
+ $start_date = null;
+ $end_date = null;
+ if (!empty($p->start_date)) {
+ try {
+ $start_date = new DateTime($p->start_date);
+ } catch (Exception $e) {
+ $start_date = null;
+ }
+ }
+ if (!empty($p->end_date)) {
+ try {
+ $end_date = new DateTime($p->end_date);
+ } catch (Exception $e) {
+ $end_date = $start_date;
+ }
+ } else {
+ $end_date = $start_date;
+ }
+
+ $amount = (float) number_format($p->amount ?? 0, 3, ",", ".");
+ $price = number_format($p->price ?? 0, 2, ",",".");
+ $price_total = number_format($p->price_total ?? 0, 2, ",",".");
+ $price_gross = number_format($p->price_gross ?? 0, 2, ",",".");
+ $vatrate = number_format($p->vatrate ?? 0, 0, ",",".");
+
+ ?>
+
+ ">
+
+ =htmlspecialchars($p->product_name ?? '')?>
+ matchcode) && $p->matchcode): ?>
+ =htmlspecialchars($p->matchcode)?>
+
+
+
+
+
+ =$start_date->format("m.Y")?>
+ billing_period) && $p->billing_period > 1): ?>
+ =$start_date->format("m.Y")?> - =$end_date->format("m.Y") ?>
+
+ format("d.m.Y") == $end_date->format("d.m.Y")): ?>
+ =$start_date->format("d.m.Y")?>
+
+ =$start_date->format("d.m.Y")?> - =$end_date->format("d.m.Y") ?>
+
+
+
+ =$start_date->format("d.m.Y")?>
+
+ -
+
+
+
+ =$price?> €
+ =$amount?>
+ =$price_total?> €
+ =$vatrate?>%
+ =$price_gross?> €
+
+
+
+ Gesamt Netto:
+ =number_format($net_total, 2, ",","."). " €"?>
+
+
+ $vat_total): ?>
+
+ 0): ?>
+
+ USt. =number_format($rate, 0, ",", ".")?>%:
+ =number_format($vat_total, 2, ",","."). " €"?>
+
+
+
+
+
+
+
+ Gesamt Brutto:
+ =number_format($gross_total, 2, ",","."). " €"?>
+
+
+
+
+
+ tax_text): ?>
+
=$invoice->tax_text?>
+
+
+
Gutschrift! Bitte nicht überweisen.
+ billing_type == "sepa"): ?>
+
BITTE NICHT EINZAHLEN, DER BETRAG WIRD AUTOMATISCH VON IHREM KONTO ABGEBUCHT !
+
+ Bitte
überweisen Sie den Rechnungsbetrag bis zum
=(new DateTime("@".$invoice->invoice_date))->modify("+14 days")->format("d.m.Y")?> auf folgendes Konto:
+
IBAN: =$bank_iban?>
+
BIC: =$bank_bic?>
+ Bitte geben Sie als Verwendungszweck unbedingt die Rechnungsnummer an, nur so können wir Ihre Zahlung eindeutig zuordnen
+
+
+
+
+
diff --git a/application/ManualInvoice/ManualInvoice.php b/application/ManualInvoice/ManualInvoice.php
new file mode 100644
index 000000000..4657df577
--- /dev/null
+++ b/application/ManualInvoice/ManualInvoice.php
@@ -0,0 +1,233 @@
+id) {
+ $invoice_number = $this->invoice_number;
+ $invoice_date = $this->invoice_date;
+ } else {
+ $invoice_number = "PROFORMA";
+ $invoice_date = 1;
+ }
+
+ $filename = "";
+ $positions = $this->getProperty("positions");
+
+ $vat = [];
+ foreach ($positions as $p) {
+ if (!array_key_exists($p->vatrate, $vat)) {
+ $vat[$p->vatrate] = 0;
+ }
+ $vat[$p->vatrate] += $p->price_gross - ($p->price * $p->amount);
+ }
+
+ $pdf_vars = [
+ "invoice" => $this,
+ "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
+ ];
+
+ // Replace placeholders in header
+ $headerHtml = file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_HEADER.html");
+ $headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_1 }}", $this->company ? $this->company : "", $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_2 }}", $this->firstname . " " . $this->lastname, $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_3 }}", nl2br($this->street), $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_4 }}", $this->zip . " " . $this->city, $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_5 }}", $this->country != "Österreich" ? $this->country : "", $headerHtml);
+ $headerHtml = str_replace("{{ customerNumber }}", $this->customer_number, $headerHtml);
+ $headerHtml = str_replace("{{ billingAccount }}", $this->fibu_account_number, $headerHtml);
+ $headerHtml = str_replace("{{ invoiceNumber }}", $invoice_number, $headerHtml);
+ $headerHtml = str_replace("{{ invoiceDate }}", date("d.m.Y", $invoice_date), $headerHtml);
+ $headerHtml = str_replace("{{ vatHtml }}", $this->uid ? "
Ihre UID: " . $this->uid . " " : "", $headerHtml);
+ $headerHtml = str_replace("{{ qrCodeSrc }}", $this->getSepaQRCode($invoice_number, round($this->total_gross, 2)), $headerHtml);
+
+ $headerFile = BASEDIR . "/var/temp/manualinvoice_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
+ file_put_contents($headerFile, $headerHtml);
+
+
+ // Replace placeholders in footer
+ $footerHtml = file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_FOOTER.html");
+ $footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml);
+ $footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml);
+ $footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml);
+ $footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml);
+
+
+ $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);
+ $wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile";
+ $filename = $pdf->render($wkhtmltopdfArgs);
+
+ return $filename;
+
+ }
+
+ public function getSepaQRCode($paymentReference, $amount) {
+ $xinonIBAN = TT_INVOICE_BANK_IBAN;
+ $xinonBIC = TT_INVOICE_BANK_BIC;
+ $xinonOwner = TT_INVOICE_BANK_OWNER;
+
+ $epc = "BCD
+001
+1
+SCT
+$xinonBIC
+$xinonOwner
+$xinonIBAN
+EUR$amount
+XINO
+$paymentReference
+
+XINON GmbH";
+
+ return (new QRCode)->render($epc);
+ }
+
+
+ public function sendByEmail($to_email = false) {
+ if(!$this->id) return false;
+
+ $pdf = $this->getProperty("pdf");
+ if(!$pdf || !$pdf->name) {
+ return false;
+ }
+
+ $pdf_filename = false;
+ try {
+ $pdf_filename = $pdf->getFullPath();
+ } catch(\Exception $e) {
+ $this->log->error("File for ManualInvoice ".$this->id." not found");
+ }
+
+ if(!$pdf_filename || !file_exists($pdf_filename)) {
+ return false;
+ }
+
+ $tpl = new Layout();
+ $tpl->setTemplate("Emailtemplates/invoice/invoice-email");
+
+ $pdf_vars = [
+ "invoice" => $this
+ ];
+
+ foreach($pdf_vars as $name => $val) {
+ $tpl->set($name, $val);
+ }
+
+ $body = $tpl->render();
+ $values = $tpl->getReturnedValue();
+
+ $subject = $values['subject'];
+ $from = $values['from_email'];
+ $from_name = $values['from_email_name'];
+ if($to_email) {
+ $to = $to_email;
+ } else {
+ $to = trim($this->email);
+ }
+
+ if(!$to) {
+ $this->log->error(__METHOD__.": ManualInvoice ".$this->invoice_number." missing email");
+ }
+
+
+
+ if(!$subject || !$from || !$from_name || !$to) {
+ $this->log->warn(__METHOD__.": ManualInvoice ".$this->invoice_number." could not be sent. Values missing. (subject: '$subject', from: '$from_name', from_email: '$from', to: '$to')");
+ return false;
+ } else {
+ $email = new Emailnotification("ManualInvoice", $this->id);
+ $email->setSubject($subject);
+ $email->setBody($body);
+ $email->setFrom($from, $from_name);
+ $email->setTo($to);
+ $email->setHeader("X-".MFAPPNAME."-Iid", $this->id);
+ $email->addAttachment($pdf_filename, null, $pdf->filename, "application/pdf");
+ $email->send();
+ $this->log->info(__METHOD__.": Sending ManualInvoice ".$this->invoice_number." to $to");
+ }
+
+ return true;
+ }
+
+ public function getProperty($name) {
+ if($this->$name == null) {
+
+ if(!$this->id) {
+ return null;
+ }
+
+
+ if($name == "positions") {
+ $positions = ManualInvoicepositionModel::search(["manualinvoice_id" => $this->id]);
+ $this->positions = $positions;
+ return $this->positions;
+ }
+
+ if($name == "pdf") {
+ $ifile = ManualInvoiceFileModel::getFirst(["manualinvoice_id" => $this->id]);
+ if(!$ifile) return null;
+
+ $file = $ifile->file;
+ if(!$file) return null;
+
+ $this->pdf = $file;
+ return $this->pdf;
+ }
+
+ if($name == "creator") {
+ $this->creator = mfValuecache::singleton()->get("Worker-id-".$this->create_by);
+ if($this->creator === null) {
+ $this->creator = new User($this->create_by);
+ if($this->creator->id) {
+ mfValuecache::singleton()->set("Worker-id-".$this->create_by, $this->creator);
+ }
+ }
+ return $this->creator;
+ }
+
+ if($name == "editor") {
+ $this->editor = mfValuecache::singleton()->get("Worker-id-".$this->edit_by);
+ if($this->editor === null) {
+ $this->editor = new User($this->edit_by);
+ if($this->editor->id) {
+ mfValuecache::singleton()->set("Worker-id-".$this->edit_by, $this->editor);
+ }
+ }
+ return $this->editor;
+ }
+
+ $classname = ucfirst($name);
+ $idfield = $name."_id";
+ $this->$name = mfValuecache::singleton()->get("mfObjectmodel-$name-".$this->$idfield);
+ if(!$this->$name) {
+ $this->$name = new $classname($this->$idfield);
+ }
+
+ if($this->$name->id) {
+ mfValuecache::singleton()->set("mfObjectmodel-$name-".$this->$name->id, $this->$name);
+ return $this->$name;
+ } else {
+ return null;
+ }
+
+ }
+
+ return $this->$name;
+ }
+}
diff --git a/application/ManualInvoice/ManualInvoiceController.php b/application/ManualInvoice/ManualInvoiceController.php
index 91b019515..973f4b2d0 100644
--- a/application/ManualInvoice/ManualInvoiceController.php
+++ b/application/ManualInvoice/ManualInvoiceController.php
@@ -5,18 +5,277 @@ class ManualInvoiceController extends TTCrud
protected string $headerTitle = 'Manuelle Rechnungen';
protected bool $createText = false;
+ protected array $additionalJS = ["js/pages/ManualInvoice/ManualInvoice.js"];
+ protected array $additionalHead = ["
"];
+
//@formatter:off
protected array $columns = [
- ['key' => 'invoiceNumber', 'text' => 'Rechnungsnr.', 'table' => ['sortable' => true, 'filter' => 'search']],
- ['key' => 'customerName', 'text' => 'Kunde', 'table' => ['sortable' => true, 'filter' => 'search']],
- ['key' => 'invoiceDate', 'text' => 'Datum', 'table' => ['sortable' => true, 'filter' => 'date']],
- ['key' => 'totalAmount', 'text' => 'Betrag', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']],
- ['key' => 'status', 'text' => 'Status', 'table' => ['filter' => 'select', 'filterOptions' => [
- ['value' => 'draft', 'text' => 'Entwurf'],
- ['value' => 'sent', 'text' => 'Gesendet'],
- ['value' => 'paid', 'text' => 'Bezahlt'],
+ ['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' => '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]],
];
//@formatter:on
-}
\ No newline at end of file
+
+ protected function createPDFAction($returnFilename = false) {
+ // Get data from POST for preview or from database for saved invoice
+ $post = json_decode(file_get_contents('php://input'), true);
+
+ if (isset($post['preview']) && $post['preview'] === true) {
+ // Create temporary invoice object from POST data for preview
+ $invoice = (object)[];
+ $invoice->id = 0;
+ $invoice->invoice_number = $post['invoice_number'] ?? null;
+ $invoice->invoice_date = $post['invoice_date'] ?? time();
+ $invoice->customer_number = $post['customer_number'] ?? 0;
+ $invoice->fibu_account_number = $post['fibu_account_number'] ?? 0;
+ $invoice->company = $post['company'] ?? '';
+ $invoice->firstname = $post['firstname'] ?? '';
+ $invoice->lastname = $post['lastname'] ?? '';
+ $invoice->street = $post['street'] ?? '';
+ $invoice->zip = $post['zip'] ?? '';
+ $invoice->city = $post['city'] ?? '';
+ $invoice->country = $post['country'] ?? 'Österreich';
+ $invoice->email = $post['email'] ?? '';
+ $invoice->uid = $post['uid'] ?? '';
+ $invoice->tax_text = $post['tax_text'] ?? '';
+ $invoice->billing_type = $post['billing_type'] ?? 'invoice';
+ $invoice->total = $post['total'] ?? 0;
+ $invoice->total_gross = $post['total_gross'] ?? 0;
+
+ $positions = [];
+ foreach ($post['positions'] ?? [] as $pos) {
+ $positions[] = (object)$pos;
+ }
+ } else {
+ // Load from database
+ $id = $this->request->id ?? $post['id'] ?? null;
+ if (!$id) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Rechnung wurde nicht gefunden']);
+ return;
+ }
+
+ $invoice = new ManualInvoice($id);
+ if (!$invoice->id) {
+ http_response_code(500);
+ self::returnJson(['success' => false, 'message' => 'Rechnung wurde nicht gefunden']);
+ return;
+ }
+
+ $positions = $invoice->getProperty('positions');
+ }
+
+ // Calculate VAT totals
+ $vat = [];
+ foreach ($positions as $p) {
+ $vatrate = is_object($p) ? $p->vatrate : $p['vatrate'];
+ $price_gross = is_object($p) ? $p->price_gross : ($p['price_gross'] ?? 0);
+ $price_total = is_object($p) ? $p->price_total : ($p['price_total'] ?? 0);
+
+ if (!array_key_exists($vatrate, $vat)) {
+ $vat[$vatrate] = 0;
+ }
+ $vat[$vatrate] += $price_gross - $price_total;
+ }
+
+ // Convert positions array to objects if needed
+ $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
+ ];
+
+ // Replace placeholders in header
+ $headerHtml = file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_HEADER.html");
+ $headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_1 }}", $invoice->company ? $invoice->company : "", $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_2 }}", trim($invoice->firstname . " " . $invoice->lastname), $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_3 }}", $invoice->street ?? '', $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_4 }}", ($invoice->zip ?? '') . " " . ($invoice->city ?? ''), $headerHtml);
+ $headerHtml = str_replace("{{ addressLine_5 }}", ($invoice->country ?? '') != "Österreich" ? ($invoice->country ?? '') : "", $headerHtml);
+ $headerHtml = str_replace("{{ customerNumber }}", $invoice->customer_number ?? '', $headerHtml);
+ $headerHtml = str_replace("{{ billingAccount }}", $invoice->fibu_account_number ?? '', $headerHtml);
+ $headerHtml = str_replace("{{ invoiceNumber }}", $invoice->invoice_number ?? "VORSCHAU", $headerHtml);
+ $headerHtml = str_replace("{{ invoiceDate }}", date("d.m.Y", $invoice->invoice_date ?? time()), $headerHtml);
+ $headerHtml = str_replace("{{ vatHtml }}", ($invoice->uid ?? '') ? "
Ihre UID: " . $invoice->uid . " " : "", $headerHtml);
+
+ // Generate QR code for SEPA payment
+ $qrCode = $this->generateSepaQRCode($invoice->invoice_number ?? "VORSCHAU", round($invoice->total_gross ?? 0, 2));
+ $headerHtml = str_replace("{{ qrCodeSrc }}", $qrCode, $headerHtml);
+
+ $headerFile = BASEDIR . "/var/temp/manualinvoice_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
+ file_put_contents($headerFile, $headerHtml);
+
+ // Replace placeholders in footer
+ $footerHtml = file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_FOOTER.html");
+ $footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml);
+ $footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml);
+ $footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml);
+ $footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml);
+
+ $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);
+ $wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile";
+ $filename = $pdf->render($wkhtmltopdfArgs);
+
+ if ($returnFilename === true) return $filename;
+
+ // Return the PDF inline for preview
+ 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) {
+ $this->layout()->setFlash("Rechnung nicht gefunden", "error");
+ $this->redirect("ManualInvoice");
+ }
+
+ $invoice = new ManualInvoice($id);
+ if (!$invoice->id) {
+ $this->layout()->setFlash("Rechnung nicht gefunden", "error");
+ $this->redirect("ManualInvoice");
+ }
+
+ // Use createPDFAction to get filename
+ $pdf_filename = $this->createPDFAction(true);
+
+ if(!$pdf_filename || !file_exists($pdf_filename)) {
+ $this->layout()->setFlash("PDF-Datei konnte nicht erstellt werden");
+ $this->redirect("ManualInvoice");
+ }
+
+ header('Content-Type: application/octet-stream');
+ header('Content-disposition: attachment; filename="'.$invoice->invoice_number.'.pdf"');
+ header('Content-Transfer-Encoding: binary');
+ header('Cache-Control: must-revalidate, post-check=0, pre-check=0');
+ header('Content-Type: application/pdf');
+ header("Content-Length: " . filesize($pdf_filename));
+
+ readfile($pdf_filename);
+ exit;
+ }
+
+ protected function beforeCreate(&$data): bool {
+ // Generate invoice number if not provided
+ if (empty($data['invoice_number'])) {
+ $data['invoice_number'] = ManualInvoiceModel::getNextInvoiceNumber();
+ }
+
+ // Set default values
+ if (empty($data['invoice_date'])) {
+ $data['invoice_date'] = time();
+ }
+
+ return true;
+ }
+
+ protected function afterCreate($data) {
+ $invoiceId = $data['id'];
+
+ // Save positions
+ if (isset($data['positions']) && is_array($data['positions'])) {
+ foreach ($data['positions'] as $position) {
+ $position['manualinvoice_id'] = $invoiceId;
+ $posModel = ManualInvoicepositionModel::create($position);
+ $posModel->save();
+ }
+ }
+
+ // Recalculate totals
+ $this->recalculateTotals($invoiceId);
+ }
+
+ protected function beforeUpdate(&$data): bool {
+ return true;
+ }
+
+ protected function afterUpdate($data) {
+ $invoiceId = $data['id'];
+
+ // Delete existing positions
+ $existingPositions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]);
+ foreach ($existingPositions as $pos) {
+ $pos->delete();
+ }
+
+ // Save new positions
+ if (isset($data['positions']) && is_array($data['positions'])) {
+ foreach ($data['positions'] as $position) {
+ $position['manualinvoice_id'] = $invoiceId;
+ $posModel = ManualInvoicepositionModel::create($position);
+ $posModel->save();
+ }
+ }
+
+ // Recalculate totals
+ $this->recalculateTotals($invoiceId);
+ }
+
+ protected function recalculateTotals($invoiceId) {
+ $invoice = new ManualInvoice($invoiceId);
+ $positions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]);
+
+ $total = 0;
+ $total_gross = 0;
+
+ foreach ($positions as $pos) {
+ $total += $pos->price_total;
+ $total_gross += $pos->price_gross;
+ }
+
+ $invoice->total = $total;
+ $invoice->total_gross = $total_gross;
+ $invoice->save();
+ }
+
+ protected function customRowsHandler($rows) {
+ foreach ($rows as &$row) {
+ // Add customer name
+ $row->customerName = trim(($row->company ? $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
+001
+1
+SCT
+$xinonBIC
+$xinonOwner
+$xinonIBAN
+EUR$amount
+XINO
+$paymentReference
+
+XINON GmbH";
+
+ return (new \chillerlan\QRCode\QRCode)->render($epc);
+ }
+}
diff --git a/application/ManualInvoice/ManualInvoiceModel.php b/application/ManualInvoice/ManualInvoiceModel.php
index 3f3eb170d..e9324a497 100644
--- a/application/ManualInvoice/ManualInvoiceModel.php
+++ b/application/ManualInvoice/ManualInvoiceModel.php
@@ -1,186 +1,408 @@
1, 'invoiceNumber' => 'RE-2025-001', 'customerName' => 'Musterfirma GmbH', 'billingAddressId' => 1,
- 'invoiceDate' => strtotime('2025-09-11'), 'dueDate' => strtotime('2025-09-25'), 'totalAmount' => 948.00, 'status' => 'paid',
- 'positions' => json_encode([
- ['product_name' => 'IT-Support-Stunden', 'product_info' => 'Remote-Hilfe für Mitarbeiter', 'start_date' => '2025-09-10', 'end_date' => '2025-09-10', 'amount' => 2, 'price' => 120.00, 'vatrate' => 20],
- ['product_name' => 'Netzwerk-Switch 24-Port', 'product_info' => 'Modell: XYZ-24G', 'start_date' => '2025-09-10', 'end_date' => '2025-09-10', 'amount' => 1, 'price' => 550.00, 'vatrate' => 20],
- ]),
- 'closingText' => 'Vielen Dank für Ihren Auftrag.', 'taxText' => ''
- ],
- [
- 'id' => 2, 'invoiceNumber' => 'RE-2025-002', 'customerName' => 'Beispiel AG', 'billingAddressId' => 2,
- 'invoiceDate' => strtotime('2025-09-14'), 'dueDate' => strtotime('2025-09-28'), 'totalAmount' => 720.00, 'status' => 'sent',
- 'positions' => json_encode([
- ['product_name' => 'Beratung Digitalisierungsstrategie', 'product_info' => 'Workshop am 05.09.2025', 'start_date' => '2025-09-05', 'end_date' => '2025-09-05', 'amount' => 4, 'price' => 150.00, 'vatrate' => 20],
- ]),
- 'closingText' => 'Wir freuen uns auf eine weiterhin gute Zusammenarbeit.', 'taxText' => ''
- ],
- [
- 'id' => 3, 'invoiceNumber' => 'RE-2025-003', 'customerName' => 'John Doe Services', 'billingAddressId' => 3,
- 'invoiceDate' => strtotime('2025-09-16'), 'dueDate' => strtotime('2025-09-30'), 'totalAmount' => 912.00, 'status' => 'draft',
- 'positions' => json_encode([
- ['product_name' => 'Kabelverlegung LWL', 'product_info' => 'Inhouse-Verkabelung Bürogebäude', 'start_date' => '2025-09-15', 'end_date' => '2025-09-15', 'amount' => 8, 'price' => 85.00, 'vatrate' => 20],
- ['product_name' => 'LWL-Kabel 8 Fasern', 'product_info' => 'Pro Meter', 'start_date' => '2025-09-15', 'end_date' => '2025-09-15', 'amount' => 100, 'price' => 0.80, 'vatrate' => 20],
- ]),
- 'closingText' => 'Bei Fragen stehen wir Ihnen gerne zur Verfügung.', 'taxText' => ''
- ],
- [
- 'id' => 4, 'invoiceNumber' => 'RE-2025-004', 'customerName' => 'Bau & Co KG', 'billingAddressId' => 4,
- 'invoiceDate' => strtotime('2025-09-06'), 'dueDate' => strtotime('2025-09-20'), 'totalAmount' => 1890.00, 'status' => 'paid',
- 'positions' => json_encode([
- ['product_name' => 'Netzwerk-Grundinstallation Baustelle', 'product_info' => 'Containerdorf Einrichtung', 'start_date' => '2025-09-02', 'end_date' => '2025-09-02', 'amount' => 1, 'price' => 1200.00, 'vatrate' => 20],
- ['product_name' => 'Stunden Elektriker', 'product_info' => 'Anpassungen Verteilerkasten', 'start_date' => '2025-09-02', 'end_date' => '2025-09-02', 'amount' => 5, 'price' => 75.00, 'vatrate' => 20],
- ]),
- 'closingText' => 'Vielen Dank für Ihren Auftrag.', 'taxText' => ''
- ],
- [
- 'id' => 5, 'invoiceNumber' => 'RE-2025-005', 'customerName' => 'Creative Solutions', 'billingAddressId' => 5,
- 'invoiceDate' => strtotime('2025-09-15'), 'dueDate' => strtotime('2025-09-29'), 'totalAmount' => 1920.00, 'status' => 'sent',
- 'positions' => json_encode([
- ['product_name' => 'Web-Entwicklung', 'product_info' => 'Umsetzung Landingpage "Herbst-Aktion"', 'start_date' => '2025-09-01', 'end_date' => '2025-09-12', 'amount' => 10, 'price' => 110.00, 'vatrate' => 20],
- ['product_name' => 'Domain-Registrierung (.at)', 'product_info' => 'herbst-aktion.at', 'start_date' => '2025-09-01', 'end_date' => '2026-08-31', 'amount' => 1, 'price' => 500.00, 'vatrate' => 20],
- ]),
- 'closingText' => 'Wir freuen uns auf eine weiterhin gute Zusammenarbeit.', 'taxText' => ''
- ],
- [
- 'id' => 6, 'invoiceNumber' => 'RE-2025-006', 'customerName' => 'Logistik Express', 'billingAddressId' => 6,
- 'invoiceDate' => strtotime('2025-08-28'), 'dueDate' => strtotime('2025-09-11'), 'totalAmount' => 3432.00, 'status' => 'paid',
- 'positions' => json_encode([
- ['product_name' => 'Software-Lizenz WMS Pro', 'product_info' => 'Jahreslizenz für 10 User', 'start_date' => '2025-09-01', 'end_date' => '2026-08-31', 'amount' => 1, 'price' => 2500.00, 'vatrate' => 20],
- ['product_name' => 'Mitarbeiterschulung WMS', 'product_info' => 'Vor Ort am 27.08.2025', 'start_date' => '2025-08-27', 'end_date' => '2025-08-27', 'amount' => 4, 'price' => 90.00, 'vatrate' => 20],
- ]),
- 'closingText' => 'Vielen Dank für Ihren Auftrag.', 'taxText' => ''
- ],
- [
- 'id' => 7, 'invoiceNumber' => 'RE-2025-007', 'customerName' => 'Gastro Profi', 'billingAddressId' => 7,
- 'invoiceDate' => strtotime('2025-09-10'), 'dueDate' => strtotime('2025-09-24'), 'totalAmount' => 2577.60, 'status' => 'draft',
- 'positions' => json_encode([
- ['product_name' => 'Kassensystem "GastroTouch"', 'product_info' => '2x Terminal, 1x Bondrucker', 'start_date' => '2025-09-09', 'end_date' => '2025-09-09', 'amount' => 2, 'price' => 899.00, 'vatrate' => 20],
- ['product_name' => 'Installationspauschale', 'product_info' => 'Inkl. Einschulung', 'start_date' => '2025-09-09', 'end_date' => '2025-09-09', 'amount' => 1, 'price' => 350.00, 'vatrate' => 20],
- ]),
- 'closingText' => 'Bei Fragen stehen wir Ihnen gerne zur Verfügung.', 'taxText' => ''
- ],
- [
- 'id' => 8, 'invoiceNumber' => 'RE-2025-008', 'customerName' => 'Sicherheitsdienst Huber', 'billingAddressId' => 8,
- 'invoiceDate' => strtotime('2025-09-01'), 'dueDate' => strtotime('2025-09-15'), 'totalAmount' => 1782.00, 'status' => 'sent',
- 'positions' => json_encode([
- ['product_name' => 'IP Kamera 4K Dome', 'product_info' => 'Modell SEC-4K-D', 'start_date' => '2025-08-29', 'end_date' => '2025-08-29', 'amount' => 8, 'price' => 180.00, 'vatrate' => 20],
- ['product_name' => 'Monatliche Wartungspauschale', 'product_info' => 'September 2025', 'start_date' => '2025-09-01', 'end_date' => '2025-09-30', 'amount' => 1, 'price' => 45.00, 'vatrate' => 20],
- ]),
- 'closingText' => 'Wir freuen uns auf eine weiterhin gute Zusammenarbeit.', 'taxText' => ''
- ],
- [
- 'id' => 9, 'invoiceNumber' => 'RE-2025-009', 'customerName' => 'Praxis Dr. Eder', 'billingAddressId' => 9,
- 'invoiceDate' => strtotime('2025-09-12'), 'dueDate' => strtotime('2025-09-26'), 'totalAmount' => 3090.00, 'status' => 'draft',
- 'positions' => json_encode([
- ['product_name' => 'Arbeitsstunden IT-Migration', 'product_info' => 'Serverumzug und Client-Setup', 'start_date' => '2025-09-11', 'end_date' => '2025-09-11', 'amount' => 5, 'price' => 95.00, 'vatrate' => 20],
- ['product_name' => 'Server-Hardware "MedServ"', 'product_info' => 'Spez. für Arztpraxen', 'start_date' => '2025-09-11', 'end_date' => '2025-09-11', 'amount' => 1, 'price' => 1800.00, 'vatrate' => 20],
- ['product_name' => 'Datensicherungslösung "CloudSafe"', 'product_info' => 'Einrichtungspauschale', 'start_date' => '2025-09-11', 'end_date' => '2025-09-11', 'amount' => 1, 'price' => 300.00, 'vatrate' => 20],
- ]),
- 'closingText' => 'Bei Fragen stehen wir Ihnen gerne zur Verfügung.', 'taxText' => ''
- ],
- [
- 'id' => 10, 'invoiceNumber' => 'RE-2025-010', 'customerName' => 'Architekturbüro Planweit', 'billingAddressId' => 10,
- 'invoiceDate' => strtotime('2025-09-08'), 'dueDate' => strtotime('2025-09-22'), 'totalAmount' => 357.60, 'status' => 'paid',
- 'positions' => json_encode([
- ['product_name' => 'Plotter Service', 'product_info' => 'Wartung und Reinigung', 'start_date' => '2025-09-04', 'end_date' => '2025-09-04', 'amount' => 1, 'price' => 250.00, 'vatrate' => 20],
- ['product_name' => 'Netzwerkkabel Cat7', 'product_info' => 'Pro Meter', 'start_date' => '2025-09-04', 'end_date' => '2025-09-04', 'amount' => 40, 'price' => 1.20, 'vatrate' => 20],
- ]),
- 'closingText' => 'Vielen Dank für Ihren Auftrag.', 'taxText' => ''
- ],
-];
-
-return $mockData;
-}
+class ManualInvoiceModel {
+ public $invoice_number;
+ public $invoice_date;
+ public $owner_id;
+ public $billingaddress_id;
+ public $customer_number;
+ public $fibu_account_number;
+ public $fibu_payment_due;
+ public $fibu_payment_skonto;
+ public $fibu_payment_skonto_rate;
+ public $sepa_date;
+ public $sepa_id;
+ public $sepa_last_date;
+ public $fibu_cost_area;
+ public $fibu_cost_account;
+ public $fibu_cost_account_legacy;
+ public $fibu_taxcode;
+ public $tax_text;
+ public $company;
+ public $firstname;
+ public $lastname;
+ public $street;
+ public $zip;
+ public $city;
+ public $country;
+ public $email;
+ public $uid;
+ public $billing_type;
+ public $billing_delivery;
+ public $bank_account_bank;
+ public $bank_account_owner;
+ public $bank_account_iban;
+ public $bank_account_bic;
+ public $total;
+ public $total_gross;
+ public $vatgroup_id;
+ public $bmd_export_date;
+ public $date_delivered;
+ public $create_by;
+ public $edit_by;
+ public $create;
+ public $edit;
-class ManualInvoiceModel extends TTCrudBaseModel {
- public int $id;
- public ?string $invoiceNumber;
- public ?int $invoiceDate;
- public ?int $dueDate;
- public int $billingAddressId;
- public ?string $customerName;
- public ?float $totalAmount;
- public string $status;
- public string $positions;
- public string $closingText;
- public string $taxText;
+ public static function create($data) {
+ $invoice = new ManualInvoice();
- private static function applyFilter(array $data, array $filter): array {
- if (empty($filter)) {
- return $data;
+ $me = new User();
+ $me->loadMe();
+
+ // Set audit fields
+ $invoice->create_by = $me->id;
+ $invoice->edit_by = $me->id;
+ $invoice->create = time();
+ $invoice->edit = time();
+
+ // Set invoice fields
+ foreach($data as $field => $value) {
+ if(property_exists($invoice, $field) && $field != 'id') {
+ $invoice->$field = $value;
+ }
}
- return array_filter($data, function ($row) use ($filter) {
- foreach ($filter as $key => $value) {
- if (!isset($row[$key]) || empty($value)) {
- continue;
- }
- if (is_array($value)) { // Handle date ranges
- if (isset($value['from']) && $row[$key] < $value['from']) return false;
- if (isset($value['to']) && $row[$key] > $value['to']) return false;
- } else if (is_array($row[$key])) {
- if (!in_array($value, $row[$key])) return false;
- } else if (stripos($row[$key], $value) === false) {
- return false;
+
+ // Set defaults
+ if (!$invoice->billing_type) {
+ $invoice->billing_type = 'invoice';
+ }
+ if (!$invoice->billing_delivery) {
+ $invoice->billing_delivery = 'email';
+ }
+ if (!$invoice->total) {
+ $invoice->total = 0;
+ }
+ if (!$invoice->total_gross) {
+ $invoice->total_gross = 0;
+ }
+ if (!$invoice->vatgroup_id) {
+ $invoice->vatgroup_id = 1; // Default VAT group
+ }
+ if (!$invoice->owner_id) {
+ $invoice->owner_id = 0;
+ }
+ if (!$invoice->billingaddress_id) {
+ $invoice->billingaddress_id = 0;
+ }
+ if (!$invoice->customer_number) {
+ $invoice->customer_number = 0;
+ }
+
+ if ($invoice->save()) {
+ return $invoice->id;
+ }
+
+ return false;
+ }
+
+ public static function getNextInvoiceNumber() {
+ $last_invoice_num = self::getLastInvoiceNumber();
+
+ if(!$last_invoice_num) {
+ return "MRN".date("Y")."-X000001";
+ }
+
+ $year_part = 0;
+ $num_part = 0;
+
+ $m = [];
+ if(preg_match('/^MRN(\d+)-X(\d+)$/', $last_invoice_num, $m)) {
+ if(array_key_exists(1, $m)) {
+ $year_part = $m[1];
+ if(array_key_exists(2, $m)) {
+ $num_part = $m[2];
}
}
- return true;
- });
+ }
+
+ if(!$year_part || !$num_part) {
+ return "MRN".date("Y")."-X000001";
+ }
+
+ if(date("Y") == $year_part) {
+ $new_year_part = $year_part;
+ $new_num_part = $num_part + 1;
+ } else {
+ $new_year_part = date("Y");
+ $new_num_part = 1;
+ }
+
+ $new_invoice_num = "MRN$new_year_part-X".str_pad($new_num_part,"6", "0", STR_PAD_LEFT);
+ return $new_invoice_num;
}
- public static function getAll($filter = [], $limit = null, $offset = 0, $order = ["key" => null]): array
+ public static function getLastInvoiceNumber() {
+ $last_invoice = self::getLast(["invoice_number" => true]);
+ if(!$last_invoice || !$last_invoice->invoice_number) {
+ return false;
+ }
+ return $last_invoice->invoice_number;
+ }
- {
- $mockData = getMockData();
- $filteredData = self::applyFilter($mockData, $filter);
+ public static function getAll($filter = [], $limit = null, $offset = 0, $order = ["key" => null]) {
+ $items = [];
+
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT ManualInvoice.* FROM ManualInvoice WHERE $where";
if ($order['key'] !== null) {
- usort($filteredData, function ($a, $b) use ($order) {
- if ($a[$order['key']] == $b[$order['key']]) return 0;
- if ($order['order'] === 'ASC') {
- return $a[$order['key']] < $b[$order['key']] ? -1 : 1;
- } else {
- return $a[$order['key']] > $b[$order['key']] ? -1 : 1;
- }
- });
+ $orderDir = isset($order['order']) ? $order['order'] : 'ASC';
+ $sql .= " ORDER BY " . $order['key'] . " " . $orderDir;
+ } else {
+ $sql .= " ORDER BY invoice_number";
}
- if ($limit !== null) {
- return array_slice($filteredData, $offset, $limit);
+ if($limit !== null) {
+ $sql .= " LIMIT " . intval($offset) . ", " . intval($limit);
}
- return $filteredData;
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ while($data = $db->fetch_object($res)) {
+ $items[] = new ManualInvoice($data);
+ }
+ }
+ return $items;
}
- public static function count($filter = []): int {
- $mockData = getMockData();
- return count(self::applyFilter($mockData, $filter));
- }
+ public static function getFirst($filter) {
+ $db = FronkDB::singleton();
- public static function get($id) {
- $mockData = getMockData();
- foreach ($mockData as $row)
- if ($row['id'] == $id)
- return new self($row);
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT ManualInvoice.* FROM ManualInvoice WHERE $where ORDER BY invoice_number LIMIT 1";
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ $item = new ManualInvoice($data);
+ if($item->id) {
+ return $item;
+ } else {
+ return null;
+ }
+ }
return null;
}
- public static function create($data) {
- error_log("ManualInvoiceModel::create called with: " . json_encode($data));
- return time();
+ public static function getLast($filter) {
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT ManualInvoice.* FROM ManualInvoice WHERE $where ORDER BY invoice_number DESC LIMIT 1";
+
+ mfLoghandler::singleton()->debug($sql);
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ $item = new ManualInvoice($data);
+ if($item->id) {
+ return $item;
+ } else {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public static function count($filter = []) {
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT COUNT(*) as cnt FROM ManualInvoice WHERE $where";
+
+ mfLoghandler::singleton()->debug($sql);
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ return $data->cnt;
+ }
+ return 0;
+ }
+
+ public static function search($filter, $limit = false, $order = false) {
+ $items = [];
+
+ if(!$order) {
+ $order = "invoice_number ASC";
+ }
+
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT ManualInvoice.* FROM ManualInvoice WHERE $where ORDER BY $order";
+
+ if(is_array($limit) && count($limit)) {
+ if(is_numeric($limit['start']) && is_numeric($limit['count'])) {
+ $sql .= " LIMIT ".$limit['start'].", ".$limit['count'];
+ } elseif(is_numeric($limit['count'])) {
+ $sql .= " LIMIT ".$limit['count'];
+ }
+ }
+
+ mfLoghandler::singleton()->debug($sql);
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ while($data = $db->fetch_object($res)) {
+ $items[$data->id] = new ManualInvoice($data);
+ }
+ }
+
+ return $items;
+ }
+
+ private static function getSqlFilter($filter) {
+ $where = "1=1 ";
+ $db = FronkDB::singleton();
+
+ if(array_key_exists("id", $filter)) {
+ $id = $filter['id'];
+ if(is_numeric($id)) {
+ $where .= " AND ManualInvoice.id like '%$id%'";
+ }
+ }
+
+ if(array_key_exists("invoice_number", $filter)) {
+ $invoice_number = $filter['invoice_number'];
+ if($invoice_number === true) {
+ $where .= " AND ManualInvoice.invoice_number IS NOT NULL AND ManualInvoice.invoice_number <> ''";
+ } elseif($invoice_number) {
+ $invoice_number = $db->escape($invoice_number);
+ $where .= " AND ManualInvoice.invoice_number='$invoice_number'";
+ } elseif($invoice_number === null || $invoice_number === false) {
+ $where .= " AND ManualInvoice.invoice_number IS NULL";
+ }
+ }
+
+ if(array_key_exists("invoice_number%", $filter)) {
+ $invoice_number = $filter['invoice_number%'];
+ if($invoice_number) {
+ $where .= " AND ManualInvoice.invoice_number LIKE '%$invoice_number%'";
+ }
+ }
+
+ if(array_key_exists("invoice_date", $filter)) {
+ $invoice_date = $filter['invoice_date'];
+ if($invoice_date) {
+ $where .= " AND ManualInvoice.invoice_date='$invoice_date'";
+ } elseif($invoice_date === null || $invoice_date === false) {
+ $where .= " AND ManualInvoice.invoice_date IS NULL";
+ }
+ }
+
+ if(array_key_exists("invoice_date>=", $filter)) {
+ $invoice_date = $db->escape($filter['invoice_date>=']);
+ if($invoice_date) {
+ $where .= " AND ManualInvoice.invoice_date >= '$invoice_date'";
+ }
+ }
+
+ if(array_key_exists("invoice_date<=", $filter)) {
+ $invoice_date = $db->escape($filter['invoice_date<=']);
+ if($invoice_date) {
+ $where .= " AND ManualInvoice.invoice_date <= '$invoice_date'";
+ }
+ }
+
+ if(array_key_exists("owner_id", $filter)) {
+ $owner_id = $filter['owner_id'];
+ if(is_numeric($owner_id)) {
+ $where .= " AND ManualInvoice.owner_id=$owner_id";
+ }
+ }
+
+ if(array_key_exists("billingaddress_id", $filter)) {
+ $billingaddress_id = $filter['billingaddress_id'];
+ if(is_numeric($billingaddress_id)) {
+ $where .= " AND ManualInvoice.billingaddress_id=$billingaddress_id";
+ }
+ }
+
+ if(array_key_exists("customer_number", $filter)) {
+ $customer_number = $filter['customer_number'];
+ if(is_numeric($customer_number)) {
+ $where .= " AND ManualInvoice.customer_number LIKE $customer_number";
+ }
+ }
+
+ if(array_key_exists("billing_type", $filter)) {
+ $billing_type = $db->escape($filter['billing_type']);
+ if($billing_type) {
+ $where .= " AND ManualInvoice.billing_type LIKE '$billing_type'";
+ }
+ }
+
+ if(array_key_exists("billing_delivery", $filter)) {
+ $billing_delivery = $db->escape($filter['billing_delivery']);
+ if($billing_delivery) {
+ $where .= " AND ManualInvoice.billing_delivery LIKE '$billing_delivery'";
+ }
+ }
+
+ if(array_key_exists("add-where", $filter)) {
+ $where .= " ".$filter['add-where'];
+ }
+
+ return $where;
+ }
+
+ public static function get($id) {
+ return self::getFirst(["id" => $id]);
}
public static function update($data) {
- error_log("ManualInvoiceModel::update called with: " . json_encode($data));
- return 1;
+ if (!isset($data['id'])) {
+ return 0;
+ }
+
+ $invoice = new ManualInvoice($data['id']);
+ if (!$invoice->id) {
+ return 0;
+ }
+
+ $me = new User();
+ $me->loadMe();
+
+ // Set audit fields
+ $invoice->edit_by = $me->id;
+ $invoice->edit = time();
+
+ // Update fields
+ foreach($data as $field => $value) {
+ if(property_exists($invoice, $field) && $field != 'id' && $field != 'create' && $field != 'create_by') {
+ $invoice->$field = $value;
+ }
+ }
+
+ if ($invoice->save()) {
+ return 1;
+ }
+
+ return 0;
}
public static function delete($id) {
- error_log("ManualInvoiceModel::delete called with ID: " . $id);
- return 1;
+ $invoice = new ManualInvoice($id);
+ if (!$invoice->id) {
+ return 0;
+ }
+
+ // Delete positions first
+ $positions = ManualInvoicepositionModel::search(['manualinvoice_id' => $id]);
+ foreach ($positions as $pos) {
+ $pos->delete();
+ }
+
+ // Delete invoice
+ if ($invoice->delete()) {
+ return 1;
+ }
+
+ return 0;
}
-}
\ No newline at end of file
+}
diff --git a/application/ManualInvoiceposition/ManualInvoiceposition.php b/application/ManualInvoiceposition/ManualInvoiceposition.php
new file mode 100644
index 000000000..c6fac1fe7
--- /dev/null
+++ b/application/ManualInvoiceposition/ManualInvoiceposition.php
@@ -0,0 +1,35 @@
+options) {
+ return null;
+ }
+
+ $options = json_decode($this->options, true);
+ if(!$options || !is_array($options)) {
+ return null;
+ }
+
+ if(array_key_exists($key, $options)) {
+ return $options[$key];
+ }
+
+ return null;
+ }
+
+ public function setOption($key, $value) {
+ if(!$this->options) {
+ $this->options = json_encode([]);
+ }
+
+ $options = json_decode($this->options, true);
+ if(!is_array($options)) {
+ $options = [];
+ }
+
+ $options[$key] = $value;
+ $this->options = json_encode($options);
+ }
+}
diff --git a/application/ManualInvoiceposition/ManualInvoicepositionModel.php b/application/ManualInvoiceposition/ManualInvoicepositionModel.php
new file mode 100644
index 000000000..884933fd7
--- /dev/null
+++ b/application/ManualInvoiceposition/ManualInvoicepositionModel.php
@@ -0,0 +1,160 @@
+ $value) {
+ if(property_exists(get_called_class(), $field)) {
+ $model ->$field = $value;
+ }
+ }
+
+ $me = new User();
+ $me->loadMe();
+
+ if($model->create_by === null) {
+ $model->create_by = $me->id;
+ }
+ if($model->edit_by === null) {
+ $model->edit_by = $me->id;
+ }
+
+ return $model;
+ }
+
+ public static function getAll() {
+ $items = [];
+
+ $db = FronkDB::singleton();
+
+ $res = $db->select("ManualInvoiceposition", "*", "1 = 1 ORDER BY manualinvoice_id,contract_id,start_date,matchcode");
+ if($db->num_rows($res)) {
+ while($data = $db->fetch_object($res)) {
+ $items[] = new ManualInvoiceposition($data);
+ }
+ }
+ return $items;
+
+ }
+
+ public static function getFirst($filter) {
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT * FROM ManualInvoiceposition
+ WHERE $where
+ ORDER BY manualinvoice_id,contract_id,start_date,matchcode LIMIT 1";
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ $item = new ManualInvoiceposition($data);
+ if($item->id) {
+ return $item;
+ } else {
+ return null;
+ }
+ }
+ return null;
+ }
+
+ public static function count($filter) {
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT COUNT(*) as cnt FROM ManualInvoiceposition WHERE $where";
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ $data = $db->fetch_object($res);
+ return $data->cnt;
+ }
+ return 0;
+ }
+
+ public static function search($filter, $limit = false, $order = false) {
+ $items = [];
+
+ if(!$order) {
+ $order = "manualinvoice_id,contract_id,id ASC";
+ }
+
+ $db = FronkDB::singleton();
+
+ $where = self::getSqlFilter($filter);
+ $sql = "SELECT * FROM ManualInvoiceposition WHERE $where ORDER BY $order";
+
+ if(is_array($limit) && count($limit)) {
+ if(is_numeric($limit['start']) && is_numeric($limit['count'])) {
+ $sql .= " LIMIT ".$limit['start'].", ".$limit['count'];
+ } elseif(is_numeric($limit['count'])) {
+ $sql .= " LIMIT ".$limit['count'];
+ }
+ }
+
+ mfLoghandler::singleton()->debug($sql);
+
+ $res = $db->query($sql);
+ if($db->num_rows($res)) {
+ while($data = $db->fetch_object($res)) {
+ $items[$data->id] = new ManualInvoiceposition($data);
+ }
+ }
+
+ return $items;
+ }
+
+ private static function getSqlFilter($filter) {
+ $where = "1=1 ";
+
+ $db = FronkDB::singleton();
+
+ if(array_key_exists("id", $filter)) {
+ $id = $filter['id'];
+ if(is_numeric($id)) {
+ $where .= " AND ManualInvoiceposition.id = $id";
+ }
+ }
+
+ if(array_key_exists("manualinvoice_id", $filter)) {
+ $manualinvoice_id = $filter['manualinvoice_id'];
+ if(is_numeric($manualinvoice_id)) {
+ $where .= " AND ManualInvoiceposition.manualinvoice_id=$manualinvoice_id";
+ }
+ }
+
+ if(array_key_exists("add-where", $filter)) {
+ $where .= " ".$filter['add-where'];
+ }
+
+ return $where;
+ }
+
+}
diff --git a/db/migrations/20251201120000_create_manual_invoice_tables.php b/db/migrations/20251201120000_create_manual_invoice_tables.php
new file mode 100644
index 000000000..22d47919d
--- /dev/null
+++ b/db/migrations/20251201120000_create_manual_invoice_tables.php
@@ -0,0 +1,110 @@
+getEnvironment() == "thetool") {
+ // Create ManualInvoice table
+ $manualInvoice = $this->table("ManualInvoice");
+
+ $manualInvoice->addColumn("invoice_number", "string", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("invoice_date", "integer", ["default" => 0]);
+ $manualInvoice->addColumn("owner_id", "integer", ["null" => false]);
+ $manualInvoice->addColumn("billingaddress_id", "integer", ["null" => false]);
+ $manualInvoice->addColumn("customer_number", "integer", ["null" => false]);
+ $manualInvoice->addColumn("fibu_account_number", "integer", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("fibu_payment_due", "integer", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("fibu_payment_skonto", "integer", ["null" => false, "default" => 0]);
+ $manualInvoice->addColumn("fibu_payment_skonto_rate", "integer", ["null" => false, "default" => 0]);
+ $manualInvoice->addColumn("sepa_date", "date", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("sepa_id", "string", ["null" => true, "default" => null, "length" => 255]);
+ $manualInvoice->addColumn("sepa_last_date", "date", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("fibu_cost_area", "string", ["null" => true, "default" => null, "length" => 255]);
+ $manualInvoice->addColumn("fibu_cost_account", "integer", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("fibu_cost_account_legacy", "integer", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("fibu_taxcode", "integer", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("tax_text", "string", ["null" => true, "default" => null, "length" => 255]);
+ $manualInvoice->addColumn("company", "string", ["null" => true, "default" => null, "length" => 1024]);
+ $manualInvoice->addColumn("firstname", "string", ["null" => true, "default" => null, "length" => 1024]);
+ $manualInvoice->addColumn("lastname", "string", ["null" => true, "default" => null, "length" => 1024]);
+ $manualInvoice->addColumn("street", "string", ["null" => false, "length" => 1024]);
+ $manualInvoice->addColumn("zip", "string", ["null" => false, "length" => 1024]);
+ $manualInvoice->addColumn("city", "string", ["null" => false, "length" => 1024]);
+ $manualInvoice->addColumn("country", "string", ["null" => true, "default" => null, "length" => 1024]);
+ $manualInvoice->addColumn("email", "string", ["null" => true, "default" => null, "length" => 1024]);
+ $manualInvoice->addColumn("uid", "string", ["null" => true, "default" => null, "length" => 1024]);
+ $manualInvoice->addColumn("billing_type", "enum", ["null" => false, "values" => "invoice,sepa"]);
+ $manualInvoice->addColumn("billing_delivery", "enum", ["null" => false, "values" => "email,paper"]);
+ $manualInvoice->addColumn("bank_account_bank", "string", ["null" => true, "default" => null, "length" => 255]);
+ $manualInvoice->addColumn("bank_account_owner", "string", ["null" => true, "default" => null, "length" => 255]);
+ $manualInvoice->addColumn("bank_account_iban", "string", ["null" => true, "default" => null, "length" => 255]);
+ $manualInvoice->addColumn("bank_account_bic", "string", ["null" => true, "default" => null, "length" => 255]);
+ $manualInvoice->addColumn("total", "decimal", ["null" => false, "precision" => 14, "scale" => 4]);
+ $manualInvoice->addColumn("total_gross", "decimal", ["null" => false, "precision" => 14, "scale" => 4]);
+ $manualInvoice->addColumn("vatgroup_id", "integer", ["null" => false]);
+ $manualInvoice->addColumn("bmd_export_date", "integer", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("date_delivered", "integer", ["null" => true, "default" => null]);
+ $manualInvoice->addColumn("create_by", "integer", ["null" => false]);
+ $manualInvoice->addColumn("edit_by", "integer", ["null" => false]);
+ $manualInvoice->addColumn("create", "integer", ["null" => false]);
+ $manualInvoice->addColumn("edit", "integer", ["null" => false]);
+
+ $manualInvoice->addIndex(["invoice_number"], ["name" => "invoice_number"]);
+ $manualInvoice->addIndex(["invoice_date"], ["name" => "invoice_date"]);
+ $manualInvoice->addIndex(["owner_id"], ["name" => "owner_id"]);
+ $manualInvoice->addIndex(["billingaddress_id"], ["name" => "billingaddress_id"]);
+ $manualInvoice->addIndex(["customer_number"], ["name" => "customer_number"]);
+
+ $manualInvoice->create();
+
+ // Create ManualInvoiceposition table
+ $manualInvoicePosition = $this->table("ManualInvoiceposition");
+
+ $manualInvoicePosition->addColumn("manualinvoice_id", "integer", ["null" => true, "default" => null]);
+ $manualInvoicePosition->addColumn("billing_id", "integer", ["null" => true, "default" => null]);
+ $manualInvoicePosition->addColumn("contract_id", "integer", ["null" => false]);
+ $manualInvoicePosition->addColumn("start_date", "date", ["null" => false]);
+ $manualInvoicePosition->addColumn("end_date", "date", ["null" => true, "default" => null]);
+ $manualInvoicePosition->addColumn("matchcode", "string", ["null" => true, "default" => null, "length" => 255]);
+ $manualInvoicePosition->addColumn("product_id", "integer", ["null" => false]);
+ $manualInvoicePosition->addColumn("product_name", "string", ["null" => false, "length" => 255]);
+ $manualInvoicePosition->addColumn("product_info", "text", ["null" => true, "default" => null]);
+ $manualInvoicePosition->addColumn("amount", "decimal", ["null" => false, "precision" => 9, "scale" => 6]);
+ $manualInvoicePosition->addColumn("price", "decimal", ["null" => false, "precision" => 14, "scale" => 4]);
+ $manualInvoicePosition->addColumn("price_total", "decimal", ["null" => false, "precision" => 14, "scale" => 4]);
+ $manualInvoicePosition->addColumn("price_gross", "decimal", ["null" => false, "precision" => 14, "scale" => 4]);
+ $manualInvoicePosition->addColumn("vatrate", "decimal", ["null" => false, "default" => 0, "precision" => 6, "scale" => 2]);
+ $manualInvoicePosition->addColumn("fibu_cost_account", "integer", ["null" => true, "default" => null]);
+ $manualInvoicePosition->addColumn("fibu_cost_account_legacy", "integer", ["null" => true, "default" => null]);
+ $manualInvoicePosition->addColumn("fibu_taxcode", "integer", ["null" => true, "default" => null]);
+ $manualInvoicePosition->addColumn("billing_period", "integer", ["null" => false, "default" => 0]);
+ $manualInvoicePosition->addColumn("options", "text", ["null" => true, "default" => null, "limit" => \Phinx\Db\Adapter\MysqlAdapter::TEXT_LONG]);
+ $manualInvoicePosition->addColumn("create_by", "integer", ["null" => false]);
+ $manualInvoicePosition->addColumn("edit_by", "integer", ["null" => false]);
+ $manualInvoicePosition->addColumn("create", "integer", ["null" => false]);
+ $manualInvoicePosition->addColumn("edit", "integer", ["null" => false]);
+
+ $manualInvoicePosition->create();
+ }
+
+ if($this->getEnvironment() == "addressdb") {
+
+ }
+ }
+
+ public function down(): void
+ {
+ if($this->getEnvironment() == "thetool") {
+ $this->table("ManualInvoiceposition")->drop()->save();
+ $this->table("ManualInvoice")->drop()->save();
+ }
+
+ if($this->getEnvironment() == "addressdb") {
+
+ }
+ }
+}
diff --git a/public/js/pages/ManualInvoice/ManualInvoice.css b/public/js/pages/ManualInvoice/ManualInvoice.css
index 4d2b28d3b..0fc7a0ec2 100644
--- a/public/js/pages/ManualInvoice/ManualInvoice.css
+++ b/public/js/pages/ManualInvoice/ManualInvoice.css
@@ -28,10 +28,40 @@
.invoice-preview-pane {
flex: 1 1 auto;
background-color: #525659;
- padding: 2rem;
- overflow-y: auto;
+ padding: 0;
+ overflow: hidden;
display: flex;
justify-content: center;
+ align-items: center;
+}
+
+.pdf-preview-container {
+ width: 100%;
+ height: 100%;
+ position: relative;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+}
+
+.pdf-preview-container object {
+ width: 100%;
+ height: 100%;
+}
+
+.pdf-loading {
+ text-align: center;
+ color: white;
+ padding: 2rem;
+}
+
+.pdf-loading i {
+ margin-bottom: 1rem;
+}
+
+.pdf-loading p {
+ font-size: 1.2rem;
+ margin-top: 1rem;
}
.info-bar {
diff --git a/public/js/pages/ManualInvoice/ManualInvoice.js b/public/js/pages/ManualInvoice/ManualInvoice.js
index c21809295..fbd2682c4 100644
--- a/public/js/pages/ManualInvoice/ManualInvoice.js
+++ b/public/js/pages/ManualInvoice/ManualInvoice.js
@@ -3,18 +3,28 @@ Vue.component('manual-invoice', {
-
-
- {{ formatPrice(row.totalAmount) }}
+
+ {{ formatPrice(row.total) }}
-
- {{ formatDate(row.invoiceDate) }}
+
+ {{ formatPrice(row.total_gross) }}
+
+
+ {{ formatDate(row.invoice_date) }}
+
+
+ {{ row.customerName }}
+
+
+
+
+
@@ -32,19 +42,6 @@ Vue.component('manual-invoice', {
editingInvoiceData: null,
}
},
- mounted() {
- const prefillData = localStorage.getItem('ManualInvoice_create');
- if (prefillData) {
- try {
- this.editingInvoiceData = JSON.parse(prefillData);
- this.isModalOpen = true;
- } catch (e) {
- console.error("Failed to parse prefill data:", e);
- } finally {
- localStorage.removeItem('ManualInvoice_create');
- }
- }
- },
methods: {
openModal(invoice = null) {
this.editingInvoiceData = invoice ? JSON.parse(JSON.stringify(invoice)) : null;
@@ -55,30 +52,75 @@ Vue.component('manual-invoice', {
this.editingInvoiceData = null;
this.$refs.table.$refs.table.refreshTable();
},
- handleSave(invoiceData) {
- console.log("--- INVOICE SAVED (DEMO) ---");
- console.log(JSON.parse(JSON.stringify(invoiceData)));
- window.notify('success', 'Rechnung in der Konsole geloggt!');
- this.closeModal();
+ async handleSave(invoiceData) {
+ try {
+ // Calculate totals for each position
+ const positions = invoiceData.positions.map(p => {
+ const amount = parseFloat(p.amount) || 0;
+ const price = parseFloat(p.price) || 0;
+ const vatrate = parseFloat(p.vatrate) || 0;
+ const price_total = amount * price;
+ const price_gross = price_total * (1 + vatrate / 100);
+
+ return {
+ ...p,
+ amount,
+ price,
+ vatrate,
+ price_total,
+ price_gross,
+ product_id: 0,
+ contract_id: 0,
+ billing_id: 0,
+ billing_period: 0
+ };
+ });
+
+ // Prepare invoice data
+ const payload = {
+ id: invoiceData.id || null,
+ invoice_number: invoiceData.invoice_number,
+ invoice_date: invoiceData.invoice_date,
+ owner_id: invoiceData.owner_id || 0,
+ billingaddress_id: invoiceData.billingaddress_id || 0,
+ customer_number: invoiceData.customer_number || 0,
+ company: invoiceData.company || '',
+ firstname: invoiceData.firstname || '',
+ lastname: invoiceData.lastname || '',
+ street: invoiceData.street || '',
+ zip: invoiceData.zip || '',
+ city: invoiceData.city || '',
+ country: invoiceData.country || 'Österreich',
+ email: invoiceData.email || '',
+ uid: invoiceData.uid || '',
+ billing_type: invoiceData.billing_type || 'invoice',
+ billing_delivery: 'email',
+ tax_text: invoiceData.tax_text || '',
+ fibu_payment_due: 14,
+ fibu_account_number: invoiceData.fibu_account_number || 0,
+ vatgroup_id: 1,
+ positions: positions
+ };
+
+ const url = invoiceData.id
+ ? window.TT_CONFIG.UPDATE_URL
+ : window.TT_CONFIG.CREATE_URL;
+
+ const response = await axios.post(url, payload);
+
+ if (response.data.success) {
+ window.notify('success', response.data.message || 'Rechnung erfolgreich gespeichert!');
+ this.closeModal();
+ } else {
+ window.notify('error', response.data.message || 'Fehler beim Speichern der Rechnung');
+ }
+ } catch (error) {
+ console.error('Error saving invoice:', error);
+ window.notify('error', 'Fehler beim Speichern der Rechnung: ' + (error.response?.data?.message || error.message));
+ }
},
- testPrefill() {
- const mockInvoice = {
- id: null,
- invoiceNumber: `RE-${new Date().getFullYear()}-XXXX`,
- invoiceDate: moment().unix(),
- dueDate: moment().add(14, 'days').unix(),
- status: 'draft',
- billingAddressId: 1, // Example ID for autocomplete to fetch
- customer: {}, // Will be populated by watcher
- positions: [
- { product_name: 'Stunden Techniker', product_info: 'Arbeiten an Server-Infrastruktur', start_date: moment().format('YYYY-MM-DD'), end_date: moment().format('YYYY-MM-DD'), amount: 3.5, price: 95.00, vatrate: 20 },
- { product_name: 'Anfahrtspauschale', product_info: '', start_date: moment().format('YYYY-MM-DD'), end_date: moment().format('YYYY-MM-DD'), amount: 1, price: 45.00, vatrate: 20 }
- ],
- closingText: 'Wir bedanken uns für die gute Zusammenarbeit.',
- taxText: ''
- };
- localStorage.setItem('ManualInvoice_create', JSON.stringify(mockInvoice));
- window.location.reload();
+ downloadPdf(invoiceId) {
+ window.location.href = `${window.TT_CONFIG.BASE_PATH}/ManualInvoice/downloadInvoicePdf?id=${invoiceId}`;
},
formatPrice(value) {
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value || 0);
@@ -93,7 +135,7 @@ Vue.component('manual-invoice', {
Vue.component('manual-invoice-modal', {
props: ['initialData'],
template: `
-
+
Drücke STRG + Q um die Vorschau umzuschalten.
@@ -102,21 +144,21 @@ Vue.component('manual-invoice-modal', {
Kunde
-
+
Rechnungsdetails
-
-
-
+
+
+
@@ -125,100 +167,20 @@ Vue.component('manual-invoice-modal', {
Texte
-
-
+
-
-
-
-
-
-
-
-
Ihre Rechnung vom {{ formatDate(invoiceData.invoiceDate) }}
-
-
-
- Leistung / Produkt
- Zeitraum
- Preis
- Menge
- Netto €
- Ust. %
- Brutto €
-
-
-
-
-
- {{ p.product_name }}
- {{ p.product_info }}
-
- {{ formatPeriod(p.start_date, p.end_date) }}
- {{ formatPrice(p.price) }}
- {{ p.amount }}
- {{ formatPrice((p.amount || 0) * (p.price || 0)) }}
- {{ p.vatrate }}%
- {{ formatPrice(((p.amount || 0) * (p.price || 0)) * (1 + (p.vatrate || 0) / 100)) }}
-
-
-
-
-
-
-
- Gesamtbetrag Netto:
- {{ formatPrice(totals.net) }} €
-
-
- + Umsatzsteuer {{ vatRate }}%:
- {{ formatPrice(vatValue) }} €
-
-
- Gesamtbetrag Brutto:
- {{ formatPrice(totals.gross) }} €
-
-
-
-
-
{{invoiceData.taxText}}
- Bitte
überweisen Sie den Rechnungsbetrag bis zum
{{ formatDate(invoiceData.dueDate) }} auf folgendes Konto:
-
IBAN: {{ bankDetails.iban }}
-
BIC: {{ bankDetails.bic }}
- Bitte geben Sie als Verwendungszweck unbedingt die Rechnungsnummer an.
-
-
-
@@ -230,22 +192,36 @@ Vue.component('manual-invoice-modal', {
selectedCustomerObject: {},
isLargeScreen: window.innerWidth >= 1920,
showPreviewOnSmallScreen: false,
+ pdfLoading: false,
+ pdfPreviewUrl: '',
+ previewDebounceTimer: null,
invoiceData: {
id: null,
- invoiceNumber: `RE-${new Date().getFullYear()}-`,
- invoiceDate: moment().unix(),
- dueDate: moment().add(14, 'days').unix(),
- status: 'draft',
- billingAddressId: null,
- customer: { company: '', name: '', street: '', zip: '', city: '', country: 'Österreich', uid: '' },
+ invoice_number: `MRN${new Date().getFullYear()}-X000001`,
+ invoice_date: moment().unix(),
+ billingaddress_id: null,
+ owner_id: null,
+ customer_number: 0,
+ fibu_account_number: 0,
+ company: '',
+ firstname: '',
+ lastname: '',
+ street: '',
+ zip: '',
+ city: '',
+ country: 'Österreich',
+ uid: '',
+ email: '',
+ billing_type: 'invoice',
+ tax_text: '',
positions: [],
- closingText: 'Wir danken für Ihren Auftrag und verbleiben mit freundlichen Grüßen,\nIhr Xinon Team',
- taxText: '',
- },
- bankDetails: {
- iban: 'ATXX XXXX XXXX XXXX XXXX',
- bic: 'XXXXXXXX'
+ total: 0,
+ total_gross: 0
},
+ billingTypeOptions: [
+ {value: 'invoice', text: 'Rechnung'},
+ {value: 'sepa', text: 'SEPA'}
+ ],
positionsConfig: {
fields: {
product_name: { type: 'input', label: 'Bezeichnung' },
@@ -289,10 +265,27 @@ Vue.component('manual-invoice-modal', {
}
},
watch: {
- 'invoiceData.billingAddressId': {
+ 'invoiceData': {
+ handler() {
+ this.debouncedPreviewUpdate();
+ },
+ deep: true
+ },
+ 'invoiceData.billingaddress_id': {
async handler(newId) {
if (!newId) {
- this.invoiceData.customer = { company: '', name: '', street: '', zip: '', city: '', country: 'Österreich', uid: '' };
+ this.invoiceData.company = '';
+ this.invoiceData.firstname = '';
+ this.invoiceData.lastname = '';
+ this.invoiceData.street = '';
+ this.invoiceData.zip = '';
+ this.invoiceData.city = '';
+ this.invoiceData.country = 'Österreich';
+ this.invoiceData.uid = '';
+ this.invoiceData.email = '';
+ this.invoiceData.customer_number = 0;
+ this.invoiceData.fibu_account_number = 0;
+ this.invoiceData.owner_id = 0;
this.selectedCustomerObject = {};
return;
}
@@ -300,15 +293,18 @@ Vue.component('manual-invoice-modal', {
if (response.data.status === 'OK' && response.data.result.address) {
const addr = response.data.result.address;
this.selectedCustomerObject = addr;
- this.invoiceData.customer = {
- company: addr.company,
- name: `${addr.firstname} ${addr.lastname}`,
- street: addr.street,
- zip: addr.zip,
- city: addr.city,
- country: 'Österreich',
- uid: addr.uid
- };
+ this.invoiceData.company = addr.company || '';
+ this.invoiceData.firstname = addr.firstname || '';
+ this.invoiceData.lastname = addr.lastname || '';
+ this.invoiceData.street = addr.street || '';
+ this.invoiceData.zip = addr.zip || '';
+ this.invoiceData.city = addr.city || '';
+ this.invoiceData.country = 'Österreich';
+ this.invoiceData.uid = addr.uid || '';
+ this.invoiceData.email = addr.email || '';
+ this.invoiceData.customer_number = addr.customer_number || 0;
+ this.invoiceData.fibu_account_number = addr.fibu_account_number || 0;
+ this.invoiceData.owner_id = newId;
}
},
immediate: true
@@ -316,16 +312,10 @@ Vue.component('manual-invoice-modal', {
},
created() {
if (this.initialData) {
- // FIX: Merge initial data with default structure to ensure all keys, especially nested ones, exist.
this.invoiceData = {
- ...this.invoiceData, // Start with default structure
- ...JSON.parse(JSON.stringify(this.initialData)) // Overwrite with passed data
+ ...this.invoiceData,
+ ...JSON.parse(JSON.stringify(this.initialData))
};
- // Explicitly ensure nested objects exist if they weren't in initialData
- if (!this.invoiceData.customer) {
- this.invoiceData.customer = { company: '', name: '', street: '', zip: '', city: '', country: 'Österreich', uid: '' };
- }
- // Ensure positions is an array
if (!Array.isArray(this.invoiceData.positions)) {
try {
const parsed = JSON.parse(this.invoiceData.positions);
@@ -338,35 +328,115 @@ Vue.component('manual-invoice-modal', {
},
mounted() {
window.addEventListener('resize', this.handleResize);
+ window.addEventListener('keydown', this.handleGlobalKeydown);
this.handleResize();
this.$nextTick(() => {
if (this.$refs.overlay) {
this.$refs.overlay.focus();
}
+ this.updatePdfPreview();
});
},
beforeDestroy() {
window.removeEventListener('resize', this.handleResize);
+ window.removeEventListener('keydown', this.handleGlobalKeydown);
+ if (this.previewDebounceTimer) {
+ clearTimeout(this.previewDebounceTimer);
+ }
},
methods: {
close() { this.$emit('close'); },
- handleResize() { this.isLargeScreen = window.innerWidth >= 1920; },
- togglePreviewVisibility() { if (!this.isLargeScreen) this.showPreviewOnSmallScreen = !this.showPreviewOnSmallScreen; },
- formatPrice(value) { return new Intl.NumberFormat('de-DE', { minimumFractionDigits: 2, maximumFractionDigits: 2 }).format(value || 0); },
- formatDate(timestamp) {
- if (!timestamp) return '';
- return moment.unix(timestamp).format('DD.MM.YYYY');
- },
- formatPeriod(start, end) {
- if (!start) return '';
- const startDate = moment(start);
- const endDate = end ? moment(end) : moment(start);
- if (!startDate.isValid()) return '';
- if (startDate.isSame(endDate, 'day')) return startDate.format('DD.MM.YYYY');
- if(startDate.isValid() && endDate.isValid()) {
- return `${startDate.format('DD.MM.YYYY')} - ${endDate.format('DD.MM.YYYY')}`;
+ saveInvoice() {
+ if (!this.invoiceData.billingaddress_id) {
+ window.notify('error', 'Bitte wählen Sie einen Kunden aus.');
+ return;
+ }
+ if (!this.invoiceData.positions || this.invoiceData.positions.length === 0) {
+ window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
+ return;
+ }
+ this.$emit('save', this.invoiceData);
+ },
+ handleResize() { this.isLargeScreen = window.innerWidth >= 1920; },
+ handleGlobalKeydown(event) {
+ // Handle CTRL+Q to toggle preview on small screens
+ if (event.ctrlKey && event.key === 'q') {
+ event.preventDefault();
+ this.togglePreviewVisibility();
+ }
+ },
+ togglePreviewVisibility() { if (!this.isLargeScreen) this.showPreviewOnSmallScreen = !this.showPreviewOnSmallScreen; },
+ debouncedPreviewUpdate() {
+ if (this.previewDebounceTimer) {
+ clearTimeout(this.previewDebounceTimer);
+ }
+ this.previewDebounceTimer = setTimeout(() => {
+ this.updatePdfPreview();
+ }, 2000);
+ },
+ async updatePdfPreview() {
+ this.pdfLoading = true;
+
+ try {
+ // Calculate position totals
+ const positions = this.invoiceData.positions.map(p => {
+ const amount = parseFloat(p.amount) || 0;
+ const price = parseFloat(p.price) || 0;
+ const vatrate = parseFloat(p.vatrate) || 0;
+ const price_total = amount * price;
+ const price_gross = price_total * (1 + vatrate / 100);
+
+ return {
+ ...p,
+ amount,
+ price,
+ vatrate,
+ price_total,
+ price_gross
+ };
+ });
+
+ const payload = {
+ preview: true,
+ invoice_number: this.invoiceData.invoice_number,
+ invoice_date: this.invoiceData.invoice_date,
+ customer_number: this.invoiceData.customer_number,
+ fibu_account_number: this.invoiceData.fibu_account_number,
+ company: this.invoiceData.company,
+ firstname: this.invoiceData.firstname,
+ lastname: this.invoiceData.lastname,
+ street: this.invoiceData.street,
+ zip: this.invoiceData.zip,
+ city: this.invoiceData.city,
+ country: this.invoiceData.country,
+ email: this.invoiceData.email,
+ uid: this.invoiceData.uid,
+ tax_text: this.invoiceData.tax_text,
+ billing_type: this.invoiceData.billing_type,
+ total: this.totals.net,
+ total_gross: this.totals.gross,
+ positions: positions
+ };
+
+ const response = await axios.post(
+ `${window.TT_CONFIG.BASE_PATH}/ManualInvoice/createPDF`,
+ payload,
+ { responseType: 'blob' }
+ );
+
+ // Create a blob URL for the PDF
+ const blob = new Blob([response.data], { type: 'application/pdf' });
+ if (this.pdfPreviewUrl) {
+ URL.revokeObjectURL(this.pdfPreviewUrl);
+ }
+ this.pdfPreviewUrl = URL.createObjectURL(blob) + '#view=FitH';
+
+ } catch (error) {
+ console.error('Error generating PDF preview:', error);
+ window.notify('error', 'Fehler beim Generieren der PDF-Vorschau');
+ } finally {
+ this.pdfLoading = false;
}
- return startDate.format('DD.MM.YYYY');
}
}
-});
\ No newline at end of file
+});