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 @@ + + + + Xinon Rechnung + + + + + + +
+
+ XINON GmbH | Fladnitz 150 | 8322 Studenzen
+ Tel.: +43 3115 40800 | E-Mail: office@xinon.at
+ UID: ATU68711968 | FN: 416556h | LG: Feldbach
+ IBAN: {{ bank_iban }} | BIC: {{ bank_bic }}
+
+ +
Seite von
+ +
+ + diff --git a/Layout/default/ManualInvoice/PDF_HEADER.html b/Layout/default/ManualInvoice/PDF_HEADER.html new file mode 100644 index 000000000..56b74016a --- /dev/null +++ b/Layout/default/ManualInvoice/PDF_HEADER.html @@ -0,0 +1,107 @@ + + + + XINON Invoice Header + + + + + +
+ +
+ Xinon Logo +
+ + + + + + + + +
+
{{ addressLine_1 }}
+
{{ addressLine_2 }}
+
{{ addressLine_3 }}
+
{{ addressLine_4 }}
+
{{ addressLine_5 }}
+
+ + + + + +
+ + + + + + + + + + + + + + + + + + {{ vatHtml }} +
Kundennummer:{{ customerNumber }}
Verrechnungskonto:{{ billingAccount }}
Rechnungsnummer:{{ invoiceNumber }}
Belegdatum:{{ invoiceDate }}
+
+
+ QR-Code +
+ + +
+ + + diff --git a/Layout/default/ManualInvoice/PDF_MAIN.php b/Layout/default/ManualInvoice/PDF_MAIN.php new file mode 100644 index 000000000..9355ae1f2 --- /dev/null +++ b/Layout/default/ManualInvoice/PDF_MAIN.php @@ -0,0 +1,208 @@ +total; +$gross_total = $invoice->total_gross; +$is_credit = $net_total < 0; + +$this->setReturnValue(['filename' => $invoice->invoice_number . ".pdf"]); + +?> + + + + + Rechnung + + + + + + + +
+ +

Ihre Xinon vom invoice_date)?>

+ + + + + + + + + + + + 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, ",","."); + + ?> + + "> + + + + + + + + + + + + + + + $vat_total): ?> + + 0): ?> + + + + + + + + + + + + + +
Leistung / ProduktZeitraumPreisMengeNetto €Ust. %Brutto €
+ product_name ?? '')?> + matchcode) && $p->matchcode): ?> +
matchcode)?>
+ +
+ + + format("m.Y")?> + billing_period) && $p->billing_period > 1): ?> + format("m.Y")?> - format("m.Y") ?> + + format("d.m.Y") == $end_date->format("d.m.Y")): ?> + format("d.m.Y")?> + + format("d.m.Y")?> - format("d.m.Y") ?> + + + + format("d.m.Y")?> + + - + + + %
Gesamt Netto:
USt. %:
Gesamt Brutto:
+ + +
+ tax_text): ?> +

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  invoice_date))->modify("+14 days")->format("d.m.Y")?> auf folgendes Konto:
+ IBAN:
+ 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', {
-
-