fix sales tax
This commit is contained in:
@@ -11,6 +11,11 @@
|
|||||||
* dieselbe QBO-Schnittstelle, die der Report "Sales Tax (QBO)" auf dem
|
* dieselbe QBO-Schnittstelle, die der Report "Sales Tax (QBO)" auf dem
|
||||||
* Reports-Tab nutzt. Die Abhängigkeit liegt auf API-Ebene, die Trennung der
|
* Reports-Tab nutzt. Die Abhängigkeit liegt auf API-Ebene, die Trennung der
|
||||||
* beiden Oberflächen berührt sie nicht.
|
* 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';
|
import '../utils/api.js';
|
||||||
@@ -175,17 +180,33 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
showLoading('sales-tax-detail', 'Loading tax summary from QBO…');
|
showLoading('sales-tax-detail', 'Loading tax summary from QBO…');
|
||||||
let taxData;
|
let taxData = null;
|
||||||
try {
|
try {
|
||||||
taxData = await window.API.accounting.getTaxSummary(startDate, endDate, 'Accrual');
|
taxData = await window.API.accounting.getTaxSummary(startDate, endDate, 'Accrual');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
// 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');
|
showError('sales-tax-detail', e.message || 'Failed to load tax summary');
|
||||||
return;
|
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 };
|
stCurrentTaxData = { ...parsed, startDate, endDate };
|
||||||
const taxCollected = parsed.taxCollected;
|
const taxCollected = parsed.taxCollected;
|
||||||
const totalSales = parsed.totalSales;
|
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>
|
class="px-2 py-1 text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
|
||||||
</div>
|
</div>
|
||||||
${reportHtml}
|
${reportHtml}
|
||||||
|
${snapshotNote}
|
||||||
|
|
||||||
<div class="mt-4 border-t pt-4">
|
<div class="mt-4 border-t pt-4">
|
||||||
<div class="flex items-center gap-4 text-sm mb-3" id="st-summary-bar">
|
<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-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>
|
<span id="st-net-due" class="text-lg font-bold text-gray-900"><strong>Net Due:</strong> ${fmtMoney(netPaid)}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -400,6 +422,56 @@ function parseTaxDataFromReport(taxData) {
|
|||||||
return { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0 };
|
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
|
// Init / Public Entry Point
|
||||||
// ────────────────────────────────────────────────────────────────────
|
// ────────────────────────────────────────────────────────────────────
|
||||||
|
|||||||
Reference in New Issue
Block a user