fix sales tax

This commit is contained in:
2026-08-07 12:46:36 -05:00
parent e086fe8931
commit 0218fa694e

View File

@@ -11,6 +11,11 @@
* dieselbe QBO-Schnittstelle, die der Report "Sales Tax (QBO)" auf dem
* Reports-Tab nutzt. Die Abhängigkeit liegt auf API-Ebene, die Trennung der
* beiden Oberflächen berührt sie nicht.
*
* Maßgeblichkeit der Beträge: Sobald eine Periode in sales_tax_periods steht,
* ist der dort gespeicherte QBO-Snapshot (tax_collected & Co.) der angezeigte
* Grundbetrag. Der Live-Abruf liefert dann nur noch die Distrikt-Aufschlüsselung.
* Nur für eine noch nicht gespeicherte Periode kommen die Kennzahlen live.
*/
import '../utils/api.js';
@@ -175,17 +180,33 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
}
showLoading('sales-tax-detail', 'Loading tax summary from QBO…');
let taxData;
let taxData = null;
try {
taxData = await window.API.accounting.getTaxSummary(startDate, endDate, 'Accrual');
} catch (e) {
showError('sales-tax-detail', e.message || 'Failed to load tax summary');
return;
// Für eine gespeicherte Periode ist der Live-Abruf nur Beiwerk: die
// Kennzahlen stehen in der DB-Zeile, also weiter mit dem Snapshot.
if (!existingPeriod) {
showError('sales-tax-detail', e.message || 'Failed to load tax summary');
return;
}
console.error('Tax summary fetch failed, showing saved snapshot:', e.message);
}
const reportHtml = taxData?.Rows ? renderQboReport(taxData) : `<p class="text-sm text-gray-500">No tax data for this period.</p>`;
// Gespeicherte Periode → DB-Snapshot ist maßgeblich; der Live-Abruf liefert
// nur die Distrikt-Zeilen. Neue Periode → live geladene Werte.
const liveParsed = parseTaxDataFromReport(taxData);
const useSnapshot = !!existingPeriod;
const parsed = useSnapshot ? snapshotFromPeriod(existingPeriod, liveParsed) : liveParsed;
const reportData = useSnapshot ? withSnapshotGrandTotal(taxData, parsed) : taxData;
const reportHtml = reportData?.Rows
? renderQboReport(reportData)
: `<p class="text-sm text-gray-500">No tax data for this period.</p>`;
const snapshotNote = useSnapshot
? `<p class="mt-2 text-xs text-gray-500">Grand total shown is the saved QBO snapshot for this period${existingPeriod.updated_at ? ` (last saved ${formatDate(existingPeriod.updated_at)})` : ''}. The district rows above are recalculated live and may differ in detail.</p>`
: '';
const parsed = parseTaxDataFromReport(taxData);
stCurrentTaxData = { ...parsed, startDate, endDate };
const taxCollected = parsed.taxCollected;
const totalSales = parsed.totalSales;
@@ -210,10 +231,11 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
class="px-2 py-1 text-gray-400 hover:text-gray-600 text-lg leading-none">&times;</button>
</div>
${reportHtml}
${snapshotNote}
<div class="mt-4 border-t pt-4">
<div class="flex items-center gap-4 text-sm mb-3" id="st-summary-bar">
<span><strong>Tax Collected:</strong> ${fmtMoney(taxCollected)}</span>
<span><strong>Tax Collected:</strong> ${fmtMoney(taxCollected)}${useSnapshot ? ' <span class="text-xs text-gray-400">(saved)</span>' : ''}</span>
<span id="st-adj-display" class="${adjustments !== 0 ? 'text-red-600' : ''}"><strong>Adjustment:</strong> ${adjustments > 0 ? `${fmtMoney(adjustments)}` : adjustments < 0 ? `+$${Math.abs(adjustments).toFixed(2)}` : '—'}</span>
<span id="st-net-due" class="text-lg font-bold text-gray-900"><strong>Net Due:</strong> ${fmtMoney(netPaid)}</span>
</div>
@@ -400,6 +422,56 @@ function parseTaxDataFromReport(taxData) {
return { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0 };
}
/** numeric aus der DB (pg liefert NUMERIC als String) — sonst Fallback. */
function numOr(value, fallback) {
if (value == null || value === '') return fallback;
const n = parseFloat(value);
return Number.isFinite(n) ? n : fallback;
}
/**
* Kennzahlen einer gespeicherten Periode aus der DB-Zeile.
* Fällt spaltenweise auf den Live-Wert zurück, falls die Spalte leer ist
* (Altbestände ohne vollständigen Snapshot).
*/
function snapshotFromPeriod(period, fallback) {
return {
totalSales: numOr(period.total_sales, fallback.totalSales),
nontaxableSales: numOr(period.nontaxable_sales, fallback.nontaxableSales),
taxableSales: numOr(period.taxable_sales, fallback.taxableSales),
taxCollected: numOr(period.tax_collected, fallback.taxCollected)
};
}
/**
* Ersetzt im live geladenen Report die GRAND-TOTAL-Zeile durch den gespeicherten
* Snapshot, damit die Tabelle nicht einen anderen Gesamtbetrag zeigt als die
* Summary-Leiste darunter. Die Distrikt-Zeilen bleiben unberührt (Aufschlüsselung).
* Arbeitet auf einer flachen Kopie — taxData selbst wird nicht mutiert.
*/
function withSnapshotGrandTotal(taxData, snapshot) {
if (!taxData?.Rows?.Row || !Array.isArray(taxData.Rows.Row)) return taxData;
const rows = taxData.Rows.Row.map(row => {
const label = row?.Header?.ColData?.[0]?.value;
if (row?.type !== 'Section' || !label || !label.toLowerCase().includes('grand total')) return row;
const cd = row.Summary?.ColData || [];
return {
...row,
Summary: {
ColData: [
{ value: cd[0]?.value || 'Total' },
{ value: snapshot.totalSales.toFixed(2) },
{ value: snapshot.nontaxableSales.toFixed(2) },
{ value: snapshot.taxableSales.toFixed(2) },
{ value: snapshot.taxCollected.toFixed(2) },
{ value: cd[5]?.value || '' }
]
}
};
});
return { ...taxData, Rows: { ...taxData.Rows, Row: rows } };
}
// ────────────────────────────────────────────────────────────────────
// Init / Public Entry Point
// ────────────────────────────────────────────────────────────────────