fix sales tax
This commit is contained in:
@@ -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">×</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
|
||||
// ────────────────────────────────────────────────────────────────────
|
||||
|
||||
Reference in New Issue
Block a user