From 7273c830006d7a7fe8b8811023d48728e8caa78e Mon Sep 17 00:00:00 2001 From: Luca Haid Date: Tue, 27 Jan 2026 16:43:38 +0100 Subject: [PATCH] created test creation invoice script for mock data --- .../ManualInvoice/create-mock-invoices.php | 314 ------------ .../ManualInvoice/create-test-invoices.php | 464 ++++++++++++++++++ 2 files changed, 464 insertions(+), 314 deletions(-) delete mode 100644 scripts/ManualInvoice/create-mock-invoices.php create mode 100644 scripts/ManualInvoice/create-test-invoices.php diff --git a/scripts/ManualInvoice/create-mock-invoices.php b/scripts/ManualInvoice/create-mock-invoices.php deleted file mode 100644 index 57cc5835e..000000000 --- a/scripts/ManualInvoice/create-mock-invoices.php +++ /dev/null @@ -1,314 +0,0 @@ -#!/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"; diff --git a/scripts/ManualInvoice/create-test-invoices.php b/scripts/ManualInvoice/create-test-invoices.php new file mode 100644 index 000000000..f98a27558 --- /dev/null +++ b/scripts/ManualInvoice/create-test-invoices.php @@ -0,0 +1,464 @@ +#!/usr/bin/php +id); +define("INTERNAL_USER_USERNAME", $me->username); + +$db = FronkDB::singleton(); + +echo "========================================\n"; +echo "Manual Invoice Test Data Generator\n"; +echo "========================================\n\n"; + +echo "Do you want to truncate ManualInvoice tables first? (y/n): "; +$handle = fopen("php://stdin", "r"); +$answer = trim(fgets($handle)); +fclose($handle); + +if (strtolower($answer) === 'y') { + echo "Truncating tables...\n"; + $db->query("SET FOREIGN_KEY_CHECKS = 0"); + $db->query("TRUNCATE TABLE ManualInvoiceJournal"); + $db->query("TRUNCATE TABLE ManualInvoiceposition"); + $db->query("TRUNCATE TABLE ManualInvoice"); + $db->query("SET FOREIGN_KEY_CHECKS = 1"); + echo "Tables truncated.\n\n"; +} + +$vatgroups = []; +$res = $db->query("SELECT * FROM Vatgroup WHERE id IN (2, 3) ORDER BY id"); +while ($row = $db->fetch_object($res)) { + $vatgroups[$row->id] = $row; +} + +if (empty($vatgroups)) { + echo "ERROR: No VAT groups found!\n"; + exit(1); +} + +echo "Found " . count($vatgroups) . " VAT group(s)\n"; + +$vatrates = []; +$res = $db->query("SELECT * FROM Vatrate WHERE vatgroup_id IN (2, 3)"); +while ($row = $db->fetch_object($res)) { + $vatrates[$row->vatgroup_id][$row->area] = $row; +} + +$articlesByVatgroup = []; +$res = $db->query("SELECT * FROM WarehouseArticle WHERE vatgroup_id IN (2, 3) AND (isEndOfLife = 0 OR isEndOfLife IS NULL)"); +while ($row = $db->fetch_object($res)) { + if (!isset($articlesByVatgroup[$row->vatgroup_id])) { + $articlesByVatgroup[$row->vatgroup_id] = []; + } + $articlesByVatgroup[$row->vatgroup_id][] = $row; +} + +$totalArticles = array_sum(array_map('count', $articlesByVatgroup)); +if ($totalArticles == 0) { + echo "ERROR: No warehouse articles found!\n"; + exit(1); +} + +echo "Found $totalArticles warehouse articles\n"; + +function getCustomersByRegion($db, $region) { + $customers = []; + switch ($region) { + case 'domestic': + $sql = "SELECT a.* FROM Address a + LEFT JOIN Country c ON a.country_id = c.id + WHERE a.customer_number > 0 + AND (a.company IS NOT NULL OR a.firstname IS NOT NULL) + AND a.street IS NOT NULL AND a.zip IS NOT NULL AND a.city IS NOT NULL + AND (c.isocode = 'AT' OR a.uid LIKE 'ATU%' OR a.country_id IS NULL) + ORDER BY RAND() LIMIT 20"; + break; + case 'eu': + $sql = "SELECT a.* FROM Address a + JOIN Country c ON a.country_id = c.id + WHERE a.customer_number > 0 + AND (a.company IS NOT NULL OR a.firstname IS NOT NULL) + AND a.street IS NOT NULL AND a.zip IS NOT NULL AND a.city IS NOT NULL + AND c.is_eu = 1 AND c.isocode != 'AT' + AND (a.uid IS NULL OR a.uid NOT LIKE 'ATU%') + ORDER BY RAND() LIMIT 20"; + break; + case 'other': + $sql = "SELECT a.* FROM Address a + JOIN Country c ON a.country_id = c.id + WHERE a.customer_number > 0 + AND (a.company IS NOT NULL OR a.firstname IS NOT NULL) + AND a.street IS NOT NULL AND a.zip IS NOT NULL AND a.city IS NOT NULL + AND c.is_eu = 0 + ORDER BY RAND() LIMIT 20"; + break; + } + $res = $db->query($sql); + while ($row = $db->fetch_object($res)) { + $customers[] = $row; + } + return $customers; +} + +$domesticCustomers = getCustomersByRegion($db, 'domestic'); +$euCustomers = getCustomersByRegion($db, 'eu'); +$nonEuCustomers = getCustomersByRegion($db, 'other'); + +echo "Found customers: " . count($domesticCustomers) . " domestic, " . count($euCustomers) . " EU, " . count($nonEuCustomers) . " non-EU\n\n"; + +function getVatArea($address, $db) { + $uid = preg_replace('/[^a-z0-9]/i', '', $address->uid ?? ''); + if (strtolower(substr($uid, 0, 3)) === 'atu') return 'domestic'; + if ($address->country_id) { + $res = $db->query("SELECT * FROM Country WHERE id = " . intval($address->country_id)); + if ($country = $db->fetch_object($res)) { + if ($country->isocode === TT_HOMECOUNTRY_ISOCODE) return 'domestic'; + if ($country->is_eu) return 'eu'; + return 'other'; + } + } + return 'domestic'; +} + +function getArticlePrice($article) { + if ($article->cheapestSellPrice) { + $prices = json_decode($article->cheapestSellPrice, true); + if (is_array($prices)) { + foreach ($prices as $p) { + if (isset($p['price']) && $p['price'] > 0) return floatval($p['price']); + } + } + } + return rand(10, 500); +} + +function createInvoice($db, $data) { + $invoiceNumber = ManualInvoiceModel::getNextInvoiceNumber(); + $now = time(); + $sql = "INSERT INTO ManualInvoice ( + invoice_number, invoice_date, performance_period, introductory_text, external_reference, total_discount, + owner_id, billingaddress_id, customer_number, fibu_account_number, fibu_payment_due, fibu_payment_skonto, fibu_payment_skonto_rate, + sepa_date, fibu_cost_area, fibu_cost_account, fibu_cost_account_legacy, fibu_taxcode, tax_text, + company, firstname, lastname, street, zip, city, country, email, uid, + billing_type, bank_account_bank, bank_account_owner, bank_account_iban, bank_account_bic, + total, total_gross, vatgroup_id, status, `lock`, exported, create_by, edit_by, `create`, edit + ) VALUES ( + '{$db->escape($invoiceNumber)}', " . intval($data['invoice_date']) . ", + '{$db->escape($data['performance_period'])}', '{$db->escape($data['introductory_text'])}', + '{$db->escape($data['external_reference'])}', " . floatval($data['total_discount']) . ", + " . intval($data['owner_id']) . ", " . intval($data['billingaddress_id']) . ", " . intval($data['customer_number']) . ", + " . ($data['fibu_account_number'] ? intval($data['fibu_account_number']) : 'NULL') . ", + " . ($data['fibu_payment_due'] ? intval($data['fibu_payment_due']) : 'NULL') . ", + " . intval($data['fibu_payment_skonto']) . ", " . intval($data['fibu_payment_skonto_rate']) . ", + " . ($data['sepa_date'] ? "'{$db->escape($data['sepa_date'])}'" : 'NULL') . ", + " . ($data['fibu_cost_area'] ? "'{$db->escape($data['fibu_cost_area'])}'" : 'NULL') . ", + " . ($data['fibu_cost_account'] ? intval($data['fibu_cost_account']) : 'NULL') . ", + " . ($data['fibu_cost_account_legacy'] ? intval($data['fibu_cost_account_legacy']) : 'NULL') . ", + " . ($data['fibu_taxcode'] ? intval($data['fibu_taxcode']) : 'NULL') . ", + " . ($data['tax_text'] ? "'{$db->escape($data['tax_text'])}'" : 'NULL') . ", + '{$db->escape($data['company'])}', '{$db->escape($data['firstname'])}', '{$db->escape($data['lastname'])}', + '{$db->escape($data['street'])}', '{$db->escape($data['zip'])}', '{$db->escape($data['city'])}', + '{$db->escape($data['country'])}', '{$db->escape($data['email'])}', '{$db->escape($data['uid'])}', + '{$db->escape($data['billing_type'])}', + '{$db->escape($data['bank_account_bank'])}', '{$db->escape($data['bank_account_owner'])}', + '{$db->escape($data['bank_account_iban'])}', '{$db->escape($data['bank_account_bic'])}', + 0, 0, " . intval($data['vatgroup_id']) . ", 'erstellt', 0, 0, 1, 1, {$now}, {$now} + )"; + $db->query($sql); + return ['id' => $db->link->insert_id, 'invoice_number' => $invoiceNumber]; +} + +function createPosition($db, $invoiceId, $pos) { + $now = time(); + $fibuAccount = $pos['fibu_cost_account'] ? intval($pos['fibu_cost_account']) : 'NULL'; + $fibuAccountLegacy = $pos['fibu_cost_account_legacy'] ? intval($pos['fibu_cost_account_legacy']) : 'NULL'; + $fibuTaxcode = $pos['fibu_taxcode'] ? intval($pos['fibu_taxcode']) : 'NULL'; + $sql = "INSERT INTO ManualInvoiceposition ( + manualinvoice_id, warehousearticle_id, warehousearticle_name, product_info, + amount, unit, price, discount, vatrate, price_total, price_gross, + fibu_cost_account, fibu_cost_account_legacy, fibu_taxcode, create_by, edit_by, `create`, edit + ) VALUES ( + $invoiceId, " . intval($pos['warehousearticle_id']) . ", '{$db->escape($pos['warehousearticle_name'])}', + '{$db->escape($pos['product_info'])}', {$pos['amount']}, '{$db->escape($pos['unit'])}', + {$pos['price']}, {$pos['discount']}, {$pos['vatrate']}, + " . round($pos['price_total'], 2) . ", " . round($pos['price_gross'], 2) . ", + $fibuAccount, $fibuAccountLegacy, $fibuTaxcode, 1, 1, $now, $now + )"; + $db->query($sql); +} + +$regions = [ + 'domestic' => ['name' => 'Domestic (AT)', 'customers' => $domesticCustomers], + 'eu' => ['name' => 'EU', 'customers' => $euCustomers], + 'other' => ['name' => 'Non-EU', 'customers' => $nonEuCustomers] +]; + +$totalCreated = 0; +$errors = 0; + +echo "Generating invoices...\n"; +echo "----------------------------------------\n"; + +foreach ($vatgroups as $vatgroupId => $vatgroup) { + $vgArticles = $articlesByVatgroup[$vatgroupId] ?? []; + if (empty($vgArticles)) { + echo "[Vatgroup: {$vatgroup->name}] SKIPPED - No articles\n"; + continue; + } + + foreach ($regions as $regionKey => $regionData) { + $customers = $regionData['customers']; + if (empty($customers)) { + echo "[Vatgroup: {$vatgroup->name}] [Region: {$regionData['name']}] SKIPPED - No customers\n"; + continue; + } + + for ($i = 1; $i <= 4; $i++) { + $customer = $customers[array_rand($customers)]; + $address = new Address($customer->id); + if (!$address || !$address->id) { $errors++; continue; } + + $vatarea = getVatArea($address, $db); + $vatrate = $vatrates[$vatgroupId][$vatarea] ?? null; + $vatrateValue = $vatrate ? floatval($vatrate->rate) : 20.0; + $fibuCostAccount = $vatrate ? intval($vatrate->account) : null; + $fibuCostAccountLegacy = $vatrate ? ($vatrate->legacy_account ? intval($vatrate->legacy_account) : null) : null; + $fibuTaxcode = $vatrate ? ($vatrate->taxcode ? intval($vatrate->taxcode) : null) : null; + $taxText = $vatrate ? ($vatrate->invoice_text ?? '') : ''; + + $numPositions = rand(1, 5); + shuffle($vgArticles); + $selectedArticles = array_slice($vgArticles, 0, min($numPositions, count($vgArticles))); + $totalDiscount = rand(0, 1) ? rand(0, 10) : 0; + + $positions = []; + foreach ($selectedArticles as $article) { + $amount = rand(1, 10); + $price = getArticlePrice($article); + $discount = rand(0, 1) ? rand(0, 15) : 0; + $priceTotal = $amount * $price * (1 - $discount / 100); + $priceTotalAfterDiscount = $priceTotal * (1 - $totalDiscount / 100); + $priceGross = $priceTotalAfterDiscount * (1 + $vatrateValue / 100); + + $positions[] = [ + 'warehousearticle_id' => $article->id, + 'warehousearticle_name' => $article->articleNumber . ' | ' . $article->title, + 'product_info' => $article->description ?? '', + 'amount' => $amount, + 'unit' => $article->unit ?: 'Stk.', + 'price' => $price, + 'discount' => $discount, + 'vatrate' => $vatrateValue, + 'price_total' => $priceTotal, + 'price_gross' => $priceGross, + 'fibu_cost_account' => $fibuCostAccount, + 'fibu_cost_account_legacy' => $fibuCostAccountLegacy, + 'fibu_taxcode' => $fibuTaxcode + ]; + } + + $randomDaysAgo = rand(0, 60); + $invoiceDate = strtotime("-{$randomDaysAgo} days"); + + $invoiceData = [ + 'invoice_date' => $invoiceDate, + 'owner_id' => $address->id, + 'billingaddress_id' => $address->id, + 'customer_number' => $address->customer_number ?: 0, + 'company' => $address->company ?: '', + 'firstname' => $address->firstname ?: '', + 'lastname' => $address->lastname ?: '', + 'street' => $address->street ?: '', + 'zip' => $address->zip ?: '', + 'city' => $address->city ?: '', + 'country' => 'Österreich', + 'email' => $address->email ?: '', + 'uid' => $address->uid ?: '', + 'fibu_account_number' => $address->fibu_account_number ?: 0, + '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, + 'fibu_cost_area' => $vatarea, + 'fibu_cost_account' => $fibuCostAccount, + 'fibu_cost_account_legacy' => $fibuCostAccountLegacy, + 'fibu_taxcode' => $fibuTaxcode, + 'billing_type' => $address->billing_type ?: 'invoice', + '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, + 'performance_period' => date('m/Y', $invoiceDate), + 'introductory_text' => "Test invoice for '{$vatgroup->name}' in '{$regionData['name']}'", + 'external_reference' => "TEST-VG{$vatgroupId}-{$regionKey}-{$i}", + 'total_discount' => $totalDiscount, + 'vatgroup_id' => $vatgroupId, + 'tax_text' => $taxText + ]; + + $result = createInvoice($db, $invoiceData); + $invoiceId = $result['id']; + $invoiceNumber = $result['invoice_number']; + + if (!$invoiceId) { echo "[Vatgroup: {$vatgroup->name}] [Region: {$regionData['name']}] Invoice #{$i}: ERROR\n"; $errors++; continue; } + + $total = 0; + $totalGross = 0; + foreach ($positions as $pos) { + createPosition($db, $invoiceId, $pos); + $total += $pos['price_total']; + $totalGross += $pos['price_gross']; + } + + $totalAfterRabatt = $total * (1 - $totalDiscount / 100); + $db->query("UPDATE ManualInvoice SET total = " . round($totalAfterRabatt, 2) . ", total_gross = " . round($totalGross, 2) . " WHERE id = $invoiceId"); + + $journalText = $db->escape("Test invoice created (Vatgroup: {$vatgroup->name}, Region: {$regionData['name']})"); + $db->query("INSERT INTO ManualInvoiceJournal (manualinvoiceId, text, statusChange, createBy, `create`) VALUES ($invoiceId, '$journalText', 'erstellt', 1, " . time() . ")"); + + $customerName = trim(($address->company ?: '') . ' ' . ($address->firstname ?: '') . ' ' . ($address->lastname ?: '')) ?: 'Unknown'; + echo "[{$vatgroup->name}] [{$regionData['name']}] #{$i}: {$invoiceNumber} - {$customerName} - " . count($positions) . " pos - " . number_format($totalAfterRabatt, 2) . " EUR\n"; + $totalCreated++; + } + } +} + +echo "\n----------------------------------------\n"; +echo "Creating mixed VAT group invoices...\n"; +echo "----------------------------------------\n"; + +foreach ($regions as $regionKey => $regionData) { + $customers = $regionData['customers']; + if (empty($customers)) { echo "[Mixed] [{$regionData['name']}] SKIPPED - No customers\n"; continue; } + + for ($i = 1; $i <= 4; $i++) { + $customer = $customers[array_rand($customers)]; + $address = new Address($customer->id); + if (!$address || !$address->id) { $errors++; continue; } + + $vatarea = getVatArea($address, $db); + $positions = []; + $usedVatgroups = []; + + foreach ($articlesByVatgroup as $vgId => $vgArticles) { + if (empty($vgArticles)) continue; + $article = $vgArticles[array_rand($vgArticles)]; + $articleVatrate = $vatrates[$vgId][$vatarea] ?? null; + $posVatrateValue = $articleVatrate ? floatval($articleVatrate->rate) : 20.0; + $posFibuCostAccount = $articleVatrate ? intval($articleVatrate->account) : null; + $posFibuCostAccountLegacy = $articleVatrate ? ($articleVatrate->legacy_account ? intval($articleVatrate->legacy_account) : null) : null; + $posFibuTaxcode = $articleVatrate ? ($articleVatrate->taxcode ? intval($articleVatrate->taxcode) : null) : null; + + $amount = rand(1, 5); + $price = getArticlePrice($article); + $discount = rand(0, 1) ? rand(0, 10) : 0; + $priceTotal = $amount * $price * (1 - $discount / 100); + $priceGross = $priceTotal * (1 + $posVatrateValue / 100); + + $positions[] = [ + 'warehousearticle_id' => $article->id, + 'warehousearticle_name' => $article->articleNumber . ' | ' . $article->title, + 'product_info' => $article->description ?? '', + 'amount' => $amount, + 'unit' => $article->unit ?: 'Stk.', + 'price' => $price, + 'discount' => $discount, + 'vatrate' => $posVatrateValue, + 'price_total' => $priceTotal, + 'price_gross' => $priceGross, + 'fibu_cost_account' => $posFibuCostAccount, + 'fibu_cost_account_legacy' => $posFibuCostAccountLegacy, + 'fibu_taxcode' => $posFibuTaxcode + ]; + $usedVatgroups[] = $vgId; + } + + if (count($positions) < 2) { echo "[Mixed] [{$regionData['name']}] #{$i}: SKIPPED - Not enough vatgroups\n"; continue; } + + $primaryVatgroupId = $usedVatgroups[0]; + $primaryVatrate = $vatrates[$primaryVatgroupId][$vatarea] ?? null; + $fibuCostAccount = $primaryVatrate ? intval($primaryVatrate->account) : null; + $fibuCostAccountLegacy = $primaryVatrate ? ($primaryVatrate->legacy_account ? intval($primaryVatrate->legacy_account) : null) : null; + $fibuTaxcode = $primaryVatrate ? ($primaryVatrate->taxcode ? intval($primaryVatrate->taxcode) : null) : null; + $taxText = $primaryVatrate ? ($primaryVatrate->invoice_text ?? '') : ''; + + $randomDaysAgo = rand(0, 60); + $invoiceDate = strtotime("-{$randomDaysAgo} days"); + $totalDiscount = 0; + + $vatgroupNames = array_map(fn($vgId) => $vatgroups[$vgId]->name ?? "VG$vgId", $usedVatgroups); + + $invoiceData = [ + 'invoice_date' => $invoiceDate, + 'owner_id' => $address->id, + 'billingaddress_id' => $address->id, + 'customer_number' => $address->customer_number ?: 0, + 'company' => $address->company ?: '', + 'firstname' => $address->firstname ?: '', + 'lastname' => $address->lastname ?: '', + 'street' => $address->street ?: '', + 'zip' => $address->zip ?: '', + 'city' => $address->city ?: '', + 'country' => 'Österreich', + 'email' => $address->email ?: '', + 'uid' => $address->uid ?: '', + 'fibu_account_number' => $address->fibu_account_number ?: 0, + '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, + 'fibu_cost_area' => $vatarea, + 'fibu_cost_account' => $fibuCostAccount, + 'fibu_cost_account_legacy' => $fibuCostAccountLegacy, + 'fibu_taxcode' => $fibuTaxcode, + 'billing_type' => $address->billing_type ?: 'invoice', + '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, + 'performance_period' => date('m/Y', $invoiceDate), + 'introductory_text' => "Mixed VAT group test (" . implode(' + ', $vatgroupNames) . ") in '{$regionData['name']}'", + 'external_reference' => "TEST-MIXED-{$regionKey}-{$i}", + 'total_discount' => $totalDiscount, + 'vatgroup_id' => $primaryVatgroupId, + 'tax_text' => $taxText + ]; + + $result = createInvoice($db, $invoiceData); + $invoiceId = $result['id']; + $invoiceNumber = $result['invoice_number']; + + if (!$invoiceId) { echo "[Mixed] [{$regionData['name']}] #{$i}: ERROR\n"; $errors++; continue; } + + $total = 0; + $totalGross = 0; + foreach ($positions as $pos) { + createPosition($db, $invoiceId, $pos); + $total += $pos['price_total']; + $totalGross += $pos['price_gross']; + } + + $db->query("UPDATE ManualInvoice SET total = " . round($total, 2) . ", total_gross = " . round($totalGross, 2) . " WHERE id = $invoiceId"); + + $journalText = $db->escape("Mixed VAT group test invoice (" . implode(', ', $vatgroupNames) . ", {$regionData['name']})"); + $db->query("INSERT INTO ManualInvoiceJournal (manualinvoiceId, text, statusChange, createBy, `create`) VALUES ($invoiceId, '$journalText', 'erstellt', 1, " . time() . ")"); + + $customerName = trim(($address->company ?: '') . ' ' . ($address->firstname ?: '') . ' ' . ($address->lastname ?: '')) ?: 'Unknown'; + echo "[Mixed] [{$regionData['name']}] #{$i}: {$invoiceNumber} - {$customerName} - " . count($positions) . " pos - " . number_format($total, 2) . " EUR\n"; + $totalCreated++; + } +} + +echo "\n========================================\n"; +echo "Complete! Created: {$totalCreated} invoices, Errors: {$errors}\n"; +echo "========================================\n";