diff --git a/application/ManualInvoice/ManualInvoice.php b/application/ManualInvoice/ManualInvoice.php deleted file mode 100644 index 4657df577..000000000 --- a/application/ManualInvoice/ManualInvoice.php +++ /dev/null @@ -1,233 +0,0 @@ -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 973f4b2d0..6bac5e0e3 100644 --- a/application/ManualInvoice/ManualInvoiceController.php +++ b/application/ManualInvoice/ManualInvoiceController.php @@ -4,11 +4,10 @@ class ManualInvoiceController extends TTCrud { protected string $headerTitle = 'Manuelle Rechnungen'; protected bool $createText = false; - protected array $additionalJS = ["js/pages/ManualInvoice/ManualInvoice.js"]; protected array $additionalHead = [""]; + private array $tempPositions = []; - //@formatter:off protected array $columns = [ ['key' => 'id', 'text' => 'ID', 'table' => ['visible' => false], 'modal' => false], ['key' => 'invoice_number', 'text' => 'Rechnungsnr.', 'table' => ['sortable' => true, 'filter' => 'search']], @@ -19,79 +18,54 @@ class ManualInvoiceController extends TTCrud ['key' => 'customer_number', 'text' => 'Kundennr.', 'table' => ['sortable' => true, 'filter' => 'search']], ['key' => 'total', 'text' => 'Netto', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']], ['key' => 'total_gross', 'text' => 'Brutto', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']], + ['key' => 'status', 'text' => 'Status', 'table' => ['filter' => 'select', 'filterOptions' => [ + ['value' => 'draft', 'text' => 'Entwurf'], + ['value' => 'finalized', 'text' => 'Finalisiert'], + ['value' => 'exported', 'text' => 'Exportiert'], + ]]], ['key' => 'billing_type', 'text' => 'Zahlungsart', 'table' => ['filter' => 'select', 'filterOptions' => [ ['value' => 'invoice', 'text' => 'Rechnung'], ['value' => 'sepa', 'text' => 'SEPA'], ]]], ['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]], ]; - //@formatter:on + + protected array $additionalActions = [ + ['key' => 'createGutschrift', 'title' => 'Gutschrift erstellen', 'class' => 'fas fa-file-invoice text-warning'] + ]; 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); + $invoice = (object)[]; + $positions = []; 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; - } + $invoice = (object) array_merge([ + 'id' => 0, 'invoice_number' => null, 'invoice_date' => time(), + 'customer_number' => 0, 'fibu_account_number' => 0, + 'company' => '', 'firstname' => '', 'lastname' => '', + 'street' => '', 'zip' => '', 'city' => '', 'country' => 'Österreich', + 'email' => '', 'uid' => '', 'tax_text' => '', 'billing_type' => 'invoice', + 'total' => 0, 'total_gross' => 0 + ], $post); + $positions = array_map(fn($p) => (object)$p, $post['positions'] ?? []); } else { - // Load from database $id = $this->request->id ?? $post['id'] ?? null; - if (!$id) { + if (!$id || !($invoice = ManualInvoiceModel::get($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; + $pObj = (object)$p; + $vat[$pObj->vatrate] = ($vat[$pObj->vatrate] ?? 0) + ($pObj->price_gross ?? 0) - ($pObj->price_total ?? 0); } - // Convert positions array to objects if needed $invoice->positions = $positions; - $pdf_vars = [ "invoice" => $invoice, "vat" => $vat, @@ -101,44 +75,40 @@ class ManualInvoiceController extends TTCrud "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); + $replacements = [ + "{{ basedir }}" => BASEDIR, + "{{ addressLine_1 }}" => $invoice->company ?: "", + "{{ addressLine_2 }}" => trim($invoice->firstname . " " . $invoice->lastname), + "{{ addressLine_3 }}" => $invoice->street ?? '', + "{{ addressLine_4 }}" => ($invoice->zip ?? '') . " " . ($invoice->city ?? ''), + "{{ addressLine_5 }}" => ($invoice->country ?? '') != "Österreich" ? ($invoice->country ?? '') : "", + "{{ customerNumber }}" => $invoice->customer_number ?? '', + "{{ billingAccount }}" => $invoice->fibu_account_number ?? '', + "{{ invoiceNumber }}" => $invoice->invoice_number ?? "VORSCHAU", + "{{ invoiceDate }}" => date("d.m.Y", $invoice->invoice_date ?? time()), + "{{ vatHtml }}" => ($invoice->uid ?? '') ? "Ihre UID:" . $invoice->uid . "" : "", + "{{ qrCodeSrc }}" => $this->generateSepaQRCode($invoice->invoice_number ?? "VORSCHAU", round($invoice->total_gross ?? 0, 2)) + ]; + $headerHtml = str_replace(array_keys($replacements), array_values($replacements), file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_HEADER.html")); $headerFile = BASEDIR . "/var/temp/manualinvoice_header-" . date("U") . "-" . rand(1000, 9999) . ".html"; file_put_contents($headerFile, $headerHtml); - // 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); - + $footerReplacements = [ + "{{ bank_iban }}" => TT_INVOICE_BANK_IBAN_FORMATTED, + "{{ bank_bic }}" => TT_INVOICE_BANK_BIC, + "{{ bank_bank }}" => TT_INVOICE_BANK_BANK, + "{{ bank_owner }}" => TT_INVOICE_BANK_OWNER + ]; + $footerHtml = str_replace(array_keys($footerReplacements), array_values($footerReplacements), file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_FOOTER.html")); $footerFile = BASEDIR . "/var/temp/manualinvoice_footer-" . date("U") . "-" . rand(1000, 9999) . ".html"; file_put_contents($footerFile, $footerHtml); $pdf = new PdfForm("ManualInvoice/PDF_MAIN", $pdf_vars); - $wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile"; - $filename = $pdf->render($wkhtmltopdfArgs); + $filename = $pdf->render("--header-html $headerFile --footer-html $footerFile"); 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); @@ -147,113 +117,185 @@ class ManualInvoiceController extends TTCrud protected function downloadInvoicePdfAction() { $id = $this->request->id; - if (!is_numeric($id) || !$id) { + if (!is_numeric($id) || !$id || !($invoice = ManualInvoiceModel::get($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)) { + if(!($pdf_filename = $this->createPDFAction(true)) || !file_exists($pdf_filename)) { $this->layout()->setFlash("PDF-Datei konnte nicht erstellt werden"); $this->redirect("ManualInvoice"); } - header('Content-Type: application/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)); - + header('Content-disposition: attachment; filename="'.$invoice->invoice_number.'.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(); + if (isset($data['positions']) && is_array($data['positions'])) { + $this->tempPositions = $data['positions']; + unset($data['positions']); } - // Set default values - if (empty($data['invoice_date'])) { - $data['invoice_date'] = time(); - } + $me = new User(); + $me->loadMe(); + + $data = array_merge([ + 'invoice_number' => ManualInvoiceModel::getNextInvoiceNumber(), + 'invoice_date' => time(), + 'status' => 'draft', + 'fibu_payment_skonto' => 0, + 'fibu_payment_skonto_rate' => 0, + 'total' => 0, + 'total_gross' => 0, + 'create_by' => $me->id, + 'edit_by' => $me->id, + 'create' => time(), + 'edit' => time() + ], $data); return true; } protected function afterCreate($data) { - $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); + $this->savePositions($data['id']); + $this->recalculateTotals($data['id']); } protected function beforeUpdate(&$data): bool { + if (isset($data['positions']) && is_array($data['positions'])) { + $this->tempPositions = $data['positions']; + unset($data['positions']); + } + + if (isset($data['id']) && ($invoice = ManualInvoiceModel::get($data['id'])) && $invoice->status === 'exported') { + $this->infoMessages['update'] = 'Rechnung wurde bereits exportiert und kann nicht mehr bearbeitet werden'; + return false; + } + + $me = new User(); + $me->loadMe(); + $data['edit_by'] = $me->id; + $data['edit'] = time(); + return true; } protected function afterUpdate($data) { - $invoiceId = $data['id']; + $existingPositions = ManualInvoicepositionModel::search(['manualinvoice_id' => $data['id']]); + foreach ($existingPositions as $pos) ManualInvoicepositionModel::delete($pos->id); - // Delete existing positions - $existingPositions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]); - foreach ($existingPositions as $pos) { - $pos->delete(); + $this->savePositions($data['id']); + $this->recalculateTotals($data['id']); + } + + private function savePositions($invoiceId) { + if (empty($this->tempPositions)) return; + + $me = new User(); + $me->loadMe(); + + foreach ($this->tempPositions as $position) { + ManualInvoicepositionModel::create(array_merge([ + 'manualinvoice_id' => $invoiceId, + 'start_date' => date('Y-m-d'), + 'create_by' => $me->id, + 'edit_by' => $me->id, + 'create' => time(), + 'edit' => time() + ], $position)); } - - // 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); + $this->tempPositions = []; } protected function recalculateTotals($invoiceId) { - $invoice = new ManualInvoice($invoiceId); + if (!($invoice = ManualInvoiceModel::get($invoiceId))) return; + $positions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]); + $invoice->total = array_sum(array_column($positions, 'price_total')); + $invoice->total_gross = array_sum(array_column($positions, 'price_gross')); + $invoice->save(); + } - $total = 0; - $total_gross = 0; + protected function getAction() { + $filter = $this->postData['filters'] ?? []; + $order = $this->postData['order']['key'] ? $this->postData['order'] : ($this->defaultOrder ?? ['key' => null, 'order' => 'ASC']); + $page = $this->postData['pagination']['page'] ?? 1; + $perPage = $this->postData['pagination']['per_page'] ?? 10; - foreach ($positions as $pos) { - $total += $pos->price_total; - $total_gross += $pos->price_gross; + $rows = ManualInvoiceModel::getAll($filter, $perPage, ($page - 1) * $perPage, $order); + $filteredAvailable = ManualInvoiceModel::count($filter); + $totalRows = ManualInvoiceModel::count(); + + foreach ($rows as &$row) { + $row->customerName = trim(($row->company ?: '') . ' ' . $row->firstname . ' ' . $row->lastname); + $row->positions = array_map([$this, 'formatPosition'], $row->getProperty('positions')); } - $invoice->total = $total; - $invoice->total_gross = $total_gross; - $invoice->save(); + self::returnJson([ + "rows" => $rows, + "autoCompleteData" => [], + "pagination" => [ + "page" => $page, + "total_pages" => ceil($filteredAvailable / $perPage), + "per_page" => $perPage, + "filtered_available" => intval($filteredAvailable), + "total_rows" => intval($totalRows) + ] + ]); + } + + protected function getByIdAction() { + $id = $_GET['id'] ?? null; + if (!$id || !is_numeric($id)) { + http_response_code(500); + self::returnJson(['success' => false, 'message' => 'No ID provided.']); + die(); + } + + if (!($invoice = ManualInvoiceModel::get($id))) { + http_response_code(404); + self::returnJson(['success' => false, 'message' => 'Invoice not found.']); + die(); + } + + $data = (array) $invoice; + $data['positions'] = array_map([$this, 'formatPosition'], $invoice->getProperty('positions')); + + self::returnJson($data); + } + + private function formatPosition($pos) { + return [ + 'id' => $pos->id, + 'manualinvoice_id' => $pos->manualinvoice_id, + 'billing_id' => $pos->billing_id, + 'contract_id' => $pos->contract_id, + 'start_date' => $pos->start_date, + 'end_date' => $pos->end_date, + 'matchcode' => $pos->matchcode, + 'product_id' => $pos->product_id, + 'product_name' => $pos->product_name, + 'product_info' => $pos->product_info, + 'amount' => $pos->amount, + 'price' => $pos->price, + 'price_total' => $pos->price_total, + 'price_gross' => $pos->price_gross, + 'vatrate' => $pos->vatrate, + 'fibu_cost_account' => $pos->fibu_cost_account, + 'fibu_cost_account_legacy' => $pos->fibu_cost_account_legacy, + 'fibu_taxcode' => $pos->fibu_taxcode, + 'billing_period' => $pos->billing_period, + 'options' => $pos->options + ]; } protected function customRowsHandler($rows) { foreach ($rows as &$row) { - // Add customer name - $row->customerName = trim(($row->company ? $row->company : '') . ' ' . $row->firstname . ' ' . $row->lastname); + $row->customerName = trim(($row->company ?: '') . ' ' . $row->firstname . ' ' . $row->lastname); } return $rows; } @@ -262,20 +304,168 @@ class ManualInvoiceController extends TTCrud $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"; - + $epc = "BCD\n001\n1\nSCT\n$xinonBIC\n$xinonOwner\n$xinonIBAN\nEUR$amount\nXINO\n$paymentReference\n\nXINON GmbH"; return (new \chillerlan\QRCode\QRCode)->render($epc); } -} + + protected function getInvoiceForGutschriftAction() { + if (!($id = $_GET['id'] ?? null) || !($invoice = ManualInvoiceModel::get($id))) { + self::returnJson(['success' => false, 'message' => 'Invoice not found']); + } + + if ($invoice->total < 0) { + self::returnJson(['success' => false, 'message' => 'Kann keine Gutschrift für eine Gutschrift erstellen']); + } + + $positions = $invoice->getProperty('positions'); + $existingCredits = ManualInvoiceModel::getAll(['credit_for_invoice_id' => $id]); + $creditedAmounts = []; + + foreach ($existingCredits as $credit) { + foreach ($credit->getProperty('positions') as $creditPos) { + $key = $creditPos->product_id . '_' . $creditPos->matchcode; + $creditedAmounts[$key] = ($creditedAmounts[$key] ?? 0) + abs($creditPos->amount); + } + } + + $availablePositions = []; + foreach ($positions as $pos) { + $key = $pos->product_id . '_' . $pos->matchcode; + $availableAmount = $pos->amount - ($creditedAmounts[$key] ?? 0); + if ($availableAmount > 0) { + $availablePositions[] = [ + 'id' => $pos->id, + 'product_name' => $pos->product_name, + 'product_info' => $pos->product_info, + 'original_amount' => $pos->amount, + 'credited_amount' => $creditedAmounts[$key] ?? 0, + 'available_amount' => $availableAmount, + 'price' => $pos->price, + 'vatrate' => $pos->vatrate, + 'product_id' => $pos->product_id, + 'matchcode' => $pos->matchcode, + 'fibu_cost_account' => $pos->fibu_cost_account, + 'fibu_taxcode' => $pos->fibu_taxcode + ]; + } + } + + self::returnJson([ + 'success' => true, + 'invoice' => [ + 'id' => $invoice->id, + 'invoice_number' => $invoice->invoice_number, + 'customer_name' => trim(($invoice->company ?? '') . ' ' . $invoice->firstname . ' ' . $invoice->lastname), + 'positions' => $availablePositions + ] + ]); + } + + protected function createGutschriftAction() { + $post = json_decode(file_get_contents('php://input'), true); + $originalInvoiceId = $post['original_invoice_id'] ?? null; + $positions = $post['positions'] ?? []; + + if (!$originalInvoiceId || empty($positions) || !($originalInvoice = ManualInvoiceModel::get($originalInvoiceId))) { + self::returnJson(['success' => false, 'message' => 'Ungültige Anfrage']); + } + + $me = new User(); + $me->loadMe(); + + $invoiceData = [ + 'invoice_number' => ManualInvoiceModel::getNextInvoiceNumber(), + 'invoice_date' => time(), + 'owner_id' => $originalInvoice->owner_id, + 'billingaddress_id' => $originalInvoice->billingaddress_id, + 'customer_number' => $originalInvoice->customer_number, + 'fibu_account_number' => $originalInvoice->fibu_account_number, + 'fibu_payment_due' => $originalInvoice->fibu_payment_due, + 'fibu_payment_skonto' => $originalInvoice->fibu_payment_skonto, + 'fibu_payment_skonto_rate' => $originalInvoice->fibu_payment_skonto_rate, + 'sepa_date' => $originalInvoice->sepa_date, + 'sepa_id' => $originalInvoice->sepa_id, + 'sepa_last_date' => $originalInvoice->sepa_last_date, + 'fibu_cost_area' => $originalInvoice->fibu_cost_area, + 'fibu_cost_account' => $originalInvoice->fibu_cost_account, + 'fibu_cost_account_legacy' => $originalInvoice->fibu_cost_account_legacy, + 'fibu_taxcode' => $originalInvoice->fibu_taxcode, + 'tax_text' => $originalInvoice->tax_text, + 'company' => $originalInvoice->company, + 'firstname' => $originalInvoice->firstname, + 'lastname' => $originalInvoice->lastname, + 'street' => $originalInvoice->street, + 'zip' => $originalInvoice->zip, + 'city' => $originalInvoice->city, + 'country' => $originalInvoice->country, + 'email' => $originalInvoice->email, + 'uid' => $originalInvoice->uid, + 'billing_type' => $originalInvoice->billing_type, + 'billing_delivery' => $originalInvoice->billing_delivery, + 'bank_account_bank' => $originalInvoice->bank_account_bank, + 'bank_account_owner' => $originalInvoice->bank_account_owner, + 'bank_account_iban' => $originalInvoice->bank_account_iban, + 'bank_account_bic' => $originalInvoice->bank_account_bic, + 'total' => 0, + 'total_gross' => 0, + 'vatgroup_id' => $originalInvoice->vatgroup_id, + 'credit_for_invoice_id' => $originalInvoiceId, + 'status' => 'finalized', + 'create' => time(), + 'edit' => time(), + 'create_by' => $me->id, + 'edit_by' => $me->id + ]; + + if (!($creditInvoiceId = ManualInvoiceModel::create($invoiceData))) { + self::returnJson(['success' => false, 'message' => 'Fehler beim Erstellen der Gutschrift']); + } + + foreach ($positions as $pos) { + $priceTotal = (-abs($pos['amount'])) * $pos['price']; + ManualInvoicepositionModel::create([ + 'manualinvoice_id' => $creditInvoiceId, + 'product_id' => $pos['product_id'], + 'product_name' => $pos['product_name'], + 'product_info' => $pos['product_info'] ?? '', + 'amount' => -abs($pos['amount']), + 'price' => $pos['price'], + 'vatrate' => $pos['vatrate'], + 'price_total' => $priceTotal, + 'price_gross' => $priceTotal * (1 + $pos['vatrate'] / 100), + 'matchcode' => $pos['matchcode'] ?? null, + 'fibu_cost_account' => $pos['fibu_cost_account'] ?? null, + 'fibu_taxcode' => $pos['fibu_taxcode'] ?? null, + 'contract_id' => 0, + 'start_date' => date('Y-m-d'), + 'billing_period' => 0, + 'create_by' => $me->id, + 'edit_by' => $me->id, + 'create' => time(), + 'edit' => time() + ]); + } + + $this->recalculateTotals($creditInvoiceId); + + self::returnJson(['success' => true, 'message' => 'Gutschrift erfolgreich erstellt', 'credit_invoice_id' => $creditInvoiceId]); + } + + protected function beforeDelete(): bool { + if ($id = $this->request->id) { + $invoice = ManualInvoiceModel::get($id); + if ($invoice && $invoice->status === 'exported') { + $this->infoMessages['delete'] = 'Rechnung wurde bereits exportiert und kann nicht gelöscht werden'; + return false; + } + if (ManualInvoiceModel::count(['credit_for_invoice_id' => $id]) > 0) { + $this->infoMessages['delete'] = 'Rechnung kann nicht gelöscht werden, da bereits Gutschriften existieren'; + return false; + } + foreach (ManualInvoicepositionModel::search(['manualinvoice_id' => $id]) as $pos) { + ManualInvoicepositionModel::delete($pos->id); + } + } + return true; + } +} \ No newline at end of file diff --git a/application/ManualInvoice/ManualInvoiceModel.php b/application/ManualInvoice/ManualInvoiceModel.php index e9324a497..61e82f7f6 100644 --- a/application/ManualInvoice/ManualInvoiceModel.php +++ b/application/ManualInvoice/ManualInvoiceModel.php @@ -1,408 +1,76 @@ 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; - } - } - - // 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; - } +class ManualInvoiceModel extends TTCrudBaseModel { + public int $id; + public ?string $invoice_number; + public int $invoice_date; + public int $owner_id; + public int $billingaddress_id; + public int $customer_number; + public ?int $fibu_account_number; + public ?int $fibu_payment_due; + public int $fibu_payment_skonto; + public int $fibu_payment_skonto_rate; + public ?string $sepa_date; + public ?string $sepa_id; + public ?string $sepa_last_date; + public ?string $fibu_cost_area; + public ?int $fibu_cost_account; + public ?int $fibu_cost_account_legacy; + public ?int $fibu_taxcode; + public ?string $tax_text; + public ?string $company; + public ?string $firstname; + public ?string $lastname; + public string $street; + public string $zip; + public string $city; + public ?string $country; + public ?string $email; + public ?string $uid; + public string $billing_type; + public string $billing_delivery; + public ?string $bank_account_bank; + public ?string $bank_account_owner; + public ?string $bank_account_iban; + public ?string $bank_account_bic; + public float $total; + public float $total_gross; + public int $vatgroup_id; + public ?int $bmd_export_date; + public ?int $date_delivered; + public string $status; + public ?int $credit_for_invoice_id; + public int $create_by; + public int $edit_by; + public int $create; + public int $edit; public static function getNextInvoiceNumber() { - $last_invoice_num = self::getLastInvoiceNumber(); + $invoices = parent::getAll(['invoice_number' => '!NULL'], 1, 0, ['key' => 'invoice_number', 'order' => 'DESC']); + $last = $invoices[0]->invoice_number ?? null; + $year = date("Y"); - 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]; - } - } - } - - 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; + if ($last && preg_match('/^MRN(\d+)-X(\d+)$/', $last, $m)) { + $num = ($m[1] == $year) ? $m[2] + 1 : 1; } else { - $new_year_part = date("Y"); - $new_num_part = 1; + $num = 1; } - $new_invoice_num = "MRN$new_year_part-X".str_pad($new_num_part,"6", "0", STR_PAD_LEFT); - return $new_invoice_num; + return sprintf("MRN%s-X%06d", $year, $num); } - public static function getLastInvoiceNumber() { - $last_invoice = self::getLast(["invoice_number" => true]); - if(!$last_invoice || !$last_invoice->invoice_number) { - return false; + public function getProperty($name) { + if (!$this->id) return null; + + switch ($name) { + case 'positions': return ManualInvoicepositionModel::search(['manualinvoice_id' => $this->id]); + case 'creator': return $this->create_by ? new User($this->create_by) : null; + case 'editor': return $this->edit_by ? new User($this->edit_by) : null; + default: + $classname = ucfirst($name); + $idfield = $name . '_id'; + return (property_exists($this, $idfield) && class_exists($classname)) ? new $classname($this->$idfield) : null; } - return $last_invoice->invoice_number; } - - 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) { - $orderDir = isset($order['order']) ? $order['order'] : 'ASC'; - $sql .= " ORDER BY " . $order['key'] . " " . $orderDir; - } else { - $sql .= " ORDER BY invoice_number"; - } - - if($limit !== null) { - $sql .= " LIMIT " . intval($offset) . ", " . intval($limit); - } - - $res = $db->query($sql); - if($db->num_rows($res)) { - while($data = $db->fetch_object($res)) { - $items[] = new ManualInvoice($data); - } - } - return $items; - } - - public static function getFirst($filter) { - $db = FronkDB::singleton(); - - $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 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) { - 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) { - $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 deleted file mode 100644 index c6fac1fe7..000000000 --- a/application/ManualInvoiceposition/ManualInvoiceposition.php +++ /dev/null @@ -1,35 +0,0 @@ -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 index 884933fd7..6345ebf1d 100644 --- a/application/ManualInvoiceposition/ManualInvoicepositionModel.php +++ b/application/ManualInvoiceposition/ManualInvoicepositionModel.php @@ -1,160 +1,28 @@ $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; - } - +class ManualInvoicepositionModel extends TTCrudBaseModel { + public int $id; + public ?int $manualinvoice_id; + public ?int $billing_id; + public int $contract_id; + public string $start_date; + public ?string $end_date; + public ?string $matchcode; + public int $product_id; + public string $product_name; + public ?string $product_info; + public float $amount; + public float $price; + public float $price_total; + public float $price_gross; + public float $vatrate; + public ?int $fibu_cost_account; + public ?int $fibu_cost_account_legacy; + public ?int $fibu_taxcode; + public int $billing_period; + public ?string $options; + public int $create_by; + public int $edit_by; + public int $create; + public int $edit; } diff --git a/db/migrations/20251202105729_add_manual_invoice_status_and_credit_fields.php b/db/migrations/20251202105729_add_manual_invoice_status_and_credit_fields.php new file mode 100644 index 000000000..4419ed98b --- /dev/null +++ b/db/migrations/20251202105729_add_manual_invoice_status_and_credit_fields.php @@ -0,0 +1,50 @@ +getEnvironment() == "thetool") { + $table = $this->table("ManualInvoice"); + + $table->addColumn("status", "enum", [ + "values" => ["draft", "finalized", "exported"], + "default" => "draft", + "null" => false, + "after" => "date_delivered" + ]); + + $table->addColumn("credit_for_invoice_id", "integer", [ + "null" => true, + "default" => null, + "after" => "status" + ]); + + $table->addIndex(["credit_for_invoice_id"], ["name" => "credit_for_invoice_id"]); + + $table->update(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } + + public function down(): void + { + if ($this->getEnvironment() == "thetool") { + $table = $this->table("ManualInvoice"); + $table->removeColumn("status"); + $table->removeColumn("credit_for_invoice_id"); + $table->update(); + } + + if ($this->getEnvironment() == "addressdb") { + + } + } +} +?> diff --git a/lib/TTCrudBaseModel/TTCrudBaseModel.php b/lib/TTCrudBaseModel/TTCrudBaseModel.php index c2a97b024..d97b36ac1 100644 --- a/lib/TTCrudBaseModel/TTCrudBaseModel.php +++ b/lib/TTCrudBaseModel/TTCrudBaseModel.php @@ -227,4 +227,36 @@ class TTCrudBaseModel { return new static($result->fetch_assoc()); } + public function save() { + $data = []; + + $reflection = new ReflectionClass(get_called_class()); + foreach ($reflection->getProperties(ReflectionProperty::IS_PUBLIC) as $property) { + $field = $property->getName(); + if (property_exists($this, $field)) { + $data[$field] = $this->$field; + } + } + + // If we have an ID, update; otherwise, create + if (isset($this->id) && $this->id > 0) { + return self::update($data); + } else { + $newId = self::create($data); + $this->id = $newId; + return $newId; + } + } + + public function deleteInstance() { + if (!isset($this->id)) { + throw new Exception("Cannot delete model without ID"); + } + return self::delete($this->id); + } + + public static function search($filter = [], $limit = null, $order = ["key" => null]): array { + return self::getAll($filter, $limit, 0, $order); + } + } \ No newline at end of file diff --git a/public/js/pages/ManualInvoice/ManualInvoice.js b/public/js/pages/ManualInvoice/ManualInvoice.js index fbd2682c4..2616cbf35 100644 --- a/public/js/pages/ManualInvoice/ManualInvoice.js +++ b/public/js/pages/ManualInvoice/ManualInvoice.js @@ -4,44 +4,23 @@ Vue.component('manual-invoice', {
- - - - - -