Add Gutschrift functionality to ManualInvoice with modal and backend support
This commit is contained in:
@@ -1,233 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
use chillerlan\QRCode\QRCode;
|
|
||||||
use chillerlan\QRCode\QROptions;
|
|
||||||
use chillerlan\QRCode\Output\QROutputInterface;
|
|
||||||
|
|
||||||
class ManualInvoice extends mfBaseModel {
|
|
||||||
private $positions;
|
|
||||||
private $pdf;
|
|
||||||
|
|
||||||
|
|
||||||
public function createPdf() {
|
|
||||||
if($this->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 ? "<tr><td>Ihre UID:</td><td>" . $this->uid . "</td></tr>" : "", $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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,11 +4,10 @@ class ManualInvoiceController extends TTCrud
|
|||||||
{
|
{
|
||||||
protected string $headerTitle = 'Manuelle Rechnungen';
|
protected string $headerTitle = 'Manuelle Rechnungen';
|
||||||
protected bool $createText = false;
|
protected bool $createText = false;
|
||||||
|
|
||||||
protected array $additionalJS = ["js/pages/ManualInvoice/ManualInvoice.js"];
|
protected array $additionalJS = ["js/pages/ManualInvoice/ManualInvoice.js"];
|
||||||
protected array $additionalHead = ["<link rel='stylesheet' href='/js/pages/ManualInvoice/ManualInvoice.css'>"];
|
protected array $additionalHead = ["<link rel='stylesheet' href='/js/pages/ManualInvoice/ManualInvoice.css'>"];
|
||||||
|
private array $tempPositions = [];
|
||||||
|
|
||||||
//@formatter:off
|
|
||||||
protected array $columns = [
|
protected array $columns = [
|
||||||
['key' => 'id', 'text' => 'ID', 'table' => ['visible' => false], 'modal' => false],
|
['key' => 'id', 'text' => 'ID', 'table' => ['visible' => false], 'modal' => false],
|
||||||
['key' => 'invoice_number', 'text' => 'Rechnungsnr.', 'table' => ['sortable' => true, 'filter' => 'search']],
|
['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' => 'customer_number', 'text' => 'Kundennr.', 'table' => ['sortable' => true, 'filter' => 'search']],
|
||||||
['key' => 'total', 'text' => 'Netto', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']],
|
['key' => 'total', 'text' => 'Netto', 'table' => ['sortable' => true, 'formatter' => 'formatPrice']],
|
||||||
['key' => 'total_gross', 'text' => 'Brutto', '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' => [
|
['key' => 'billing_type', 'text' => 'Zahlungsart', 'table' => ['filter' => 'select', 'filterOptions' => [
|
||||||
['value' => 'invoice', 'text' => 'Rechnung'],
|
['value' => 'invoice', 'text' => 'Rechnung'],
|
||||||
['value' => 'sepa', 'text' => 'SEPA'],
|
['value' => 'sepa', 'text' => 'SEPA'],
|
||||||
]]],
|
]]],
|
||||||
['key' => 'actions', 'text' => 'Aktionen', 'modal' => false, 'table' => ['filter' => false, 'sortable' => false]],
|
['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) {
|
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);
|
$post = json_decode(file_get_contents('php://input'), true);
|
||||||
|
$invoice = (object)[];
|
||||||
|
$positions = [];
|
||||||
|
|
||||||
if (isset($post['preview']) && $post['preview'] === true) {
|
if (isset($post['preview']) && $post['preview'] === true) {
|
||||||
// Create temporary invoice object from POST data for preview
|
$invoice = (object) array_merge([
|
||||||
$invoice = (object)[];
|
'id' => 0, 'invoice_number' => null, 'invoice_date' => time(),
|
||||||
$invoice->id = 0;
|
'customer_number' => 0, 'fibu_account_number' => 0,
|
||||||
$invoice->invoice_number = $post['invoice_number'] ?? null;
|
'company' => '', 'firstname' => '', 'lastname' => '',
|
||||||
$invoice->invoice_date = $post['invoice_date'] ?? time();
|
'street' => '', 'zip' => '', 'city' => '', 'country' => 'Österreich',
|
||||||
$invoice->customer_number = $post['customer_number'] ?? 0;
|
'email' => '', 'uid' => '', 'tax_text' => '', 'billing_type' => 'invoice',
|
||||||
$invoice->fibu_account_number = $post['fibu_account_number'] ?? 0;
|
'total' => 0, 'total_gross' => 0
|
||||||
$invoice->company = $post['company'] ?? '';
|
], $post);
|
||||||
$invoice->firstname = $post['firstname'] ?? '';
|
$positions = array_map(fn($p) => (object)$p, $post['positions'] ?? []);
|
||||||
$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 {
|
} else {
|
||||||
// Load from database
|
|
||||||
$id = $this->request->id ?? $post['id'] ?? null;
|
$id = $this->request->id ?? $post['id'] ?? null;
|
||||||
if (!$id) {
|
if (!$id || !($invoice = ManualInvoiceModel::get($id))) {
|
||||||
http_response_code(500);
|
http_response_code(500);
|
||||||
self::returnJson(['success' => false, 'message' => 'Rechnung wurde nicht gefunden']);
|
self::returnJson(['success' => false, 'message' => 'Rechnung wurde nicht gefunden']);
|
||||||
return;
|
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');
|
$positions = $invoice->getProperty('positions');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate VAT totals
|
|
||||||
$vat = [];
|
$vat = [];
|
||||||
foreach ($positions as $p) {
|
foreach ($positions as $p) {
|
||||||
$vatrate = is_object($p) ? $p->vatrate : $p['vatrate'];
|
$pObj = (object)$p;
|
||||||
$price_gross = is_object($p) ? $p->price_gross : ($p['price_gross'] ?? 0);
|
$vat[$pObj->vatrate] = ($vat[$pObj->vatrate] ?? 0) + ($pObj->price_gross ?? 0) - ($pObj->price_total ?? 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;
|
$invoice->positions = $positions;
|
||||||
|
|
||||||
$pdf_vars = [
|
$pdf_vars = [
|
||||||
"invoice" => $invoice,
|
"invoice" => $invoice,
|
||||||
"vat" => $vat,
|
"vat" => $vat,
|
||||||
@@ -101,44 +75,40 @@ class ManualInvoiceController extends TTCrud
|
|||||||
"bank_owner" => TT_INVOICE_BANK_OWNER
|
"bank_owner" => TT_INVOICE_BANK_OWNER
|
||||||
];
|
];
|
||||||
|
|
||||||
// Replace placeholders in header
|
$replacements = [
|
||||||
$headerHtml = file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_HEADER.html");
|
"{{ basedir }}" => BASEDIR,
|
||||||
$headerHtml = str_replace("{{ basedir }}", BASEDIR, $headerHtml);
|
"{{ addressLine_1 }}" => $invoice->company ?: "",
|
||||||
$headerHtml = str_replace("{{ addressLine_1 }}", $invoice->company ? $invoice->company : "", $headerHtml);
|
"{{ addressLine_2 }}" => trim($invoice->firstname . " " . $invoice->lastname),
|
||||||
$headerHtml = str_replace("{{ addressLine_2 }}", trim($invoice->firstname . " " . $invoice->lastname), $headerHtml);
|
"{{ addressLine_3 }}" => $invoice->street ?? '',
|
||||||
$headerHtml = str_replace("{{ addressLine_3 }}", $invoice->street ?? '', $headerHtml);
|
"{{ addressLine_4 }}" => ($invoice->zip ?? '') . " " . ($invoice->city ?? ''),
|
||||||
$headerHtml = str_replace("{{ addressLine_4 }}", ($invoice->zip ?? '') . " " . ($invoice->city ?? ''), $headerHtml);
|
"{{ addressLine_5 }}" => ($invoice->country ?? '') != "Österreich" ? ($invoice->country ?? '') : "",
|
||||||
$headerHtml = str_replace("{{ addressLine_5 }}", ($invoice->country ?? '') != "Österreich" ? ($invoice->country ?? '') : "", $headerHtml);
|
"{{ customerNumber }}" => $invoice->customer_number ?? '',
|
||||||
$headerHtml = str_replace("{{ customerNumber }}", $invoice->customer_number ?? '', $headerHtml);
|
"{{ billingAccount }}" => $invoice->fibu_account_number ?? '',
|
||||||
$headerHtml = str_replace("{{ billingAccount }}", $invoice->fibu_account_number ?? '', $headerHtml);
|
"{{ invoiceNumber }}" => $invoice->invoice_number ?? "VORSCHAU",
|
||||||
$headerHtml = str_replace("{{ invoiceNumber }}", $invoice->invoice_number ?? "VORSCHAU", $headerHtml);
|
"{{ invoiceDate }}" => date("d.m.Y", $invoice->invoice_date ?? time()),
|
||||||
$headerHtml = str_replace("{{ invoiceDate }}", date("d.m.Y", $invoice->invoice_date ?? time()), $headerHtml);
|
"{{ vatHtml }}" => ($invoice->uid ?? '') ? "<tr><td>Ihre UID:</td><td>" . $invoice->uid . "</td></tr>" : "",
|
||||||
$headerHtml = str_replace("{{ vatHtml }}", ($invoice->uid ?? '') ? "<tr><td>Ihre UID:</td><td>" . $invoice->uid . "</td></tr>" : "", $headerHtml);
|
"{{ qrCodeSrc }}" => $this->generateSepaQRCode($invoice->invoice_number ?? "VORSCHAU", round($invoice->total_gross ?? 0, 2))
|
||||||
|
];
|
||||||
// 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);
|
|
||||||
|
|
||||||
|
$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";
|
$headerFile = BASEDIR . "/var/temp/manualinvoice_header-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
||||||
file_put_contents($headerFile, $headerHtml);
|
file_put_contents($headerFile, $headerHtml);
|
||||||
|
|
||||||
// Replace placeholders in footer
|
$footerReplacements = [
|
||||||
$footerHtml = file_get_contents(BASEDIR . "/Layout/default/ManualInvoice/PDF_FOOTER.html");
|
"{{ bank_iban }}" => TT_INVOICE_BANK_IBAN_FORMATTED,
|
||||||
$footerHtml = str_replace("{{ bank_iban }}", TT_INVOICE_BANK_IBAN_FORMATTED, $footerHtml);
|
"{{ bank_bic }}" => TT_INVOICE_BANK_BIC,
|
||||||
$footerHtml = str_replace("{{ bank_bic }}", TT_INVOICE_BANK_BIC, $footerHtml);
|
"{{ bank_bank }}" => TT_INVOICE_BANK_BANK,
|
||||||
$footerHtml = str_replace("{{ bank_bank }}", TT_INVOICE_BANK_BANK, $footerHtml);
|
"{{ bank_owner }}" => TT_INVOICE_BANK_OWNER
|
||||||
$footerHtml = str_replace("{{ bank_owner }}", TT_INVOICE_BANK_OWNER, $footerHtml);
|
];
|
||||||
|
$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";
|
$footerFile = BASEDIR . "/var/temp/manualinvoice_footer-" . date("U") . "-" . rand(1000, 9999) . ".html";
|
||||||
file_put_contents($footerFile, $footerHtml);
|
file_put_contents($footerFile, $footerHtml);
|
||||||
|
|
||||||
$pdf = new PdfForm("ManualInvoice/PDF_MAIN", $pdf_vars);
|
$pdf = new PdfForm("ManualInvoice/PDF_MAIN", $pdf_vars);
|
||||||
$wkhtmltopdfArgs = "--header-html $headerFile --footer-html $footerFile";
|
$filename = $pdf->render("--header-html $headerFile --footer-html $footerFile");
|
||||||
$filename = $pdf->render($wkhtmltopdfArgs);
|
|
||||||
|
|
||||||
if ($returnFilename === true) return $filename;
|
if ($returnFilename === true) return $filename;
|
||||||
|
|
||||||
// Return the PDF inline for preview
|
|
||||||
header('Content-Type: application/pdf');
|
header('Content-Type: application/pdf');
|
||||||
header('Content-Disposition: inline; filename="' . ($invoice->invoice_number ?? 'preview') . '.pdf"');
|
header('Content-Disposition: inline; filename="' . ($invoice->invoice_number ?? 'preview') . '.pdf"');
|
||||||
readfile($filename);
|
readfile($filename);
|
||||||
@@ -147,113 +117,185 @@ class ManualInvoiceController extends TTCrud
|
|||||||
|
|
||||||
protected function downloadInvoicePdfAction() {
|
protected function downloadInvoicePdfAction() {
|
||||||
$id = $this->request->id;
|
$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->layout()->setFlash("Rechnung nicht gefunden", "error");
|
||||||
$this->redirect("ManualInvoice");
|
$this->redirect("ManualInvoice");
|
||||||
}
|
}
|
||||||
|
|
||||||
$invoice = new ManualInvoice($id);
|
if(!($pdf_filename = $this->createPDFAction(true)) || !file_exists($pdf_filename)) {
|
||||||
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->layout()->setFlash("PDF-Datei konnte nicht erstellt werden");
|
||||||
$this->redirect("ManualInvoice");
|
$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-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);
|
readfile($pdf_filename);
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function beforeCreate(&$data): bool {
|
protected function beforeCreate(&$data): bool {
|
||||||
// Generate invoice number if not provided
|
if (isset($data['positions']) && is_array($data['positions'])) {
|
||||||
if (empty($data['invoice_number'])) {
|
$this->tempPositions = $data['positions'];
|
||||||
$data['invoice_number'] = ManualInvoiceModel::getNextInvoiceNumber();
|
unset($data['positions']);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set default values
|
$me = new User();
|
||||||
if (empty($data['invoice_date'])) {
|
$me->loadMe();
|
||||||
$data['invoice_date'] = time();
|
|
||||||
}
|
$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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function afterCreate($data) {
|
protected function afterCreate($data) {
|
||||||
$invoiceId = $data['id'];
|
$this->savePositions($data['id']);
|
||||||
|
$this->recalculateTotals($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 {
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function afterUpdate($data) {
|
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
|
$this->savePositions($data['id']);
|
||||||
$existingPositions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]);
|
$this->recalculateTotals($data['id']);
|
||||||
foreach ($existingPositions as $pos) {
|
|
||||||
$pos->delete();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save new positions
|
private function savePositions($invoiceId) {
|
||||||
if (isset($data['positions']) && is_array($data['positions'])) {
|
if (empty($this->tempPositions)) return;
|
||||||
foreach ($data['positions'] as $position) {
|
|
||||||
$position['manualinvoice_id'] = $invoiceId;
|
|
||||||
$posModel = ManualInvoicepositionModel::create($position);
|
|
||||||
$posModel->save();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recalculate totals
|
$me = new User();
|
||||||
$this->recalculateTotals($invoiceId);
|
$me->loadMe();
|
||||||
|
|
||||||
|
foreach ($this->tempPositions as $position) {
|
||||||
|
ManualInvoicepositionModel::create(array_merge([
|
||||||
|
'manualinvoice_id' => $invoiceId,
|
||||||
|
'start_date' => date('Y-m-d'),
|
||||||
|
'create_by' => $me->id,
|
||||||
|
'edit_by' => $me->id,
|
||||||
|
'create' => time(),
|
||||||
|
'edit' => time()
|
||||||
|
], $position));
|
||||||
|
}
|
||||||
|
$this->tempPositions = [];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function recalculateTotals($invoiceId) {
|
protected function recalculateTotals($invoiceId) {
|
||||||
$invoice = new ManualInvoice($invoiceId);
|
if (!($invoice = ManualInvoiceModel::get($invoiceId))) return;
|
||||||
|
|
||||||
$positions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]);
|
$positions = ManualInvoicepositionModel::search(['manualinvoice_id' => $invoiceId]);
|
||||||
|
$invoice->total = array_sum(array_column($positions, 'price_total'));
|
||||||
$total = 0;
|
$invoice->total_gross = array_sum(array_column($positions, 'price_gross'));
|
||||||
$total_gross = 0;
|
$invoice->save();
|
||||||
|
|
||||||
foreach ($positions as $pos) {
|
|
||||||
$total += $pos->price_total;
|
|
||||||
$total_gross += $pos->price_gross;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$invoice->total = $total;
|
protected function getAction() {
|
||||||
$invoice->total_gross = $total_gross;
|
$filter = $this->postData['filters'] ?? [];
|
||||||
$invoice->save();
|
$order = $this->postData['order']['key'] ? $this->postData['order'] : ($this->defaultOrder ?? ['key' => null, 'order' => 'ASC']);
|
||||||
|
$page = $this->postData['pagination']['page'] ?? 1;
|
||||||
|
$perPage = $this->postData['pagination']['per_page'] ?? 10;
|
||||||
|
|
||||||
|
$rows = ManualInvoiceModel::getAll($filter, $perPage, ($page - 1) * $perPage, $order);
|
||||||
|
$filteredAvailable = ManualInvoiceModel::count($filter);
|
||||||
|
$totalRows = ManualInvoiceModel::count();
|
||||||
|
|
||||||
|
foreach ($rows as &$row) {
|
||||||
|
$row->customerName = trim(($row->company ?: '') . ' ' . $row->firstname . ' ' . $row->lastname);
|
||||||
|
$row->positions = array_map([$this, 'formatPosition'], $row->getProperty('positions'));
|
||||||
|
}
|
||||||
|
|
||||||
|
self::returnJson([
|
||||||
|
"rows" => $rows,
|
||||||
|
"autoCompleteData" => [],
|
||||||
|
"pagination" => [
|
||||||
|
"page" => $page,
|
||||||
|
"total_pages" => ceil($filteredAvailable / $perPage),
|
||||||
|
"per_page" => $perPage,
|
||||||
|
"filtered_available" => intval($filteredAvailable),
|
||||||
|
"total_rows" => intval($totalRows)
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected function getByIdAction() {
|
||||||
|
$id = $_GET['id'] ?? null;
|
||||||
|
if (!$id || !is_numeric($id)) {
|
||||||
|
http_response_code(500);
|
||||||
|
self::returnJson(['success' => false, 'message' => 'No ID provided.']);
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!($invoice = ManualInvoiceModel::get($id))) {
|
||||||
|
http_response_code(404);
|
||||||
|
self::returnJson(['success' => false, 'message' => 'Invoice not found.']);
|
||||||
|
die();
|
||||||
|
}
|
||||||
|
|
||||||
|
$data = (array) $invoice;
|
||||||
|
$data['positions'] = array_map([$this, 'formatPosition'], $invoice->getProperty('positions'));
|
||||||
|
|
||||||
|
self::returnJson($data);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function formatPosition($pos) {
|
||||||
|
return [
|
||||||
|
'id' => $pos->id,
|
||||||
|
'manualinvoice_id' => $pos->manualinvoice_id,
|
||||||
|
'billing_id' => $pos->billing_id,
|
||||||
|
'contract_id' => $pos->contract_id,
|
||||||
|
'start_date' => $pos->start_date,
|
||||||
|
'end_date' => $pos->end_date,
|
||||||
|
'matchcode' => $pos->matchcode,
|
||||||
|
'product_id' => $pos->product_id,
|
||||||
|
'product_name' => $pos->product_name,
|
||||||
|
'product_info' => $pos->product_info,
|
||||||
|
'amount' => $pos->amount,
|
||||||
|
'price' => $pos->price,
|
||||||
|
'price_total' => $pos->price_total,
|
||||||
|
'price_gross' => $pos->price_gross,
|
||||||
|
'vatrate' => $pos->vatrate,
|
||||||
|
'fibu_cost_account' => $pos->fibu_cost_account,
|
||||||
|
'fibu_cost_account_legacy' => $pos->fibu_cost_account_legacy,
|
||||||
|
'fibu_taxcode' => $pos->fibu_taxcode,
|
||||||
|
'billing_period' => $pos->billing_period,
|
||||||
|
'options' => $pos->options
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function customRowsHandler($rows) {
|
protected function customRowsHandler($rows) {
|
||||||
foreach ($rows as &$row) {
|
foreach ($rows as &$row) {
|
||||||
// Add customer name
|
$row->customerName = trim(($row->company ?: '') . ' ' . $row->firstname . ' ' . $row->lastname);
|
||||||
$row->customerName = trim(($row->company ? $row->company : '') . ' ' . $row->firstname . ' ' . $row->lastname);
|
|
||||||
}
|
}
|
||||||
return $rows;
|
return $rows;
|
||||||
}
|
}
|
||||||
@@ -262,20 +304,168 @@ class ManualInvoiceController extends TTCrud
|
|||||||
$xinonIBAN = TT_INVOICE_BANK_IBAN;
|
$xinonIBAN = TT_INVOICE_BANK_IBAN;
|
||||||
$xinonBIC = TT_INVOICE_BANK_BIC;
|
$xinonBIC = TT_INVOICE_BANK_BIC;
|
||||||
$xinonOwner = TT_INVOICE_BANK_OWNER;
|
$xinonOwner = TT_INVOICE_BANK_OWNER;
|
||||||
|
$epc = "BCD\n001\n1\nSCT\n$xinonBIC\n$xinonOwner\n$xinonIBAN\nEUR$amount\nXINO\n$paymentReference\n\nXINON GmbH";
|
||||||
$epc = "BCD
|
|
||||||
001
|
|
||||||
1
|
|
||||||
SCT
|
|
||||||
$xinonBIC
|
|
||||||
$xinonOwner
|
|
||||||
$xinonIBAN
|
|
||||||
EUR$amount
|
|
||||||
XINO
|
|
||||||
$paymentReference
|
|
||||||
|
|
||||||
XINON GmbH";
|
|
||||||
|
|
||||||
return (new \chillerlan\QRCode\QRCode)->render($epc);
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,408 +1,76 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class ManualInvoiceModel {
|
class ManualInvoiceModel extends TTCrudBaseModel {
|
||||||
public $invoice_number;
|
public int $id;
|
||||||
public $invoice_date;
|
public ?string $invoice_number;
|
||||||
public $owner_id;
|
public int $invoice_date;
|
||||||
public $billingaddress_id;
|
public int $owner_id;
|
||||||
public $customer_number;
|
public int $billingaddress_id;
|
||||||
public $fibu_account_number;
|
public int $customer_number;
|
||||||
public $fibu_payment_due;
|
public ?int $fibu_account_number;
|
||||||
public $fibu_payment_skonto;
|
public ?int $fibu_payment_due;
|
||||||
public $fibu_payment_skonto_rate;
|
public int $fibu_payment_skonto;
|
||||||
public $sepa_date;
|
public int $fibu_payment_skonto_rate;
|
||||||
public $sepa_id;
|
public ?string $sepa_date;
|
||||||
public $sepa_last_date;
|
public ?string $sepa_id;
|
||||||
public $fibu_cost_area;
|
public ?string $sepa_last_date;
|
||||||
public $fibu_cost_account;
|
public ?string $fibu_cost_area;
|
||||||
public $fibu_cost_account_legacy;
|
public ?int $fibu_cost_account;
|
||||||
public $fibu_taxcode;
|
public ?int $fibu_cost_account_legacy;
|
||||||
public $tax_text;
|
public ?int $fibu_taxcode;
|
||||||
public $company;
|
public ?string $tax_text;
|
||||||
public $firstname;
|
public ?string $company;
|
||||||
public $lastname;
|
public ?string $firstname;
|
||||||
public $street;
|
public ?string $lastname;
|
||||||
public $zip;
|
public string $street;
|
||||||
public $city;
|
public string $zip;
|
||||||
public $country;
|
public string $city;
|
||||||
public $email;
|
public ?string $country;
|
||||||
public $uid;
|
public ?string $email;
|
||||||
public $billing_type;
|
public ?string $uid;
|
||||||
public $billing_delivery;
|
public string $billing_type;
|
||||||
public $bank_account_bank;
|
public string $billing_delivery;
|
||||||
public $bank_account_owner;
|
public ?string $bank_account_bank;
|
||||||
public $bank_account_iban;
|
public ?string $bank_account_owner;
|
||||||
public $bank_account_bic;
|
public ?string $bank_account_iban;
|
||||||
public $total;
|
public ?string $bank_account_bic;
|
||||||
public $total_gross;
|
public float $total;
|
||||||
public $vatgroup_id;
|
public float $total_gross;
|
||||||
public $bmd_export_date;
|
public int $vatgroup_id;
|
||||||
public $date_delivered;
|
public ?int $bmd_export_date;
|
||||||
public $create_by;
|
public ?int $date_delivered;
|
||||||
public $edit_by;
|
public string $status;
|
||||||
public $create;
|
public ?int $credit_for_invoice_id;
|
||||||
public $edit;
|
public int $create_by;
|
||||||
|
public int $edit_by;
|
||||||
|
public int $create;
|
||||||
public static function create($data) {
|
public int $edit;
|
||||||
$invoice = new ManualInvoice();
|
|
||||||
|
|
||||||
$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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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() {
|
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) {
|
if ($last && preg_match('/^MRN(\d+)-X(\d+)$/', $last, $m)) {
|
||||||
return "MRN".date("Y")."-X000001";
|
$num = ($m[1] == $year) ? $m[2] + 1 : 1;
|
||||||
}
|
|
||||||
|
|
||||||
$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;
|
|
||||||
} else {
|
} else {
|
||||||
$new_year_part = date("Y");
|
$num = 1;
|
||||||
$new_num_part = 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
$new_invoice_num = "MRN$new_year_part-X".str_pad($new_num_part,"6", "0", STR_PAD_LEFT);
|
return sprintf("MRN%s-X%06d", $year, $num);
|
||||||
return $new_invoice_num;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static function getLastInvoiceNumber() {
|
public function getProperty($name) {
|
||||||
$last_invoice = self::getLast(["invoice_number" => true]);
|
if (!$this->id) return null;
|
||||||
if(!$last_invoice || !$last_invoice->invoice_number) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return $last_invoice->invoice_number;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static function getAll($filter = [], $limit = null, $offset = 0, $order = ["key" => null]) {
|
switch ($name) {
|
||||||
$items = [];
|
case 'positions': return ManualInvoicepositionModel::search(['manualinvoice_id' => $this->id]);
|
||||||
|
case 'creator': return $this->create_by ? new User($this->create_by) : null;
|
||||||
$db = FronkDB::singleton();
|
case 'editor': return $this->edit_by ? new User($this->edit_by) : null;
|
||||||
|
default:
|
||||||
$where = self::getSqlFilter($filter);
|
$classname = ucfirst($name);
|
||||||
$sql = "SELECT ManualInvoice.* FROM ManualInvoice WHERE $where";
|
$idfield = $name . '_id';
|
||||||
|
return (property_exists($this, $idfield) && class_exists($classname)) ? new $classname($this->$idfield) : null;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
class ManualInvoiceposition extends mfBaseModel {
|
|
||||||
|
|
||||||
public function getOption($key) {
|
|
||||||
if(!$this->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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,160 +1,28 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
class ManualInvoicepositionModel {
|
class ManualInvoicepositionModel extends TTCrudBaseModel {
|
||||||
public $manualinvoice_id;
|
public int $id;
|
||||||
public $billing_id;
|
public ?int $manualinvoice_id;
|
||||||
public $contract_id;
|
public ?int $billing_id;
|
||||||
public $start_date;
|
public int $contract_id;
|
||||||
public $end_date;
|
public string $start_date;
|
||||||
public $matchcode;
|
public ?string $end_date;
|
||||||
public $product_id;
|
public ?string $matchcode;
|
||||||
public $product_name;
|
public int $product_id;
|
||||||
public $product_info;
|
public string $product_name;
|
||||||
public $amount;
|
public ?string $product_info;
|
||||||
public $price;
|
public float $amount;
|
||||||
public $price_total;
|
public float $price;
|
||||||
public $price_gross;
|
public float $price_total;
|
||||||
public $vatrate;
|
public float $price_gross;
|
||||||
public $fibu_cost_account;
|
public float $vatrate;
|
||||||
public $fibu_cost_account_legacy;
|
public ?int $fibu_cost_account;
|
||||||
public $fibu_taxcode;
|
public ?int $fibu_cost_account_legacy;
|
||||||
public $billing_period;
|
public ?int $fibu_taxcode;
|
||||||
public $options;
|
public int $billing_period;
|
||||||
|
public ?string $options;
|
||||||
public $create_by;
|
public int $create_by;
|
||||||
public $edit_by;
|
public int $edit_by;
|
||||||
public $create;
|
public int $create;
|
||||||
public $edit;
|
public int $edit;
|
||||||
|
|
||||||
|
|
||||||
public static function create(Array $data) {
|
|
||||||
$model = new ManualInvoiceposition();
|
|
||||||
|
|
||||||
foreach($data as $field => $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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
<?php
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Phinx\Migration\AbstractMigration;
|
||||||
|
|
||||||
|
final class AddManualInvoiceStatusAndCreditFields extends AbstractMigration
|
||||||
|
{
|
||||||
|
public function up(): void
|
||||||
|
{
|
||||||
|
if ($this->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") {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
?>
|
||||||
@@ -227,4 +227,36 @@ class TTCrudBaseModel {
|
|||||||
return new static($result->fetch_assoc());
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -4,44 +4,23 @@ Vue.component('manual-invoice', {
|
|||||||
<div class="d-flex justify-content-between align-items-center mb-3">
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
||||||
<tt-button text="Neue Rechnung" icon="fas fa-plus" @click="openModal()" additional-class="btn-primary"/>
|
<tt-button text="Neue Rechnung" icon="fas fa-plus" @click="openModal()" additional-class="btn-primary"/>
|
||||||
</div>
|
</div>
|
||||||
|
<tt-table-crud ref="table" emit-edit @edit="openModal($event)" @createGutschrift="openGutschriftModal($event)">
|
||||||
<tt-table-crud
|
<template v-slot:total="{ row }">{{ formatPrice(row.total) }}</template>
|
||||||
ref="table"
|
<template v-slot:total_gross="{ row }">{{ formatPrice(row.total_gross) }}</template>
|
||||||
emit-edit
|
<template v-slot:invoice_date="{ row }">{{ formatDate(row.invoice_date) }}</template>
|
||||||
@edit="openModal($event)">
|
<template v-slot:customerName="{ row }">{{ row.customerName }}</template>
|
||||||
<template v-slot:total="{ row }">
|
<template v-slot:status="{ row }">
|
||||||
{{ formatPrice(row.total) }}
|
<span :class="getStatusClass(row.status)">{{ getStatusText(row.status) }}</span>
|
||||||
</template>
|
|
||||||
<template v-slot:total_gross="{ row }">
|
|
||||||
{{ formatPrice(row.total_gross) }}
|
|
||||||
</template>
|
|
||||||
<template v-slot:invoice_date="{ row }">
|
|
||||||
{{ formatDate(row.invoice_date) }}
|
|
||||||
</template>
|
|
||||||
<template v-slot:customerName="{ row }">
|
|
||||||
{{ row.customerName }}
|
|
||||||
</template>
|
</template>
|
||||||
<template v-slot:actions="{ row }">
|
<template v-slot:actions="{ row }">
|
||||||
<button class="btn btn-sm btn-primary" @click="downloadPdf(row.id)" title="PDF herunterladen">
|
<button class="btn btn-sm btn-primary" @click="downloadPdf(row.id)" title="PDF herunterladen"><i class="fas fa-file-pdf"></i></button>
|
||||||
<i class="fas fa-file-pdf"></i>
|
|
||||||
</button>
|
|
||||||
</template>
|
</template>
|
||||||
</tt-table-crud>
|
</tt-table-crud>
|
||||||
|
<manual-invoice-modal v-if="isModalOpen" :initial-data="editingInvoiceData" @close="closeModal" @save="handleSave"/>
|
||||||
<manual-invoice-modal
|
<gutschrift-modal v-if="isGutschriftModalOpen" :invoice-id="gutschriftInvoiceId" @close="closeGutschriftModal" @created="handleGutschriftCreated"/>
|
||||||
v-if="isModalOpen"
|
|
||||||
:initial-data="editingInvoiceData"
|
|
||||||
@close="closeModal"
|
|
||||||
@save="handleSave"
|
|
||||||
/>
|
|
||||||
</tt-card>
|
</tt-card>
|
||||||
`,
|
`,
|
||||||
data() {
|
data: () => ({ isModalOpen: false, editingInvoiceData: null, isGutschriftModalOpen: false, gutschriftInvoiceId: null }),
|
||||||
return {
|
|
||||||
isModalOpen: false,
|
|
||||||
editingInvoiceData: null,
|
|
||||||
}
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
openModal(invoice = null) {
|
openModal(invoice = null) {
|
||||||
this.editingInvoiceData = invoice ? JSON.parse(JSON.stringify(invoice)) : null;
|
this.editingInvoiceData = invoice ? JSON.parse(JSON.stringify(invoice)) : null;
|
||||||
@@ -54,81 +33,68 @@ Vue.component('manual-invoice', {
|
|||||||
},
|
},
|
||||||
async handleSave(invoiceData) {
|
async handleSave(invoiceData) {
|
||||||
try {
|
try {
|
||||||
// Calculate totals for each position
|
|
||||||
const positions = invoiceData.positions.map(p => {
|
const positions = invoiceData.positions.map(p => {
|
||||||
const amount = parseFloat(p.amount) || 0;
|
const amount = parseFloat(p.amount) || 0;
|
||||||
const price = parseFloat(p.price) || 0;
|
const price = parseFloat(p.price) || 0;
|
||||||
const vatrate = parseFloat(p.vatrate) || 0;
|
const vatrate = parseFloat(p.vatrate) || 0;
|
||||||
const price_total = amount * price;
|
|
||||||
const price_gross = price_total * (1 + vatrate / 100);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...p,
|
...p, amount, price, vatrate,
|
||||||
amount,
|
price_total: amount * price,
|
||||||
price,
|
price_gross: (amount * price) * (1 + vatrate / 100),
|
||||||
vatrate,
|
product_id: p.product_id || 0,
|
||||||
price_total,
|
contract_id: p.contract_id || 0,
|
||||||
price_gross,
|
billing_id: p.billing_id || null,
|
||||||
product_id: 0,
|
billing_period: p.billing_period || 0,
|
||||||
contract_id: 0,
|
start_date: p.start_date || moment().format('YYYY-MM-DD'),
|
||||||
billing_id: 0,
|
end_date: p.end_date || null,
|
||||||
billing_period: 0
|
matchcode: p.matchcode || null,
|
||||||
|
fibu_cost_account: p.fibu_cost_account || null,
|
||||||
|
fibu_cost_account_legacy: p.fibu_cost_account_legacy || null,
|
||||||
|
fibu_taxcode: p.fibu_taxcode || null,
|
||||||
|
options: p.options || null
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Prepare invoice data
|
|
||||||
const payload = {
|
const payload = {
|
||||||
id: invoiceData.id || null,
|
...invoiceData,
|
||||||
invoice_number: invoiceData.invoice_number,
|
positions,
|
||||||
invoice_date: invoiceData.invoice_date,
|
|
||||||
owner_id: invoiceData.owner_id || 0,
|
owner_id: invoiceData.owner_id || 0,
|
||||||
billingaddress_id: invoiceData.billingaddress_id || 0,
|
billingaddress_id: invoiceData.billingaddress_id || 0,
|
||||||
customer_number: invoiceData.customer_number || 0,
|
customer_number: invoiceData.customer_number || 0,
|
||||||
company: invoiceData.company || '',
|
|
||||||
firstname: invoiceData.firstname || '',
|
|
||||||
lastname: invoiceData.lastname || '',
|
|
||||||
street: invoiceData.street || '',
|
|
||||||
zip: invoiceData.zip || '',
|
|
||||||
city: invoiceData.city || '',
|
|
||||||
country: invoiceData.country || 'Österreich',
|
country: invoiceData.country || 'Österreich',
|
||||||
email: invoiceData.email || '',
|
|
||||||
uid: invoiceData.uid || '',
|
|
||||||
billing_type: invoiceData.billing_type || 'invoice',
|
billing_type: invoiceData.billing_type || 'invoice',
|
||||||
billing_delivery: 'email',
|
billing_delivery: 'email',
|
||||||
tax_text: invoiceData.tax_text || '',
|
|
||||||
fibu_payment_due: 14,
|
fibu_payment_due: 14,
|
||||||
fibu_account_number: invoiceData.fibu_account_number || 0,
|
fibu_account_number: invoiceData.fibu_account_number || 0,
|
||||||
vatgroup_id: 1,
|
vatgroup_id: 1
|
||||||
positions: positions
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const url = invoiceData.id
|
const url = invoiceData.id ? window.TT_CONFIG.UPDATE_URL : window.TT_CONFIG.CREATE_URL;
|
||||||
? window.TT_CONFIG.UPDATE_URL
|
const { data } = await axios.post(url, payload);
|
||||||
: window.TT_CONFIG.CREATE_URL;
|
|
||||||
|
|
||||||
const response = await axios.post(url, payload);
|
if (data.success) {
|
||||||
|
window.notify('success', data.message || 'Rechnung erfolgreich gespeichert!');
|
||||||
if (response.data.success) {
|
|
||||||
window.notify('success', response.data.message || 'Rechnung erfolgreich gespeichert!');
|
|
||||||
this.closeModal();
|
this.closeModal();
|
||||||
} else {
|
} else {
|
||||||
window.notify('error', response.data.message || 'Fehler beim Speichern der Rechnung');
|
window.notify('error', data.message || 'Fehler beim Speichern der Rechnung');
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (e) {
|
||||||
console.error('Error saving invoice:', error);
|
console.error('Error saving invoice:', e);
|
||||||
window.notify('error', 'Fehler beim Speichern der Rechnung: ' + (error.response?.data?.message || error.message));
|
window.notify('error', 'Fehler: ' + (e.response?.data?.message || e.message));
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
downloadPdf(invoiceId) {
|
downloadPdf(id) { window.location.href = `${window.TT_CONFIG.BASE_PATH}/ManualInvoice/downloadInvoicePdf?id=${id}`; },
|
||||||
window.location.href = `${window.TT_CONFIG.BASE_PATH}/ManualInvoice/downloadInvoicePdf?id=${invoiceId}`;
|
formatPrice(v) { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(v || 0); },
|
||||||
|
formatDate(ts) { return ts ? moment.unix(ts).format('DD.MM.YYYY') : '–'; },
|
||||||
|
openGutschriftModal(invoice) {
|
||||||
|
if (invoice.total < 0) return window.notify('error', 'Kann keine Gutschrift für eine Gutschrift erstellen');
|
||||||
|
this.gutschriftInvoiceId = invoice.id;
|
||||||
|
this.isGutschriftModalOpen = true;
|
||||||
},
|
},
|
||||||
formatPrice(value) {
|
closeGutschriftModal() { this.isGutschriftModalOpen = false; this.gutschriftInvoiceId = null; },
|
||||||
return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(value || 0);
|
handleGutschriftCreated() { this.closeGutschriftModal(); this.$refs.table.$refs.table.refreshTable(); },
|
||||||
},
|
getStatusClass(s) { return { 'draft': 'badge badge-secondary', 'finalized': 'badge badge-success', 'exported': 'badge badge-primary' }[s] || 'badge badge-secondary'; },
|
||||||
formatDate(timestamp) {
|
getStatusText(s) { return { 'draft': 'Entwurf', 'finalized': 'Finalisiert', 'exported': 'Exportiert' }[s] || s; }
|
||||||
if (!timestamp) return '–';
|
|
||||||
return moment.unix(timestamp).format('DD.MM.YYYY');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -136,10 +102,7 @@ Vue.component('manual-invoice-modal', {
|
|||||||
props: ['initialData'],
|
props: ['initialData'],
|
||||||
template: `
|
template: `
|
||||||
<div class="manual-invoice-overlay" :class="overlayClasses" tabindex="-1" ref="overlay">
|
<div class="manual-invoice-overlay" :class="overlayClasses" tabindex="-1" ref="overlay">
|
||||||
<div class="info-bar" v-if="!isLargeScreen">
|
<div class="info-bar" v-if="!isLargeScreen"><i class="fas fa-info-circle mr-2"></i> Drücke <strong>STRG + Q</strong> um die Vorschau umzuschalten.</div>
|
||||||
<i class="fas fa-info-circle mr-2"></i> Drücke <strong>STRG + Q</strong> um die Vorschau umzuschalten.
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="invoice-editor-pane" v-show="isLargeScreen || !showPreviewOnSmallScreen">
|
<div class="invoice-editor-pane" v-show="isLargeScreen || !showPreviewOnSmallScreen">
|
||||||
<div class="editor-header">
|
<div class="editor-header">
|
||||||
<h3>{{ isCreateMode ? 'Neue Rechnung' : 'Rechnung bearbeiten' }}</h3>
|
<h3>{{ isCreateMode ? 'Neue Rechnung' : 'Rechnung bearbeiten' }}</h3>
|
||||||
@@ -149,37 +112,29 @@ Vue.component('manual-invoice-modal', {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="editor-content">
|
<div class="editor-content">
|
||||||
<tt-card>
|
<tt-card><template v-slot:header><h5><i class="fas fa-user-tie mr-2"></i>Kunde</h5></template>
|
||||||
<template v-slot:header><h5><i class="fas fa-user-tie mr-2"></i>Kunde</h5></template>
|
|
||||||
<tt-autocomplete label="Kunde suchen" :api-url="customerApiUrl" v-model="invoiceData.billingaddress_id" sm row />
|
<tt-autocomplete label="Kunde suchen" :api-url="customerApiUrl" v-model="invoiceData.billingaddress_id" sm row />
|
||||||
</tt-card>
|
</tt-card>
|
||||||
<tt-card>
|
<tt-card><template v-slot:header><h5><i class="fas fa-file-invoice mr-2"></i>Rechnungsdetails</h5></template>
|
||||||
<template v-slot:header><h5><i class="fas fa-file-invoice mr-2"></i>Rechnungsdetails</h5></template>
|
|
||||||
<div class="form-grid">
|
<div class="form-grid">
|
||||||
<tt-input label="Rechnungsnr." v-model="invoiceData.invoice_number" sm/>
|
<tt-input label="Rechnungsnr." v-model="invoiceData.invoice_number" sm/>
|
||||||
<tt-date-picker label="Rechnungsdatum" v-model="invoiceData.invoice_date" :date-range="false" sm/>
|
<tt-date-picker label="Rechnungsdatum" v-model="invoiceData.invoice_date" :date-range="false" sm/>
|
||||||
<tt-select label="Zahlungsart" v-model="invoiceData.billing_type" :options="billingTypeOptions" sm/>
|
<tt-select label="Zahlungsart" v-model="invoiceData.billing_type" :options="billingTypeOptions" sm/>
|
||||||
</div>
|
</div>
|
||||||
</tt-card>
|
</tt-card>
|
||||||
<tt-card>
|
<tt-card><template v-slot:header><h5><i class="fas fa-list-ol mr-2"></i>Positionen</h5></template>
|
||||||
<template v-slot:header><h5><i class="fas fa-list-ol mr-2"></i>Positionen</h5></template>
|
|
||||||
<tt-positions-manager ref="positionsManager" v-model="invoiceData.positions" :config="positionsConfig" />
|
<tt-positions-manager ref="positionsManager" v-model="invoiceData.positions" :config="positionsConfig" />
|
||||||
</tt-card>
|
</tt-card>
|
||||||
<tt-card>
|
<tt-card><template v-slot:header><h5><i class="fas fa-paragraph mr-2"></i>Texte</h5></template>
|
||||||
<template v-slot:header><h5><i class="fas fa-paragraph mr-2"></i>Texte</h5></template>
|
<tt-textarea label="Steuerhinweis" v-model="invoiceData.tax_text" rows="2"/>
|
||||||
<tt-textarea label="Steuerhinweis (z.B. Reverse Charge)" v-model="invoiceData.tax_text" rows="2"/>
|
|
||||||
</tt-card>
|
</tt-card>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="invoice-preview-pane" v-show="isLargeScreen || showPreviewOnSmallScreen">
|
<div class="invoice-preview-pane" v-show="isLargeScreen || showPreviewOnSmallScreen">
|
||||||
<div class="pdf-preview-container">
|
<div class="pdf-preview-container">
|
||||||
<div v-if="pdfLoading" class="pdf-loading">
|
<div v-if="pdfLoading" class="pdf-loading"><i class="fas fa-spinner fa-spin fa-3x"></i><p>PDF wird generiert...</p></div>
|
||||||
<i class="fas fa-spinner fa-spin fa-3x"></i>
|
|
||||||
<p>PDF wird generiert...</p>
|
|
||||||
</div>
|
|
||||||
<object v-else :data="pdfPreviewUrl" type="application/pdf" width="100%" height="100%">
|
<object v-else :data="pdfPreviewUrl" type="application/pdf" width="100%" height="100%">
|
||||||
<p>PDF Vorschau kann nicht angezeigt werden. <a :href="pdfPreviewUrl" target="_blank">Hier klicken zum Öffnen</a></p>
|
<p>PDF Vorschau kann nicht angezeigt werden. <a :href="pdfPreviewUrl" target="_blank">Hier klicken</a></p>
|
||||||
</object>
|
</object>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -187,41 +142,20 @@ Vue.component('manual-invoice-modal', {
|
|||||||
`,
|
`,
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
isCreateMode: !this.initialData || !this.initialData.id,
|
isCreateMode: !this.initialData?.id,
|
||||||
customerApiUrl: window.TT_CONFIG['BASE_PATH'] + '/Address/Api?do=findAddress&fibu_primary_account=1',
|
customerApiUrl: window.TT_CONFIG.BASE_PATH + '/Address/Api?do=findAddress&fibu_primary_account=1',
|
||||||
selectedCustomerObject: {},
|
|
||||||
isLargeScreen: window.innerWidth >= 1920,
|
isLargeScreen: window.innerWidth >= 1920,
|
||||||
showPreviewOnSmallScreen: false,
|
showPreviewOnSmallScreen: false,
|
||||||
pdfLoading: false,
|
pdfLoading: false,
|
||||||
pdfPreviewUrl: '',
|
pdfPreviewUrl: '',
|
||||||
previewDebounceTimer: null,
|
previewDebounceTimer: null,
|
||||||
invoiceData: {
|
invoiceData: {
|
||||||
id: null,
|
id: null, invoice_number: `MRN${new Date().getFullYear()}-X000001`, invoice_date: moment().unix(),
|
||||||
invoice_number: `MRN${new Date().getFullYear()}-X000001`,
|
billingaddress_id: null, owner_id: null, customer_number: 0, fibu_account_number: 0,
|
||||||
invoice_date: moment().unix(),
|
company: '', firstname: '', lastname: '', street: '', zip: '', city: '', country: 'Österreich',
|
||||||
billingaddress_id: null,
|
uid: '', email: '', billing_type: 'invoice', tax_text: '', positions: [], total: 0, total_gross: 0
|
||||||
owner_id: null,
|
|
||||||
customer_number: 0,
|
|
||||||
fibu_account_number: 0,
|
|
||||||
company: '',
|
|
||||||
firstname: '',
|
|
||||||
lastname: '',
|
|
||||||
street: '',
|
|
||||||
zip: '',
|
|
||||||
city: '',
|
|
||||||
country: 'Österreich',
|
|
||||||
uid: '',
|
|
||||||
email: '',
|
|
||||||
billing_type: 'invoice',
|
|
||||||
tax_text: '',
|
|
||||||
positions: [],
|
|
||||||
total: 0,
|
|
||||||
total_gross: 0
|
|
||||||
},
|
},
|
||||||
billingTypeOptions: [
|
billingTypeOptions: [{value: 'invoice', text: 'Rechnung'}, {value: 'sepa', text: 'SEPA'}],
|
||||||
{value: 'invoice', text: 'Rechnung'},
|
|
||||||
{value: 'sepa', text: 'SEPA'}
|
|
||||||
],
|
|
||||||
positionsConfig: {
|
positionsConfig: {
|
||||||
fields: {
|
fields: {
|
||||||
product_name: { type: 'input', label: 'Bezeichnung' },
|
product_name: { type: 'input', label: 'Bezeichnung' },
|
||||||
@@ -232,207 +166,106 @@ Vue.component('manual-invoice-modal', {
|
|||||||
price: { type: 'input', label: 'Einzelpreis (€)', inputType: 'number' },
|
price: { type: 'input', label: 'Einzelpreis (€)', inputType: 'number' },
|
||||||
vatrate: { type: 'input', label: 'USt. (%)', inputType: 'number' },
|
vatrate: { type: 'input', label: 'USt. (%)', inputType: 'number' },
|
||||||
},
|
},
|
||||||
validateForm: (formData) => {
|
validateForm: (d) => {
|
||||||
if (!formData.product_name) { window.notify('error', 'Bezeichnung ist erforderlich.'); return false; }
|
if (!d.product_name) { window.notify('error', 'Bezeichnung ist erforderlich.'); return false; }
|
||||||
if (!formData.amount) { window.notify('error', 'Menge ist erforderlich.'); return false; }
|
if (!d.amount) { window.notify('error', 'Menge ist erforderlich.'); return false; }
|
||||||
if (formData.price === null || formData.price === undefined) { window.notify('error', 'Preis ist erforderlich.'); return false; }
|
if (d.price == null) { window.notify('error', 'Preis ist erforderlich.'); return false; }
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
overlayClasses() {
|
overlayClasses() { return { 'preview-active-small': !this.isLargeScreen && this.showPreviewOnSmallScreen, 'editor-active-small': !this.isLargeScreen && !this.showPreviewOnSmallScreen }; },
|
||||||
return {
|
|
||||||
'preview-active-small': !this.isLargeScreen && this.showPreviewOnSmallScreen,
|
|
||||||
'editor-active-small': !this.isLargeScreen && !this.showPreviewOnSmallScreen,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
totals() {
|
totals() {
|
||||||
let net = 0;
|
let net = 0, vat = {};
|
||||||
const vat = {};
|
(this.invoiceData.positions || []).forEach(p => {
|
||||||
if (!Array.isArray(this.invoiceData.positions)) return { net: 0, vat: {}, gross: 0 };
|
|
||||||
|
|
||||||
this.invoiceData.positions.forEach(p => {
|
|
||||||
const lineTotal = (parseFloat(p.amount) || 0) * (parseFloat(p.price) || 0);
|
const lineTotal = (parseFloat(p.amount) || 0) * (parseFloat(p.price) || 0);
|
||||||
const vatRate = parseInt(p.vatrate) || 0;
|
const r = parseInt(p.vatrate) || 0;
|
||||||
net += lineTotal;
|
net += lineTotal;
|
||||||
if (!vat[vatRate]) { vat[vatRate] = 0; }
|
vat[r] = (vat[r] || 0) + lineTotal * (r / 100);
|
||||||
vat[vatRate] += lineTotal * (vatRate / 100);
|
|
||||||
});
|
});
|
||||||
const gross = net + Object.values(vat).reduce((sum, v) => sum + v, 0);
|
return { net, vat, gross: net + Object.values(vat).reduce((a, b) => a + b, 0) };
|
||||||
return { net, vat, gross };
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'invoiceData': {
|
'invoiceData': { handler() { this.debouncedPreviewUpdate(); }, deep: true },
|
||||||
handler() {
|
|
||||||
this.debouncedPreviewUpdate();
|
|
||||||
},
|
|
||||||
deep: true
|
|
||||||
},
|
|
||||||
'invoiceData.billingaddress_id': {
|
'invoiceData.billingaddress_id': {
|
||||||
async handler(newId) {
|
async handler(newId) {
|
||||||
if (!newId) {
|
if (!newId) return Object.assign(this.invoiceData, {
|
||||||
this.invoiceData.company = '';
|
company: '', firstname: '', lastname: '', street: '', zip: '', city: '',
|
||||||
this.invoiceData.firstname = '';
|
country: 'Österreich', uid: '', email: '', customer_number: 0, fibu_account_number: 0, owner_id: 0
|
||||||
this.invoiceData.lastname = '';
|
});
|
||||||
this.invoiceData.street = '';
|
|
||||||
this.invoiceData.zip = '';
|
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Address/api?do=getAddress&id=${newId}`);
|
||||||
this.invoiceData.city = '';
|
if (data.status === 'OK' && data.result.address) {
|
||||||
this.invoiceData.country = 'Österreich';
|
const a = data.result.address;
|
||||||
this.invoiceData.uid = '';
|
Object.assign(this.invoiceData, {
|
||||||
this.invoiceData.email = '';
|
company: a.company || '', firstname: a.firstname || '', lastname: a.lastname || '',
|
||||||
this.invoiceData.customer_number = 0;
|
street: a.street || '', zip: a.zip || '', city: a.city || '', country: 'Österreich',
|
||||||
this.invoiceData.fibu_account_number = 0;
|
uid: a.uid || '', email: a.email || '', customer_number: a.customer_number || 0,
|
||||||
this.invoiceData.owner_id = 0;
|
fibu_account_number: a.fibu_account_number || 0, owner_id: newId
|
||||||
this.selectedCustomerObject = {};
|
});
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const response = await axios.get(`${window.TT_CONFIG.BASE_PATH}/Address/api?do=getAddress&id=${newId}`);
|
|
||||||
if (response.data.status === 'OK' && response.data.result.address) {
|
|
||||||
const addr = response.data.result.address;
|
|
||||||
this.selectedCustomerObject = addr;
|
|
||||||
this.invoiceData.company = addr.company || '';
|
|
||||||
this.invoiceData.firstname = addr.firstname || '';
|
|
||||||
this.invoiceData.lastname = addr.lastname || '';
|
|
||||||
this.invoiceData.street = addr.street || '';
|
|
||||||
this.invoiceData.zip = addr.zip || '';
|
|
||||||
this.invoiceData.city = addr.city || '';
|
|
||||||
this.invoiceData.country = 'Österreich';
|
|
||||||
this.invoiceData.uid = addr.uid || '';
|
|
||||||
this.invoiceData.email = addr.email || '';
|
|
||||||
this.invoiceData.customer_number = addr.customer_number || 0;
|
|
||||||
this.invoiceData.fibu_account_number = addr.fibu_account_number || 0;
|
|
||||||
this.invoiceData.owner_id = newId;
|
|
||||||
}
|
}
|
||||||
},
|
|
||||||
immediate: true
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
created() {
|
created() {
|
||||||
if (this.initialData) {
|
if (this.initialData) {
|
||||||
this.invoiceData = {
|
this.invoiceData = { ...this.invoiceData, ...JSON.parse(JSON.stringify(this.initialData)) };
|
||||||
...this.invoiceData,
|
if (typeof this.invoiceData.positions === 'string') {
|
||||||
...JSON.parse(JSON.stringify(this.initialData))
|
try { this.invoiceData.positions = JSON.parse(this.invoiceData.positions); } catch { this.invoiceData.positions = []; }
|
||||||
};
|
|
||||||
if (!Array.isArray(this.invoiceData.positions)) {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(this.invoiceData.positions);
|
|
||||||
this.invoiceData.positions = Array.isArray(parsed) ? parsed : [];
|
|
||||||
} catch (e) {
|
|
||||||
this.invoiceData.positions = [];
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
if (!Array.isArray(this.invoiceData.positions)) this.invoiceData.positions = [];
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
window.addEventListener('resize', this.handleResize);
|
window.addEventListener('resize', this.handleResize);
|
||||||
window.addEventListener('keydown', this.handleGlobalKeydown);
|
window.addEventListener('keydown', this.handleGlobalKeydown);
|
||||||
this.handleResize();
|
this.handleResize();
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => { this.$refs.overlay?.focus(); this.updatePdfPreview(); });
|
||||||
if (this.$refs.overlay) {
|
|
||||||
this.$refs.overlay.focus();
|
|
||||||
}
|
|
||||||
this.updatePdfPreview();
|
|
||||||
});
|
|
||||||
},
|
},
|
||||||
beforeDestroy() {
|
beforeDestroy() {
|
||||||
window.removeEventListener('resize', this.handleResize);
|
window.removeEventListener('resize', this.handleResize);
|
||||||
window.removeEventListener('keydown', this.handleGlobalKeydown);
|
window.removeEventListener('keydown', this.handleGlobalKeydown);
|
||||||
if (this.previewDebounceTimer) {
|
|
||||||
clearTimeout(this.previewDebounceTimer);
|
clearTimeout(this.previewDebounceTimer);
|
||||||
}
|
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
close() { this.$emit('close'); },
|
close() { this.$emit('close'); },
|
||||||
saveInvoice() {
|
saveInvoice() {
|
||||||
if (!this.invoiceData.billingaddress_id) {
|
if (!this.invoiceData.billingaddress_id) return window.notify('error', 'Bitte wählen Sie einen Kunden aus.');
|
||||||
window.notify('error', 'Bitte wählen Sie einen Kunden aus.');
|
if (!this.invoiceData.positions?.length) return window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!this.invoiceData.positions || this.invoiceData.positions.length === 0) {
|
|
||||||
window.notify('error', 'Bitte fügen Sie mindestens eine Position hinzu.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this.$emit('save', this.invoiceData);
|
this.$emit('save', this.invoiceData);
|
||||||
},
|
},
|
||||||
handleResize() { this.isLargeScreen = window.innerWidth >= 1920; },
|
handleResize() { this.isLargeScreen = window.innerWidth >= 1920; },
|
||||||
handleGlobalKeydown(event) {
|
handleGlobalKeydown(e) {
|
||||||
// Handle CTRL+Q to toggle preview on small screens
|
if (e.ctrlKey && e.key === 'q') { e.preventDefault(); this.togglePreviewVisibility(); }
|
||||||
if (event.ctrlKey && event.key === 'q') {
|
|
||||||
event.preventDefault();
|
|
||||||
this.togglePreviewVisibility();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
togglePreviewVisibility() { if (!this.isLargeScreen) this.showPreviewOnSmallScreen = !this.showPreviewOnSmallScreen; },
|
togglePreviewVisibility() { if (!this.isLargeScreen) this.showPreviewOnSmallScreen = !this.showPreviewOnSmallScreen; },
|
||||||
debouncedPreviewUpdate() {
|
debouncedPreviewUpdate() {
|
||||||
if (this.previewDebounceTimer) {
|
|
||||||
clearTimeout(this.previewDebounceTimer);
|
clearTimeout(this.previewDebounceTimer);
|
||||||
}
|
this.previewDebounceTimer = setTimeout(() => this.updatePdfPreview(), 2000);
|
||||||
this.previewDebounceTimer = setTimeout(() => {
|
|
||||||
this.updatePdfPreview();
|
|
||||||
}, 2000);
|
|
||||||
},
|
},
|
||||||
async updatePdfPreview() {
|
async updatePdfPreview() {
|
||||||
this.pdfLoading = true;
|
this.pdfLoading = true;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Calculate position totals
|
|
||||||
const positions = this.invoiceData.positions.map(p => {
|
const positions = this.invoiceData.positions.map(p => {
|
||||||
const amount = parseFloat(p.amount) || 0;
|
const amount = parseFloat(p.amount) || 0;
|
||||||
const price = parseFloat(p.price) || 0;
|
const price = parseFloat(p.price) || 0;
|
||||||
const vatrate = parseFloat(p.vatrate) || 0;
|
const vatrate = parseFloat(p.vatrate) || 0;
|
||||||
const price_total = amount * price;
|
return { ...p, amount, price, vatrate, price_total: amount * price, price_gross: (amount * price) * (1 + vatrate / 100) };
|
||||||
const price_gross = price_total * (1 + vatrate / 100);
|
|
||||||
|
|
||||||
return {
|
|
||||||
...p,
|
|
||||||
amount,
|
|
||||||
price,
|
|
||||||
vatrate,
|
|
||||||
price_total,
|
|
||||||
price_gross
|
|
||||||
};
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
preview: true,
|
preview: true, ...this.invoiceData,
|
||||||
invoice_number: this.invoiceData.invoice_number,
|
total: this.totals.net, total_gross: this.totals.gross, positions
|
||||||
invoice_date: this.invoiceData.invoice_date,
|
|
||||||
customer_number: this.invoiceData.customer_number,
|
|
||||||
fibu_account_number: this.invoiceData.fibu_account_number,
|
|
||||||
company: this.invoiceData.company,
|
|
||||||
firstname: this.invoiceData.firstname,
|
|
||||||
lastname: this.invoiceData.lastname,
|
|
||||||
street: this.invoiceData.street,
|
|
||||||
zip: this.invoiceData.zip,
|
|
||||||
city: this.invoiceData.city,
|
|
||||||
country: this.invoiceData.country,
|
|
||||||
email: this.invoiceData.email,
|
|
||||||
uid: this.invoiceData.uid,
|
|
||||||
tax_text: this.invoiceData.tax_text,
|
|
||||||
billing_type: this.invoiceData.billing_type,
|
|
||||||
total: this.totals.net,
|
|
||||||
total_gross: this.totals.gross,
|
|
||||||
positions: positions
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const response = await axios.post(
|
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/createPDF`, payload, { responseType: 'blob' });
|
||||||
`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/createPDF`,
|
if (this.pdfPreviewUrl) URL.revokeObjectURL(this.pdfPreviewUrl);
|
||||||
payload,
|
this.pdfPreviewUrl = URL.createObjectURL(new Blob([data], { type: 'application/pdf' })) + '#view=FitH';
|
||||||
{ responseType: 'blob' }
|
} catch (e) {
|
||||||
);
|
console.error('Error preview:', e);
|
||||||
|
|
||||||
// Create a blob URL for the PDF
|
|
||||||
const blob = new Blob([response.data], { type: 'application/pdf' });
|
|
||||||
if (this.pdfPreviewUrl) {
|
|
||||||
URL.revokeObjectURL(this.pdfPreviewUrl);
|
|
||||||
}
|
|
||||||
this.pdfPreviewUrl = URL.createObjectURL(blob) + '#view=FitH';
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error generating PDF preview:', error);
|
|
||||||
window.notify('error', 'Fehler beim Generieren der PDF-Vorschau');
|
window.notify('error', 'Fehler beim Generieren der PDF-Vorschau');
|
||||||
} finally {
|
} finally {
|
||||||
this.pdfLoading = false;
|
this.pdfLoading = false;
|
||||||
@@ -440,3 +273,80 @@ Vue.component('manual-invoice-modal', {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
Vue.component('gutschrift-modal', {
|
||||||
|
props: ['invoiceId'],
|
||||||
|
template: `
|
||||||
|
<tt-modal :show="true" @close="close" size="lg" title="Gutschrift erstellen">
|
||||||
|
<div v-if="loading" class="text-center py-5"><i class="fas fa-spinner fa-spin fa-3x"></i><p class="mt-3">Lade Rechnungsdaten...</p></div>
|
||||||
|
<div v-else-if="invoice">
|
||||||
|
<div class="alert alert-info"><strong>Originalrechnung:</strong> {{ invoice.invoice_number }} - {{ invoice.customer_name }}</div>
|
||||||
|
<div v-if="!invoice.positions.length" class="alert alert-warning"><i class="fas fa-exclamation-triangle"></i> Alle Positionen gutgeschrieben.</div>
|
||||||
|
<div v-else>
|
||||||
|
<p><strong>Positionen wählen:</strong></p>
|
||||||
|
<table class="table table-sm table-bordered">
|
||||||
|
<thead><tr>
|
||||||
|
<th style="width: 50px;"><input type="checkbox" @change="toggleAll" v-model="allSelected"></th>
|
||||||
|
<th>Bezeichnung</th><th style="width: 100px;">Orig.</th><th style="width: 100px;">Gutschr.</th><th style="width: 100px;">Verfügbar</th>
|
||||||
|
<th style="width: 120px;">Neu Gutschrift</th><th style="width: 100px;">Einzel</th><th style="width: 100px;">Gesamt</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
<tr v-for="(pos, index) in invoice.positions" :key="index">
|
||||||
|
<td class="text-center"><input type="checkbox" v-model="selectedPositions[index]"></td>
|
||||||
|
<td><strong>{{ pos.product_name }}</strong><div v-if="pos.product_info" class="text-muted small">{{ pos.product_info }}</div></td>
|
||||||
|
<td class="text-right">{{ pos.original_amount }}</td><td class="text-right">{{ pos.credited_amount }}</td>
|
||||||
|
<td class="text-right">{{ pos.available_amount }}</td>
|
||||||
|
<td><input type="number" class="form-control form-control-sm" v-model.number="creditAmounts[index]" :max="pos.available_amount" :disabled="!selectedPositions[index]" step="0.001" min="0.001"></td>
|
||||||
|
<td class="text-right">{{ formatPrice(pos.price) }}</td><td class="text-right">{{ formatPrice(calcTotal(index)) }}</td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
<tfoot><tr><td colspan="7" class="text-right"><strong>Gesamt:</strong></td><td class="text-right"><strong>{{ formatPrice(totalCredit) }}</strong></td></tr></tfoot>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<template v-slot:footer>
|
||||||
|
<tt-button text="Erstellen" icon="fas fa-check" @click="create" additional-class="btn-success" :disabled="!validSelection || creating"/>
|
||||||
|
<tt-button text="Abbrechen" icon="fas fa-times" @click="close" additional-class="btn-secondary"/>
|
||||||
|
</template>
|
||||||
|
</tt-modal>
|
||||||
|
`,
|
||||||
|
data: () => ({ loading: true, creating: false, invoice: null, selectedPositions: {}, creditAmounts: {}, allSelected: false }),
|
||||||
|
computed: {
|
||||||
|
validSelection() { return this.invoice && Object.keys(this.selectedPositions).some(i => this.selectedPositions[i] && this.creditAmounts[i] > 0 && this.creditAmounts[i] <= this.invoice.positions[i].available_amount); },
|
||||||
|
totalCredit() { return Object.keys(this.selectedPositions).reduce((sum, i) => this.selectedPositions[i] ? sum + ((this.creditAmounts[i] || 0) * this.invoice.positions[i].price) : sum, 0); }
|
||||||
|
},
|
||||||
|
async mounted() {
|
||||||
|
try {
|
||||||
|
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getInvoiceForGutschrift?id=${this.invoiceId}`);
|
||||||
|
if (data.success) {
|
||||||
|
this.invoice = data.invoice;
|
||||||
|
this.invoice.positions.forEach((p, i) => { this.$set(this.selectedPositions, i, false); this.$set(this.creditAmounts, i, p.available_amount); });
|
||||||
|
} else {
|
||||||
|
window.notify('error', data.message || 'Fehler'); this.close();
|
||||||
|
}
|
||||||
|
} catch (e) { window.notify('error', 'Fehler'); this.close(); } finally { this.loading = false; }
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
toggleAll() { this.invoice.positions.forEach((p, i) => this.$set(this.selectedPositions, i, this.allSelected)); },
|
||||||
|
calcTotal(i) { return this.selectedPositions[i] ? (this.creditAmounts[i] || 0) * this.invoice.positions[i].price : 0; },
|
||||||
|
async create() {
|
||||||
|
const positions = this.invoice.positions
|
||||||
|
.map((p, i) => ({ p, i })).filter(({ i }) => this.selectedPositions[i])
|
||||||
|
.map(({ p, i }) => {
|
||||||
|
const amt = this.creditAmounts[i];
|
||||||
|
if (amt > p.available_amount) throw new Error(`Menge zu hoch: ${p.product_name}`);
|
||||||
|
return amt > 0 ? { ...p, amount: amt } : null;
|
||||||
|
}).filter(Boolean);
|
||||||
|
|
||||||
|
if (!positions.length) return window.notify('error', 'Keine Positionen gewählt');
|
||||||
|
this.creating = true;
|
||||||
|
try {
|
||||||
|
const { data } = await axios.post(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/createGutschrift`, { original_invoice_id: this.invoiceId, positions });
|
||||||
|
if (data.success) { window.notify('success', 'Gutschrift erstellt'); this.$emit('created', data.credit_invoice_id); }
|
||||||
|
else window.notify('error', data.message || 'Fehler');
|
||||||
|
} catch (e) { window.notify('error', e.message || 'Fehler'); } finally { this.creating = false; }
|
||||||
|
},
|
||||||
|
close() { this.$emit('close'); },
|
||||||
|
formatPrice(v) { return new Intl.NumberFormat('de-DE', { style: 'currency', currency: 'EUR' }).format(v || 0); }
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user