added stuff to manualinvoice
This commit is contained in:
@@ -208,8 +208,6 @@ class ManualInvoiceController extends TTCrud
|
||||
$post = json_decode(file_get_contents('php://input'), true);
|
||||
$id = $post['id'] ?? null;
|
||||
$recipientEmail = $post['email'] ?? null;
|
||||
$subject = $post['subject'] ?? 'Ihre Rechnung von XINON GmbH';
|
||||
$bodyText = $post['body'] ?? 'Sehr geehrte Damen und Herren,\n\nanbei erhalten Sie Ihre Rechnung.\n\nMit freundlichen Grüßen\nIhr Xinon Team';
|
||||
|
||||
if (!$id || !$recipientEmail) {
|
||||
self::returnJson(['success' => false, 'message' => 'ID oder E-Mail-Adresse fehlt']);
|
||||
@@ -222,6 +220,19 @@ class ManualInvoiceController extends TTCrud
|
||||
return;
|
||||
}
|
||||
|
||||
// Format invoice date for display
|
||||
$invoiceDateFormatted = date('d.m.Y', $invoice->invoice_date);
|
||||
|
||||
// Set default subject and body with invoice number and date
|
||||
$defaultSubject = "Ihre Rechnung {$invoice->invoice_number} vom {$invoiceDateFormatted}";
|
||||
$defaultBody = "Sehr geehrte Damen und Herren,\n\nanbei erhalten Sie Ihre Rechnung Nr. {$invoice->invoice_number} vom {$invoiceDateFormatted}.\n\nMit freundlichen Grüßen\nIhr XINON Team";
|
||||
|
||||
$subject = $post['subject'] ?? $defaultSubject;
|
||||
$bodyText = $post['body'] ?? $defaultBody;
|
||||
|
||||
// Convert literal \n strings to actual newlines (in case frontend sends escaped strings)
|
||||
$bodyText = str_replace('\n', "\n", $bodyText);
|
||||
|
||||
// Generate PDF
|
||||
$pdf_filename = $this->createPDFAction(true);
|
||||
if (!$pdf_filename || !file_exists($pdf_filename)) {
|
||||
@@ -232,19 +243,33 @@ class ManualInvoiceController extends TTCrud
|
||||
$pdfContent = file_get_contents($pdf_filename);
|
||||
|
||||
// --- HTML Email Generation ---
|
||||
$logoToolPath = BASEDIR . '/public/assets/images/the-tool-logo.png';
|
||||
$logoXinonPath = BASEDIR . '/public/assets/images/xinon-full.png';
|
||||
$logoToolExists = file_exists($logoToolPath);
|
||||
$logoXinonExists = file_exists($logoXinonPath);
|
||||
|
||||
// Construct HTML Body
|
||||
$html = '<!DOCTYPE html><html lang="de"><head><meta charset="UTF-8"><title>Rechnung</title><style>body { font-family: Arial, sans-serif; color: #333; }</style></head><body style="margin:0;padding:20px;background-color:#f3f4f6;">';
|
||||
$html .= '<div style="background-color:#fff;padding:20px;border-radius:8px;max-width:600px;margin:0 auto;box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.1);">';
|
||||
// Construct HTML Body with Outlook compatibility
|
||||
$html = '<!DOCTYPE html>';
|
||||
$html .= '<html lang="de" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office">';
|
||||
$html .= '<head>';
|
||||
$html .= '<meta charset="UTF-8">';
|
||||
$html .= '<meta http-equiv="X-UA-Compatible" content="IE=edge">';
|
||||
$html .= '<meta name="viewport" content="width=device-width, initial-scale=1.0">';
|
||||
$html .= '<title>Rechnung</title>';
|
||||
$html .= '<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings></xml><![endif]-->';
|
||||
$html .= '<style>body { font-family: Arial, sans-serif; color: #333; margin: 0; padding: 0; }</style>';
|
||||
$html .= '</head>';
|
||||
$html .= '<body style="margin:0;padding:20px;background-color:#f3f4f6;">';
|
||||
|
||||
// Logos
|
||||
$html .= '<div style="text-align:center;margin-bottom:20px;border-bottom: 1px solid #e5e7eb;padding-bottom: 15px;">';
|
||||
if ($logoToolExists) $html .= '<img src="cid:logo_thetool" alt="The Tool" style="height:40px;margin-right:15px;vertical-align:middle;">';
|
||||
if ($logoXinonExists) $html .= '<img src="cid:logo_xinon" alt="Xinon" style="height:40px;vertical-align:middle;">';
|
||||
// Outlook-safe container table
|
||||
$html .= '<!--[if mso]><table role="presentation" width="600" cellspacing="0" cellpadding="0" border="0" align="center"><tr><td><![endif]-->';
|
||||
$html .= '<div style="background-color:#fff;padding:20px;border-radius:8px;max-width:600px;margin:0 auto;">';
|
||||
|
||||
// Logo with Outlook-safe sizing
|
||||
$html .= '<div style="text-align:center;margin-bottom:20px;border-bottom:1px solid #e5e7eb;padding-bottom:15px;">';
|
||||
if ($logoXinonExists) {
|
||||
$html .= '<!--[if mso]><table role="presentation" width="100%" cellspacing="0" cellpadding="0" border="0"><tr><td align="center"><![endif]-->';
|
||||
$html .= '<img src="cid:logo_xinon" alt="XINON GmbH" width="150" height="50" style="display:block;width:150px;height:50px;max-width:150px;margin:0 auto;">';
|
||||
$html .= '<!--[if mso]></td></tr></table><![endif]-->';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<h2 style="color:#00558c;text-align:center;font-size:20px;margin-bottom:20px;">' . htmlspecialchars($subject) . '</h2>';
|
||||
@@ -254,7 +279,9 @@ class ManualInvoiceController extends TTCrud
|
||||
|
||||
$html .= '<br><div style="border-top:1px solid #eee;padding-top:20px;font-size:12px;color:#999;text-align:center;">';
|
||||
$html .= 'XINON GmbH | <a href="https://www.xinon.at" style="color:#00558c;text-decoration:none;">www.xinon.at</a>';
|
||||
$html .= '</div></div></body></html>';
|
||||
$html .= '</div></div>';
|
||||
$html .= '<!--[if mso]></td></tr></table><![endif]-->';
|
||||
$html .= '</body></html>';
|
||||
|
||||
$mail = new PHPMailer(true);
|
||||
try {
|
||||
@@ -269,12 +296,11 @@ class ManualInvoiceController extends TTCrud
|
||||
$mail->SMTPSecure = PHPMailer::ENCRYPTION_STARTTLS;
|
||||
$mail->Port = 587;
|
||||
|
||||
// Logos
|
||||
if ($logoToolExists) $mail->addEmbeddedImage($logoToolPath, 'logo_thetool');
|
||||
// Logo embedding
|
||||
if ($logoXinonExists) $mail->addEmbeddedImage($logoXinonPath, 'logo_xinon');
|
||||
|
||||
$mail->addReplyTo('backoffice@xinon.at', 'XINON Backoffice');
|
||||
$mail->setFrom('thetool@xinon.at', 'XINON TheTool');
|
||||
$mail->setFrom('thetool@xinon.at', 'XINON GmbH - Rechnungswesen');
|
||||
|
||||
$customerName = trim(($invoice->company ?: '') . ' ' . $invoice->firstname . ' ' . $invoice->lastname);
|
||||
$mail->addAddress($recipientEmail, $customerName);
|
||||
@@ -283,7 +309,10 @@ class ManualInvoiceController extends TTCrud
|
||||
$mail->Body = $html;
|
||||
$mail->AltBody = strip_tags($bodyText);
|
||||
|
||||
$mail->addStringAttachment($pdfContent, $invoice->invoice_number . '_Rechnung.pdf', 'base64', 'application/pdf');
|
||||
// Attachment filename: YYYY-MM-DD_InvoiceNumber_Rechnung.pdf
|
||||
$invoiceDateFile = date('Y-m-d', $invoice->invoice_date);
|
||||
$attachmentFilename = "{$invoiceDateFile}_{$invoice->invoice_number}_Rechnung.pdf";
|
||||
$mail->addStringAttachment($pdfContent, $attachmentFilename, 'base64', 'application/pdf');
|
||||
|
||||
$mail->send();
|
||||
|
||||
@@ -349,20 +378,21 @@ class ManualInvoiceController extends TTCrud
|
||||
$data['invoice_date'] = strtotime($data['invoice_date']);
|
||||
}
|
||||
|
||||
$data = array_merge([
|
||||
'invoice_number' => ManualInvoiceModel::getNextInvoiceNumber(),
|
||||
'invoice_date' => $data['invoice_date'] ?? time(),
|
||||
'status' => 'erstellt',
|
||||
'fibu_payment_skonto' => 0,
|
||||
'fibu_payment_skonto_rate' => 0,
|
||||
'gesamtrabatt' => 0,
|
||||
'total' => 0,
|
||||
'total_gross' => 0,
|
||||
'create_by' => $me->id,
|
||||
'edit_by' => $me->id,
|
||||
'create' => time(),
|
||||
'edit' => time()
|
||||
], $data);
|
||||
// Always generate invoice number (override any null from frontend)
|
||||
$data['invoice_number'] = ManualInvoiceModel::getNextInvoiceNumber();
|
||||
$data['invoice_date'] = $data['invoice_date'] ?? time();
|
||||
$data['status'] = 'erstellt';
|
||||
$data['fibu_payment_skonto'] = $data['fibu_payment_skonto'] ?? 0;
|
||||
$data['fibu_payment_skonto_rate'] = $data['fibu_payment_skonto_rate'] ?? 0;
|
||||
$data['gesamtrabatt'] = $data['gesamtrabatt'] ?? 0;
|
||||
$data['total'] = $data['total'] ?? 0;
|
||||
$data['total_gross'] = $data['total_gross'] ?? 0;
|
||||
$data['lock'] = 0;
|
||||
$data['exported'] = 0;
|
||||
$data['create_by'] = $me->id;
|
||||
$data['edit_by'] = $me->id;
|
||||
$data['create'] = time();
|
||||
$data['edit'] = time();
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -389,10 +419,16 @@ class ManualInvoiceController extends TTCrud
|
||||
unset($data['positions']);
|
||||
}
|
||||
|
||||
if (isset($data['id']) && ($invoice = ManualInvoiceModel::get($data['id'])) && $invoice->status === 'exportiert') {
|
||||
if (isset($data['id']) && ($invoice = ManualInvoiceModel::get($data['id']))) {
|
||||
if ($invoice->lock == 1) {
|
||||
$this->infoMessages['update'] = 'Rechnung ist gesperrt und kann nicht bearbeitet werden';
|
||||
return false;
|
||||
}
|
||||
if ($invoice->status === 'exportiert') {
|
||||
$this->infoMessages['update'] = 'Rechnung wurde bereits exportiert und kann nicht mehr bearbeitet werden';
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert invoice_date from string to timestamp if needed
|
||||
if (isset($data['invoice_date']) && is_string($data['invoice_date'])) {
|
||||
@@ -626,6 +662,12 @@ class ManualInvoiceController extends TTCrud
|
||||
|
||||
if (!$originalInvoiceId || empty($positions) || !($originalInvoice = ManualInvoiceModel::get($originalInvoiceId))) {
|
||||
self::returnJson(['success' => false, 'message' => 'Ungültige Anfrage']);
|
||||
return;
|
||||
}
|
||||
|
||||
if ($originalInvoice->lock == 1) {
|
||||
self::returnJson(['success' => false, 'message' => 'Originalrechnung ist gesperrt und kann nicht gutgeschrieben werden']);
|
||||
return;
|
||||
}
|
||||
|
||||
$me = new User();
|
||||
@@ -673,6 +715,8 @@ class ManualInvoiceController extends TTCrud
|
||||
'vatgroup_id' => $originalInvoice->vatgroup_id,
|
||||
'credit_for_invoice_id' => $originalInvoiceId,
|
||||
'status' => 'erstellt',
|
||||
'lock' => 0,
|
||||
'exported' => 0,
|
||||
'create' => time(),
|
||||
'edit' => time(),
|
||||
'create_by' => $me->id,
|
||||
@@ -681,6 +725,7 @@ class ManualInvoiceController extends TTCrud
|
||||
|
||||
if (!($creditInvoiceId = ManualInvoiceModel::create($invoiceData))) {
|
||||
self::returnJson(['success' => false, 'message' => 'Fehler beim Erstellen der Gutschrift']);
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($positions as $pos) {
|
||||
@@ -718,7 +763,11 @@ class ManualInvoiceController extends TTCrud
|
||||
protected function beforeDelete(): bool {
|
||||
if ($id = $this->request->id) {
|
||||
$invoice = ManualInvoiceModel::get($id);
|
||||
if ($invoice && $invoice->status === 'exported') {
|
||||
if ($invoice && $invoice->lock == 1) {
|
||||
$this->infoMessages['delete'] = 'Rechnung ist gesperrt und kann nicht gelöscht werden';
|
||||
return false;
|
||||
}
|
||||
if ($invoice && ($invoice->status === 'exported' || $invoice->status === 'exportiert')) {
|
||||
$this->infoMessages['delete'] = 'Rechnung wurde bereits exportiert und kann nicht gelöscht werden';
|
||||
return false;
|
||||
}
|
||||
@@ -732,4 +781,49 @@ class ManualInvoiceController extends TTCrud
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected function getArticleVatInfoAction() {
|
||||
$articleId = $_GET['article_id'] ?? null;
|
||||
$vatarea = $_GET['vatarea'] ?? 'domestic';
|
||||
|
||||
if (!$articleId) {
|
||||
self::returnJson(['success' => false, 'message' => 'Article ID required']);
|
||||
return;
|
||||
}
|
||||
|
||||
$article = WarehouseArticleModel::get($articleId);
|
||||
if (!$article) {
|
||||
self::returnJson(['success' => false, 'message' => 'Article not found']);
|
||||
return;
|
||||
}
|
||||
|
||||
// Map revenueAccount to vatgroup_id
|
||||
// revenueAccount 0 = Dienstleistungen = vatgroup_id 2
|
||||
// revenueAccount 1 = Handelswaren = vatgroup_id 3
|
||||
$vatgroupId = $article->revenueAccount == 0 ? 2 : 3;
|
||||
|
||||
// Get vatrate for this vatgroup and area
|
||||
$vatrate = VatrateModel::getFirst(['vatgroup_id' => $vatgroupId, 'area' => $vatarea]);
|
||||
|
||||
if (!$vatrate) {
|
||||
self::returnJson(['success' => false, 'message' => 'Vatrate not found for vatgroup ' . $vatgroupId . ' and area ' . $vatarea]);
|
||||
return;
|
||||
}
|
||||
|
||||
self::returnJson([
|
||||
'success' => true,
|
||||
'article' => [
|
||||
'id' => $article->id,
|
||||
'title' => $article->title,
|
||||
'articleNumber' => $article->articleNumber,
|
||||
'description' => $article->description,
|
||||
'revenueAccount' => $article->revenueAccount
|
||||
],
|
||||
'vatgroup_id' => $vatgroupId,
|
||||
'fibu_cost_account' => $vatrate->account,
|
||||
'fibu_cost_account_legacy' => $vatrate->legacy_account,
|
||||
'fibu_taxcode' => $vatrate->taxcode,
|
||||
'vatrate' => $vatrate->rate
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -44,6 +44,8 @@ class ManualInvoiceModel extends TTCrudBaseModel {
|
||||
public ?int $bmd_export_date;
|
||||
public ?int $date_delivered;
|
||||
public string $status;
|
||||
public int $lock = 0;
|
||||
public int $exported = 0;
|
||||
public ?int $credit_for_invoice_id;
|
||||
public int $create_by;
|
||||
public int $edit_by;
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
use Phinx\Migration\AbstractMigration;
|
||||
|
||||
final class AddLockExportedToManualinvoice extends AbstractMigration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
if($this->getEnvironment() == "thetool") {
|
||||
$table = $this->table("ManualInvoice");
|
||||
|
||||
$table->addColumn("lock", "integer", [
|
||||
"null" => false,
|
||||
"default" => 0,
|
||||
"limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY,
|
||||
"after" => "status"
|
||||
]);
|
||||
|
||||
$table->addColumn("exported", "integer", [
|
||||
"null" => false,
|
||||
"default" => 0,
|
||||
"limit" => \Phinx\Db\Adapter\MysqlAdapter::INT_TINY,
|
||||
"after" => "lock"
|
||||
]);
|
||||
|
||||
$table->save();
|
||||
}
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
if($this->getEnvironment() == "thetool") {
|
||||
$table = $this->table("ManualInvoice");
|
||||
$table->removeColumn("lock")->save();
|
||||
$table->removeColumn("exported")->save();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -170,7 +170,7 @@ Vue.component('manual-invoice-modal', {
|
||||
<tt-textarea label="Einleitender Text" v-model="invoiceData.einleitender_text" rows="3" sm row/>
|
||||
</tt-card>
|
||||
<tt-card><template v-slot:header><h5><i class="fas fa-list-ol mr-2"></i>Positionen</h5></template>
|
||||
<tt-positions-manager group-mode ref="positionsManager" v-model="invoiceData.positions" :config="positionsConfig" />
|
||||
<tt-positions-manager group-mode ref="positionsManager" v-model="invoiceData.positions" :config="positionsConfig" @updateField-article_id="onArticleSelected" />
|
||||
</tt-card>
|
||||
<tt-card><template v-slot:header><h5><i class="fas fa-paragraph mr-2"></i>Texte & Rabatt</h5></template>
|
||||
<tt-input label="Gesamtrabatt (%)" v-model.number="invoiceData.gesamtrabatt" sm row type="number" placeholder="0"/>
|
||||
@@ -208,6 +208,12 @@ Vue.component('manual-invoice-modal', {
|
||||
billingTypeOptions: [{value: 'invoice', text: 'Rechnung'}, {value: 'sepa', text: 'SEPA'}],
|
||||
positionsConfig: {
|
||||
fields: {
|
||||
article_id: {
|
||||
type: 'autocomplete',
|
||||
label: 'Artikel (optional)',
|
||||
apiUrl: '/WarehouseArticle/autocomplete',
|
||||
customFieldReference: 'WarehouseArticle'
|
||||
},
|
||||
product_name: { type: 'input', label: 'Bezeichnung' },
|
||||
product_info: { type: 'input', label: 'Zusatzinfo' },
|
||||
amount: { type: 'input', label: 'Menge', inputType: 'number' },
|
||||
@@ -330,6 +336,28 @@ Vue.component('manual-invoice-modal', {
|
||||
if (e.ctrlKey && e.key === 'q') { e.preventDefault(); this.togglePreviewVisibility(); }
|
||||
},
|
||||
togglePreviewVisibility() { if (!this.isLargeScreen) this.showPreviewOnSmallScreen = !this.showPreviewOnSmallScreen; },
|
||||
async onArticleSelected(articleId) {
|
||||
if (!articleId) return;
|
||||
try {
|
||||
const { data } = await axios.get(`${window.TT_CONFIG.BASE_PATH}/ManualInvoice/getArticleVatInfo?article_id=${articleId}`);
|
||||
if (data.success && this.$refs.positionsManager) {
|
||||
// Update the formData in the positions manager
|
||||
const pm = this.$refs.positionsManager;
|
||||
if (data.article) {
|
||||
pm.$set(pm.formData, 'product_name', data.article.title);
|
||||
pm.$set(pm.formData, 'product_info', data.article.description || '');
|
||||
}
|
||||
pm.$set(pm.formData, 'vatrate', parseFloat(data.vatrate) || 20);
|
||||
pm.$set(pm.formData, 'fibu_cost_account', data.fibu_cost_account);
|
||||
pm.$set(pm.formData, 'fibu_cost_account_legacy', data.fibu_cost_account_legacy);
|
||||
pm.$set(pm.formData, 'fibu_taxcode', data.fibu_taxcode);
|
||||
// Store vatgroup_id on invoice level if needed
|
||||
this.invoiceData.vatgroup_id = data.vatgroup_id;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Error fetching article VAT info:', e);
|
||||
}
|
||||
},
|
||||
debouncedPreviewUpdate() {
|
||||
clearTimeout(this.previewDebounceTimer);
|
||||
this.previewDebounceTimer = setTimeout(() => this.updatePdfPreview(), 2000);
|
||||
|
||||
314
scripts/ManualInvoice/create-mock-invoices.php
Normal file
314
scripts/ManualInvoice/create-mock-invoices.php
Normal file
@@ -0,0 +1,314 @@
|
||||
#!/usr/bin/php
|
||||
<?php
|
||||
require("../../config/config.php");
|
||||
|
||||
define('FRONKDB_SQLDEBUG', false);
|
||||
error_reporting(E_ALL & ~(E_NOTICE | E_STRICT | E_DEPRECATED));
|
||||
|
||||
require_once(LIBDIR."/mvcfronk/mfRouter/mfRouter.php");
|
||||
require_once(LIBDIR."/mvcfronk/mfBase/mfBaseModel.php");
|
||||
require_once(LIBDIR."/mvcfronk/mfBase/mfBaseController.php");
|
||||
|
||||
$layout = \Layout::singleton();
|
||||
|
||||
$me = new User(1);
|
||||
define("INTERNAL_USER_ID", $me->id);
|
||||
define("INTERNAL_USER_USERNAME", $me->username);
|
||||
|
||||
echo "========================================\n";
|
||||
echo "Creating 20 mock manual invoices...\n";
|
||||
echo "========================================\n\n";
|
||||
|
||||
$db = FronkDB::singleton();
|
||||
|
||||
// Get random customers with valid data for invoicing
|
||||
$customerSql = "SELECT * FROM Address
|
||||
WHERE customer_number IS NOT NULL
|
||||
AND customer_number > 0
|
||||
AND (company IS NOT NULL OR firstname IS NOT NULL)
|
||||
AND street IS NOT NULL
|
||||
AND zip IS NOT NULL
|
||||
AND city IS NOT NULL
|
||||
ORDER BY RAND()
|
||||
LIMIT 50";
|
||||
$customerRes = $db->query($customerSql);
|
||||
$customers = [];
|
||||
while ($row = $db->fetch_object($customerRes)) {
|
||||
$customers[] = $row;
|
||||
}
|
||||
|
||||
if (empty($customers)) {
|
||||
echo "ERROR: No valid customers found in database!\n";
|
||||
exit(1);
|
||||
}
|
||||
|
||||
echo "Found " . count($customers) . " random customers to use.\n\n";
|
||||
|
||||
// Get last 20 shipping notes for position data
|
||||
$sql = "SELECT * FROM WarehouseShippingNote ORDER BY `create` DESC LIMIT 20";
|
||||
$res = $db->query($sql);
|
||||
|
||||
$shippingNotes = [];
|
||||
while ($row = $db->fetch_object($res)) {
|
||||
$shippingNotes[] = new WarehouseShippingNote($row);
|
||||
}
|
||||
|
||||
$count = 0;
|
||||
$errors = 0;
|
||||
|
||||
foreach ($shippingNotes as $index => $shippingNote) {
|
||||
echo "Processing shipping note #{$shippingNote->id}...\n";
|
||||
|
||||
// Pick a random customer
|
||||
$customer = $customers[array_rand($customers)];
|
||||
$address = new Address($customer->id);
|
||||
|
||||
if (!$address || !$address->id) {
|
||||
echo " - Skipping: Could not load customer address\n";
|
||||
$errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Build positions from shipping note
|
||||
$positions = json_decode($shippingNote->positions, true) ?: [];
|
||||
$enrichedPositions = [];
|
||||
|
||||
foreach ($positions as $position) {
|
||||
if (isset($position['article'])) {
|
||||
$article = WarehouseArticleModel::get($position['article']);
|
||||
if (!$article) continue;
|
||||
|
||||
$prices = json_decode($article->cheapestSellPrice, true) ?: [];
|
||||
$price = 0;
|
||||
foreach ($prices as $p) {
|
||||
if (isset($p['price'])) {
|
||||
$price = $p['price'];
|
||||
break;
|
||||
}
|
||||
}
|
||||
// Use random price if no price found
|
||||
if ($price == 0) {
|
||||
$price = rand(10, 500);
|
||||
}
|
||||
|
||||
$enrichedPositions[] = [
|
||||
'product_name' => $article->articleNumber . " | " . $article->title,
|
||||
'product_info' => $article->description ?: '',
|
||||
'amount' => $position['amount'] ?: 1,
|
||||
'unit' => $article->unit ?: 'Stk.',
|
||||
'price' => $price,
|
||||
'discount' => 0,
|
||||
'vatrate' => 20,
|
||||
'article_id' => $article->id
|
||||
];
|
||||
} elseif (isset($position['articlePacket'])) {
|
||||
$packet = WarehouseArticlePacketModel::get($position['articlePacket']);
|
||||
if (!$packet) continue;
|
||||
|
||||
$enrichedPositions[] = [
|
||||
'product_name' => $packet->title,
|
||||
'product_info' => $packet->description ?? '',
|
||||
'amount' => $position['amount'] ?: 1,
|
||||
'unit' => 'Pau.',
|
||||
'price' => rand(50, 300),
|
||||
'discount' => 0,
|
||||
'vatrate' => 20
|
||||
];
|
||||
} elseif (isset($position['articleText'])) {
|
||||
$enrichedPositions[] = [
|
||||
'product_name' => $position['articleText'],
|
||||
'product_info' => '',
|
||||
'amount' => $position['amount'] ?? 1,
|
||||
'unit' => 'Stk.',
|
||||
'price' => rand(10, 100),
|
||||
'discount' => 0,
|
||||
'vatrate' => 20
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Add hours entries
|
||||
$hoursEntries = json_decode($shippingNote->hoursEntries, true) ?: [];
|
||||
foreach ($hoursEntries as $hoursEntry) {
|
||||
$hourCount = floatval(str_replace(",", ".", $hoursEntry['hourCount'] ?? 0));
|
||||
if ($hourCount <= 0) continue;
|
||||
|
||||
$userName = 'Mitarbeiter';
|
||||
if (!empty($hoursEntry['userId']) && is_numeric($hoursEntry['userId'])) {
|
||||
$user = UserModel::getOne($hoursEntry['userId']);
|
||||
$userName = $user ? $user->name : 'Mitarbeiter';
|
||||
} elseif (!empty($hoursEntry['userId_text'])) {
|
||||
$userName = $hoursEntry['userId_text'];
|
||||
}
|
||||
|
||||
$enrichedPositions[] = [
|
||||
'product_name' => 'Arbeitsstunden - ' . $userName,
|
||||
'product_info' => 'Datum: ' . (isset($hoursEntry['date']) ? date('d.m.Y', strtotime($hoursEntry['date'])) : date('d.m.Y')),
|
||||
'amount' => $hourCount,
|
||||
'unit' => 'h',
|
||||
'price' => 60,
|
||||
'discount' => 0,
|
||||
'vatrate' => 20
|
||||
];
|
||||
}
|
||||
|
||||
// If still no positions, create some mock positions
|
||||
if (empty($enrichedPositions)) {
|
||||
$mockPositions = [
|
||||
['name' => 'Beratungsleistung', 'unit' => 'h', 'price' => 85],
|
||||
['name' => 'Installationsarbeiten', 'unit' => 'Pau.', 'price' => 250],
|
||||
['name' => 'Netzwerkkabel Cat6', 'unit' => 'm', 'price' => 3.50],
|
||||
['name' => 'Router TP-Link', 'unit' => 'Stk.', 'price' => 89.90],
|
||||
['name' => 'Montage vor Ort', 'unit' => 'h', 'price' => 65],
|
||||
];
|
||||
|
||||
// Add 1-3 random mock positions
|
||||
$numPositions = rand(1, 3);
|
||||
for ($i = 0; $i < $numPositions; $i++) {
|
||||
$mock = $mockPositions[array_rand($mockPositions)];
|
||||
$enrichedPositions[] = [
|
||||
'product_name' => $mock['name'],
|
||||
'product_info' => 'Mock-Position für Testzwecke',
|
||||
'amount' => rand(1, 10),
|
||||
'unit' => $mock['unit'],
|
||||
'price' => $mock['price'],
|
||||
'discount' => rand(0, 1) ? rand(5, 15) : 0,
|
||||
'vatrate' => 20
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Use random invoice date within last 90 days
|
||||
$randomDaysAgo = rand(0, 90);
|
||||
$invoiceDate = strtotime("-{$randomDaysAgo} days");
|
||||
|
||||
// Create invoice data
|
||||
$invoiceData = [
|
||||
'invoice_number' => ManualInvoiceModel::getNextInvoiceNumber(),
|
||||
'invoice_date' => $invoiceDate,
|
||||
'owner_id' => $address->id,
|
||||
'billingaddress_id' => $address->id,
|
||||
'customer_number' => $address->customer_number,
|
||||
'company' => $address->company,
|
||||
'firstname' => $address->firstname,
|
||||
'lastname' => $address->lastname,
|
||||
'street' => $address->street,
|
||||
'zip' => $address->zip,
|
||||
'city' => $address->city,
|
||||
'country' => $address->country ?: 'Österreich',
|
||||
'email' => $address->email,
|
||||
'uid' => $address->uid,
|
||||
'fibu_account_number' => $address->fibu_account_number,
|
||||
'fibu_payment_due' => $address->fibu_payment_due ?: 14,
|
||||
'fibu_payment_skonto' => $address->fibu_payment_skonto ?: 0,
|
||||
'fibu_payment_skonto_rate' => $address->fibu_payment_skonto_rate ?: 0,
|
||||
'billing_type' => $address->billing_type ?: 'invoice',
|
||||
'billing_delivery' => $address->billing_delivery ?: 'email',
|
||||
'bank_account_bank' => $address->bank_account_bank,
|
||||
'bank_account_owner' => $address->bank_account_owner,
|
||||
'bank_account_iban' => $address->bank_account_iban,
|
||||
'bank_account_bic' => $address->bank_account_bic,
|
||||
'sepa_date' => $address->sepa_date ? (is_numeric($address->sepa_date) ? date('Y-m-d', $address->sepa_date) : $address->sepa_date) : null,
|
||||
'leistungszeitraum' => date('m/Y', $invoiceDate),
|
||||
'einleitender_text' => 'Testrechnung basierend auf Lieferschein #' . $shippingNote->id,
|
||||
'externe_referenz' => 'TEST-LS-' . $shippingNote->id,
|
||||
'gesamtrabatt' => rand(0, 1) ? rand(0, 10) : 0,
|
||||
'total' => 0,
|
||||
'total_gross' => 0,
|
||||
'vatgroup_id' => rand(1, 3),
|
||||
'status' => 'erstellt',
|
||||
'lock' => 0,
|
||||
'exported' => 0,
|
||||
'create_by' => 1,
|
||||
'edit_by' => 1,
|
||||
'create' => time(),
|
||||
'edit' => time()
|
||||
];
|
||||
|
||||
// Create the invoice
|
||||
$invoiceId = ManualInvoiceModel::create($invoiceData);
|
||||
|
||||
if (!$invoiceId) {
|
||||
echo " - Error creating invoice\n";
|
||||
$errors++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Create positions
|
||||
$total = 0;
|
||||
$totalGross = 0;
|
||||
$gesamtrabatt = floatval($invoiceData['gesamtrabatt']);
|
||||
|
||||
foreach ($enrichedPositions as $pos) {
|
||||
$amount = floatval($pos['amount']);
|
||||
$price = floatval($pos['price']);
|
||||
$discount = floatval($pos['discount'] ?? 0);
|
||||
$vatrate = floatval($pos['vatrate'] ?? 20);
|
||||
|
||||
// Validate amount is within reasonable bounds
|
||||
if ($amount <= 0 || $amount > 999999) {
|
||||
$amount = 1;
|
||||
}
|
||||
if ($price < 0 || $price > 999999) {
|
||||
$price = 0;
|
||||
}
|
||||
|
||||
$priceTotal = $amount * $price * (1 - $discount / 100);
|
||||
$priceTotalAfterGesamtrabatt = $priceTotal * (1 - $gesamtrabatt / 100);
|
||||
$priceGross = $priceTotalAfterGesamtrabatt * (1 + $vatrate / 100);
|
||||
|
||||
// Use direct SQL to bypass model validation for mock data
|
||||
$posProduct = $db->escape($pos['product_name']);
|
||||
$posInfo = $db->escape($pos['product_info'] ?? '');
|
||||
$posProductId = intval($pos['article_id'] ?? 0);
|
||||
$posUnit = $db->escape($pos['unit'] ?? 'Stk.');
|
||||
$posTime = time();
|
||||
|
||||
// Ensure values are numeric and within DB limits
|
||||
$amount = round($amount, 2);
|
||||
$price = round($price, 2);
|
||||
$priceTotal = round($priceTotal, 2);
|
||||
$priceGross = round($priceGross, 2);
|
||||
|
||||
$insertSql = "INSERT INTO ManualInvoiceposition
|
||||
(manualinvoice_id, position_group, product_id, product_name, product_info, amount, unit, price, discount, vatrate, price_total, price_gross, matchcode, fibu_cost_account, fibu_cost_account_legacy, fibu_taxcode, contract_id, billing_id, create_by, edit_by, `create`, edit)
|
||||
VALUES
|
||||
($invoiceId, NULL, $posProductId, '$posProduct', '$posInfo', $amount, '$posUnit', $price, $discount, $vatrate, $priceTotal, $priceGross, NULL, NULL, NULL, NULL, 0, NULL, 1, 1, $posTime, $posTime)";
|
||||
|
||||
try {
|
||||
$db->query($insertSql);
|
||||
} catch (Throwable $e) {
|
||||
echo " Warning: Position skipped (amount=$amount, price=$price): " . $e->getMessage() . "\n";
|
||||
continue;
|
||||
}
|
||||
|
||||
$total += $priceTotal;
|
||||
$totalGross += $priceGross;
|
||||
}
|
||||
|
||||
// Apply gesamtrabatt to total
|
||||
$totalAfterRabatt = $total * (1 - $gesamtrabatt / 100);
|
||||
|
||||
// Update invoice totals using direct SQL (bypass model validation)
|
||||
$db->query("UPDATE ManualInvoice SET total = " . floatval($totalAfterRabatt) . ", total_gross = " . floatval($totalGross) . " WHERE id = " . intval($invoiceId));
|
||||
|
||||
// Create journal entry using direct SQL
|
||||
$journalText = $db->escape('Mock-Rechnung erstellt (basierend auf LS #' . $shippingNote->id . ')');
|
||||
$journalTime = time();
|
||||
$db->query("INSERT INTO ManualInvoiceJournal (manualinvoiceId, text, statusChange, createBy, `create`)
|
||||
VALUES ($invoiceId, '$journalText', 'erstellt', 1, $journalTime)");
|
||||
|
||||
$invoiceNumber = $invoiceData['invoice_number'];
|
||||
$customerName = trim(($address->company ?: '') . ' ' . $address->firstname . ' ' . $address->lastname);
|
||||
echo " - Created invoice #{$invoiceId} ({$invoiceNumber})\n";
|
||||
echo " Customer: {$customerName}\n";
|
||||
echo " Positions: " . count($enrichedPositions) . ", Total: €" . number_format($totalAfterRabatt, 2) . "\n";
|
||||
$count++;
|
||||
}
|
||||
|
||||
echo "\n========================================\n";
|
||||
echo "Mock invoice creation complete!\n";
|
||||
echo "Created: {$count} invoices\n";
|
||||
echo "Errors/Skipped: {$errors}\n";
|
||||
echo "========================================\n";
|
||||
echo "\nNOTE: No emails were sent. These are test invoices only.\n";
|
||||
Reference in New Issue
Block a user