From af235987eac0c6424edc7b2dc2b831a15351b1cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mathias=20Gla=CC=88ser?= Date: Sun, 20 Sep 2026 12:57:09 +0200 Subject: [PATCH] Steuerstatus und Steuersatz in Kostenkarte wiederherstellen --- .../market_management_hermes_mcp_api.md | 7 ++- modules/market_management/mcp.php | 4 +- modules/market_management/service.php | 45 ++++++++++++------- modules/shared/auth/ui/home.php | 3 ++ public/api/market-occurrence.php | 2 +- 5 files changed, 39 insertions(+), 22 deletions(-) diff --git a/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md index bbe8064..5f89f92 100644 --- a/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md +++ b/docs/modules/market_management/konzepte/market_management_hermes_mcp_api.md @@ -187,8 +187,11 @@ Weitere mögliche Tagesfelder sind `stand_start_time`, `stand_end_time`, `contact_email` und `contact_phone` ändern. - Kosten werden bei `market_management.create_occurrence` automatisch mit einer `Standkosten`-Position (`stand_fee`) und dem Standardsteuersatz 8.1 % angelegt. -- `market_management.update_occurrence` ändert Kosten über `gross_cost` sowie die - optionale mehrzeilige Freitext-Aufschlüsselung `cost_items_text`. +- `market_management.update_occurrence` ändert Kosten über `gross_cost`, + `tax_status`, `tax_rate` sowie die optionale mehrzeilige Freitext-Aufschlüsselung + `cost_items_text`. +- `tax_status` ist `taxable` oder `tax_free`; bei `tax_free` ist der Steuersatz + `0.00`. - `gross_cost` darf `0.00` sein; einzelne Kostenpositionen und Kostenarten werden nicht separat über MCP verwaltet. diff --git a/modules/market_management/mcp.php b/modules/market_management/mcp.php index 6d939de..28006e6 100644 --- a/modules/market_management/mcp.php +++ b/modules/market_management/mcp.php @@ -40,7 +40,7 @@ function market_management_mcp_tools(): array 'payment_status' => ['type' => ['string', 'null'], 'enum' => ['paid', 'unpaid']], 'payment_date' => ['type' => ['string', 'null'], 'format' => 'date'], 'paid_amount' => ['type' => ['number', 'null']], 'payment_method_code' => ['type' => ['string', 'null']], 'stand_size' => ['type' => ['string', 'null']], - 'gross_cost' => ['type' => ['number', 'null'], 'minimum' => 0], 'cost_items_text' => ['type' => ['string', 'null']], + 'gross_cost' => ['type' => ['number', 'null'], 'minimum' => 0], 'tax_status' => ['type' => ['string', 'null'], 'enum' => ['tax_free', 'taxable']], 'tax_rate' => ['type' => ['number', 'null'], 'minimum' => 0], 'cost_items_text' => ['type' => ['string', 'null']], 'table_available' => ['type' => ['boolean', 'null']], 'table_rentable' => ['type' => ['boolean', 'null']], 'own_table_required' => ['type' => ['boolean', 'null']], 'shelter_available' => ['type' => ['boolean', 'null']], 'power_available' => ['type' => ['boolean', 'null']], 'water_available' => ['type' => ['boolean', 'null']], @@ -216,7 +216,7 @@ function market_management_mcp_get_market(PDO $pdo, int $marketId, bool $include function market_management_mcp_update_occurrence(PDO $pdo, int $occurrenceId, array $input): array { $simpleCostFields = []; - foreach (['gross_cost', 'cost_items_text'] as $field) { + foreach (['gross_cost', 'cost_items_text', 'tax_status', 'tax_rate'] as $field) { if (array_key_exists($field, $input)) { $simpleCostFields[$field] = $input[$field]; unset($input[$field]); diff --git a/modules/market_management/service.php b/modules/market_management/service.php index 560e4b0..32722ae 100644 --- a/modules/market_management/service.php +++ b/modules/market_management/service.php @@ -1358,34 +1358,45 @@ function market_management_update_cost_item(PDO $pdo, int $costItemId, array $in function market_management_update_occurrence_simple_costs(PDO $pdo, int $occurrenceId, array $input): array { - $grossAmount = market_management_optional_number($input, 'gross_cost'); - if ($grossAmount === null) { - throw new InvalidArgumentException('Gesamtkosten sind erforderlich.'); - } $itemStmt = $pdo->prepare( - 'SELECT id FROM market_occurrence_cost_item + 'SELECT id, gross_amount, tax_status, tax_rate, description FROM market_occurrence_cost_item WHERE occurrence_id = :occurrence_id ORDER BY id ASC LIMIT 1' ); $itemStmt->execute([':occurrence_id' => $occurrenceId]); - $itemId = $itemStmt->fetchColumn(); - $description = array_key_exists('cost_items_text', $input) - ? market_management_optional_string($input, 'cost_items_text') - : null; - if ($itemId === false) { + $existingItem = $itemStmt->fetch(); + if (!is_array($existingItem)) { + $grossAmount = market_management_optional_number($input, 'gross_cost') ?? '0.00'; + $taxStatus = trim((string) ($input['tax_status'] ?? 'taxable')); + $taxRate = market_management_optional_number($input, 'tax_rate') ?? ($taxStatus === 'tax_free' ? '0.00' : '8.10'); $item = market_management_create_cost_item($pdo, [ 'occurrence_id' => $occurrenceId, 'cost_type_id' => market_management_get_cost_type_id($pdo, 'stand_fee'), - 'description' => $description, + 'description' => array_key_exists('cost_items_text', $input) ? market_management_optional_string($input, 'cost_items_text') : null, 'gross_amount' => $grossAmount, - 'tax_status' => 'taxable', - 'tax_rate' => 8.10, + 'tax_status' => $taxStatus, + 'tax_rate' => $taxRate, ]); } else { - $item = market_management_update_cost_item($pdo, (int) $itemId, [ - 'description' => $description, - 'gross_amount' => $grossAmount, - ]); + $costFields = []; + if (array_key_exists('gross_cost', $input)) { + $costFields['gross_amount'] = market_management_optional_number($input, 'gross_cost'); + } + if (array_key_exists('cost_items_text', $input)) { + $costFields['description'] = market_management_optional_string($input, 'cost_items_text'); + } + if (array_key_exists('tax_status', $input)) { + $costFields['tax_status'] = trim((string) $input['tax_status']); + if ($costFields['tax_status'] === 'tax_free' && !array_key_exists('tax_rate', $input)) { + $costFields['tax_rate'] = '0.00'; + } + } + if (array_key_exists('tax_rate', $input)) { + $costFields['tax_rate'] = market_management_optional_number($input, 'tax_rate'); + } + $item = $costFields === [] + ? (array) $existingItem + : market_management_update_cost_item($pdo, (int) $existingItem['id'], $costFields); } return $item; } diff --git a/modules/shared/auth/ui/home.php b/modules/shared/auth/ui/home.php index 4b9ce53..76d8945 100644 --- a/modules/shared/auth/ui/home.php +++ b/modules/shared/auth/ui/home.php @@ -1200,6 +1200,9 @@ function render_auth_home_page(array $user, array $otcProducts = [], array $otcM echo " const marketOccurrenceFieldObserver = new MutationObserver(() => normalizeOccurrenceOverviewForm(contentRoot)); marketOccurrenceFieldObserver.observe(contentRoot, { childList: true, subtree: true });"; echo " const normalizeMarketSimpleCostFields = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketSimpleCostsNormalized === 'true') { return; } const drawer = getMarketDrawer(); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) {} const item = Array.isArray(payload.occurrence?.cost_items) ? payload.occurrence.cost_items[0] : null; const total = form.querySelector('[name=gross_cost]'); const text = form.querySelector('[name=cost_items_text]'); const totalLabel = total?.closest('.sg-labeled-input-row')?.querySelector('.sg-label'); const textLabel = text?.closest('.sg-labeled-input-row')?.querySelector('.sg-label'); if (total) { total.disabled = false; total.removeAttribute('disabled'); total.type = 'number'; total.step = '0.01'; total.min = '0'; total.required = true; total.classList.remove('sg-input-single-line--disabled', 'sg-form-disabled'); total.value = payload.occurrence?.cost_summary?.gross_amount ?? item?.gross_amount ?? ''; } if (text) { text.disabled = false; text.removeAttribute('disabled'); text.classList.remove('sg-form-disabled'); text.value = item?.description || ''; } if (totalLabel) { totalLabel.textContent = 'Gesamtkosten'; } if (textLabel) { textLabel.textContent = 'Kostenpositionen'; } form.dataset.marketSimpleCostsNormalized = 'true'; }); };"; echo " const marketSimpleCostFieldsObserver = new MutationObserver(() => normalizeMarketSimpleCostFields(contentRoot)); marketSimpleCostFieldsObserver.observe(contentRoot, { childList: true, subtree: true }); normalizeMarketSimpleCostFields(contentRoot);"; + echo " const normalizeMarketTaxFields = (root) => { root.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation]').forEach((form) => { if (form.dataset.marketTaxFieldsNormalized === 'true') { return; } const total = form.querySelector('[name=gross_cost]'); const totalGroup = total?.closest('.sg-form-sections-card__field-group'); if (!totalGroup) { return; } const drawer = getMarketDrawer(); let payload = {}; try { payload = drawer?.dataset.marketPayload ? JSON.parse(drawer.dataset.marketPayload) : {}; } catch (error) {} const item = Array.isArray(payload.occurrence?.cost_items) ? payload.occurrence.cost_items[0] : null; const taxStatusGroup = document.createElement('div'); taxStatusGroup.className = 'sg-form-sections-card__field-group'; taxStatusGroup.innerHTML = marketEditField('tax_status', 'Steuerstatus', 'select', item?.tax_status || 'taxable', [['taxable', 'Steuerpflichtig'], ['tax_free', 'Steuerfrei']], true); const taxRateGroup = document.createElement('div'); taxRateGroup.className = 'sg-form-sections-card__field-group'; taxRateGroup.innerHTML = marketEditField('tax_rate', 'Steuersatz', 'number', item?.tax_rate ?? '8.10', [], true); totalGroup.after(taxStatusGroup, taxRateGroup); const taxStatus = taxStatusGroup.querySelector('[name=tax_status]'); const taxRate = taxRateGroup.querySelector('[name=tax_rate]'); const sync = () => { const taxFree = taxStatus?.value === 'tax_free'; if (taxFree && taxRate) { taxRate.value = '0.00'; } if (taxRate) { taxRate.disabled = taxFree; taxRate.setAttribute('aria-disabled', String(taxFree)); } }; taxStatus?.addEventListener('change', sync); sync(); form.dataset.marketTaxFieldsNormalized = 'true'; }); };"; + echo " const marketTaxFieldsObserver = new MutationObserver(() => normalizeMarketTaxFields(contentRoot)); marketTaxFieldsObserver.observe(contentRoot, { childList: true, subtree: true }); normalizeMarketTaxFields(contentRoot);"; + echo " const marketTaxRateAttributesObserver = new MutationObserver(() => { contentRoot.querySelectorAll('[data-market-edit-form][data-market-edit-scope=participation] [name=tax_rate]').forEach((input) => { input.step = '0.01'; input.min = '0'; }); }); marketTaxRateAttributesObserver.observe(contentRoot, { childList: true, subtree: true });"; echo " bindTable();"; echo "}"; echo "function initMarketDefinitionsBindings() {"; diff --git a/public/api/market-occurrence.php b/public/api/market-occurrence.php index ca32278..dd7c0fc 100644 --- a/public/api/market-occurrence.php +++ b/public/api/market-occurrence.php @@ -83,7 +83,7 @@ if ($requestMethod === 'POST') { } if ($fields !== []) { $simpleCostFields = []; - foreach (['gross_cost', 'cost_items_text'] as $costField) { + foreach (['gross_cost', 'cost_items_text', 'tax_status', 'tax_rate'] as $costField) { if (array_key_exists($costField, $fields)) { $simpleCostFields[$costField] = $fields[$costField]; unset($fields[$costField]);