#!/usr/bin/php 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";