sales tax
This commit is contained in:
19
migrations/add-sales-tax-district-breakdown.sql
Normal file
19
migrations/add-sales-tax-district-breakdown.sql
Normal file
@@ -0,0 +1,19 @@
|
||||
-- Speichert die Distrikt-Aufschlüsselung einer Sales-Tax-Periode zum Zeitpunkt
|
||||
-- des Speicherns. Die Aufschlüsselung liegt in der getTaxSummary()-Antwort bereits
|
||||
-- vor (accounting-service.js:438-466) und wurde bisher im Frontend verworfen.
|
||||
--
|
||||
-- Rein additiv: die vier Aggregat-Spalten (total_sales, nontaxable_sales,
|
||||
-- taxable_sales, tax_collected) und ihre Werte bleiben unangetastet.
|
||||
--
|
||||
-- Bestandszeilen bleiben bewusst NULL. Sie werden NICHT rückwirkend per
|
||||
-- Live-Abfrage befuellt — deren Datumsfenster weicht ab (Datums-Roundtrip in
|
||||
-- accounting-service.js:393). Altperioden laufen dauerhaft über die als
|
||||
-- indikativ gekennzeichnete Live-Anzeige.
|
||||
--
|
||||
-- Idempotent: mehrfach ausführbar.
|
||||
|
||||
ALTER TABLE sales_tax_periods
|
||||
ADD COLUMN IF NOT EXISTS district_breakdown JSONB;
|
||||
|
||||
COMMENT ON COLUMN sales_tax_periods.district_breakdown IS
|
||||
'Snapshot der Distrikt-Aufschlüsselung beim Speichern. NULL = kein Snapshot vorhanden (Altperiode), Anzeige faellt auf indikative Live-Werte zurueck.';
|
||||
@@ -155,6 +155,17 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
||||
const adjStr = adj > 0 ? `−${fmtMoney(adj)}` : adj < 0 ? `+$${Math.abs(adj).toFixed(2)}` : '—';
|
||||
const paidOn = formatDate(existingPeriod.booked_at);
|
||||
|
||||
// Aufschlüsselung nur, wenn sie beim Speichern festgehalten wurde. Für
|
||||
// Altperioden bleibt es wie bisher bei den Aggregaten — hier wird bewusst
|
||||
// NICHT live nachgeladen.
|
||||
const paidSnapshot = snapshotFromPeriod(existingPeriod, EMPTY_TAX_DATA);
|
||||
const paidBreakdownHtml = paidSnapshot.districts.length
|
||||
? `<div class="mb-4">
|
||||
${renderQboReport(reportFromSnapshot(paidSnapshot.districts, paidSnapshot, existingPeriod.period_start, existingPeriod.period_end))}
|
||||
<p class="mt-2 text-xs text-gray-500">District rows and grand total both come from the saved snapshot for this period.</p>
|
||||
</div>`
|
||||
: '';
|
||||
|
||||
detailEl.innerHTML = `
|
||||
<div class="bg-white rounded-lg shadow-sm border border-gray-200 p-4">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
@@ -162,6 +173,7 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
||||
<button onclick="window.salesTaxView.closeTaxPeriodDetail()" class="px-2 py-1 text-gray-400 hover:text-gray-600 text-lg leading-none">×</button>
|
||||
</div>
|
||||
<p class="text-sm text-green-700 font-semibold mb-4">✅ Paid on ${paidOn}</p>
|
||||
${paidBreakdownHtml}
|
||||
<table class="min-w-full text-sm border border-gray-200 rounded">
|
||||
<tbody>
|
||||
<tr class="border-t"><td class="px-3 py-2 text-gray-600">Total Sales</td><td class="px-3 py-2 text-right font-medium">${fmtMoney(parseFloat(existingPeriod.total_sales) || 0)}</td></tr>
|
||||
@@ -193,19 +205,15 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
||||
console.error('Tax summary fetch failed, showing saved snapshot:', e.message);
|
||||
}
|
||||
|
||||
// Gespeicherte Periode → DB-Snapshot ist maßgeblich; der Live-Abruf liefert
|
||||
// nur die Distrikt-Zeilen. Neue Periode → live geladene Werte.
|
||||
// Gespeicherte Periode → DB-Snapshot ist maßgeblich. Neue Periode → live.
|
||||
const liveParsed = parseTaxDataFromReport(taxData);
|
||||
const useSnapshot = !!existingPeriod;
|
||||
const parsed = useSnapshot ? snapshotFromPeriod(existingPeriod, liveParsed) : liveParsed;
|
||||
const hasSavedBreakdown = useSnapshot && parsed.districts.length > 0;
|
||||
|
||||
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 { html: reportHtml, note: snapshotNote } = renderPeriodBreakdown({
|
||||
taxData, parsed, startDate, endDate, existingPeriod, useSnapshot, hasSavedBreakdown
|
||||
});
|
||||
|
||||
stCurrentTaxData = { ...parsed, startDate, endDate };
|
||||
const taxCollected = parsed.taxCollected;
|
||||
@@ -235,7 +243,7 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
||||
|
||||
<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)}${useSnapshot ? ' <span class="text-xs text-gray-400">(saved)</span>' : ''}</span>
|
||||
<span class="${useSnapshot ? 'px-2 py-1 rounded bg-gray-100 border border-gray-300' : ''}"><strong>Tax Collected:</strong> ${fmtMoney(taxCollected)}${useSnapshot ? ` <span class="text-xs text-gray-500">(saved${hasSavedBreakdown ? '' : ' — authoritative'})</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>
|
||||
@@ -335,6 +343,19 @@ export function updateTaxPreview() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Breakdown, der beim Speichern mitgeht — oder null.
|
||||
*
|
||||
* null bedeutet für den Server "nicht anfassen" (COALESCE im UPSERT). Bei einer
|
||||
* Altperiode ohne gespeicherten Breakdown ist stCurrentTaxData.districts leer,
|
||||
* weil snapshotFromPeriod() bewusst nicht auf die Live-Zeilen zurückfällt — ein
|
||||
* erneutes Speichern befüllt die Spalte also NICHT nachträglich mit Live-Daten.
|
||||
*/
|
||||
function breakdownForSave() {
|
||||
const districts = stCurrentTaxData?.districts;
|
||||
return Array.isArray(districts) && districts.length ? districts : null;
|
||||
}
|
||||
|
||||
export async function saveTaxPeriodDraft() {
|
||||
const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0;
|
||||
const adjReason = document.getElementById('st-adjustment-reason').value.trim();
|
||||
@@ -352,6 +373,7 @@ export async function saveTaxPeriodDraft() {
|
||||
adjustment_amount: adjAmount,
|
||||
adjustment_reason: adjReason || null,
|
||||
net_paid: stCurrentTaxData.taxCollected - adjAmount,
|
||||
district_breakdown: breakdownForSave(),
|
||||
status: 'open'
|
||||
});
|
||||
await loadTaxPeriods();
|
||||
@@ -387,6 +409,7 @@ export async function markPeriodPaid() {
|
||||
adjustment_amount: adjAmount,
|
||||
adjustment_reason: adjReason || null,
|
||||
net_paid: netPaid,
|
||||
district_breakdown: breakdownForSave(),
|
||||
status: 'open'
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -405,21 +428,109 @@ export async function markPeriodPaid() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Entscheidet, welche Aufschlüsselung eine Periode zeigt:
|
||||
* 1. gespeicherter Breakdown → Zeilen und Grand Total aus derselben DB-Zeile
|
||||
* 2. gespeichert, kein Breakdown (Altperiode) → Live-Zeilen, als indikativ markiert
|
||||
* 3. neue Periode → Live-Report, live ist hier maßgeblich
|
||||
*/
|
||||
function renderPeriodBreakdown({ taxData, parsed, startDate, endDate, existingPeriod, useSnapshot, hasSavedBreakdown }) {
|
||||
const savedOn = existingPeriod?.updated_at ? ` (saved ${formatDate(existingPeriod.updated_at)})` : '';
|
||||
|
||||
if (hasSavedBreakdown) {
|
||||
return {
|
||||
html: renderQboReport(reportFromSnapshot(parsed.districts, parsed, startDate, endDate)),
|
||||
note: `<p class="mt-2 text-xs text-gray-500">District rows and grand total both come from the saved snapshot for this period${savedOn}.</p>`
|
||||
};
|
||||
}
|
||||
|
||||
if (!useSnapshot) {
|
||||
return {
|
||||
html: taxData?.Rows ? renderQboReport(taxData) : `<p class="text-sm text-gray-500">No tax data for this period.</p>`,
|
||||
note: ''
|
||||
};
|
||||
}
|
||||
|
||||
// Altperiode: kein Snapshot der Aufschlüsselung vorhanden und keiner mehr
|
||||
// rekonstruierbar. Live-Zeilen optisch untergeordnet und explizit entwertet.
|
||||
const liveTable = taxData?.Rows
|
||||
? `<div class="opacity-60 text-xs">${renderQboReport(markLiveGrandTotalAsIndicative(taxData))}</div>`
|
||||
: `<p class="text-sm text-gray-500">No live tax data for this period.</p>`;
|
||||
|
||||
return {
|
||||
html: `
|
||||
<div class="rounded border border-amber-200 bg-amber-50 px-3 py-2 mb-3">
|
||||
<p class="text-sm font-semibold text-amber-900">Indicative live breakdown — not the filed figures</p>
|
||||
<p class="text-xs text-amber-800 mt-0.5">
|
||||
No district breakdown was stored for this period${savedOn ? savedOn.replace(' (saved', ' (recorded') : ''}.
|
||||
The rows below are recalculated live from QuickBooks and will not add up to the grand total.
|
||||
<strong>The saved grand total below is the authoritative figure.</strong>
|
||||
</p>
|
||||
</div>
|
||||
${liveTable}`,
|
||||
note: ''
|
||||
};
|
||||
}
|
||||
|
||||
const EMPTY_TAX_DATA = { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0, districts: [] };
|
||||
|
||||
function isGrandTotalSection(row) {
|
||||
return row?.type === 'Section'
|
||||
&& String(row.Header?.ColData?.[0]?.value || '').toLowerCase().includes('grand total');
|
||||
}
|
||||
|
||||
/**
|
||||
* Liest die GRAND-TOTAL-Aggregate UND die Distrikt-Sections aus der
|
||||
* getTaxSummary()-Antwort. Beide stammen aus demselben Belegsatz
|
||||
* (accounting-service.js:438-494), sind also in sich konsistent.
|
||||
*/
|
||||
function parseTaxDataFromReport(taxData) {
|
||||
if (!taxData?.Rows?.Row) return { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0 };
|
||||
const rows = Array.isArray(taxData.Rows.Row) ? taxData.Rows.Row : [];
|
||||
for (const row of rows) {
|
||||
if (row.type === 'Section' && row.Header?.ColData?.[0]?.value?.toLowerCase().includes('grand total')) {
|
||||
const rows = Array.isArray(taxData?.Rows?.Row) ? taxData.Rows.Row : null;
|
||||
if (!rows) return { ...EMPTY_TAX_DATA };
|
||||
|
||||
const districts = rows
|
||||
.filter(row => row?.type === 'Section' && !isGrandTotalSection(row))
|
||||
.map(row => {
|
||||
const cd = row.Summary?.ColData || [];
|
||||
return {
|
||||
totalSales: parseFloat(cd[1]?.value || '0') || 0,
|
||||
nontaxableSales: parseFloat(cd[2]?.value || '0') || 0,
|
||||
taxableSales: parseFloat(cd[3]?.value || '0') || 0,
|
||||
taxCollected: parseFloat(cd[4]?.value || '0') || 0
|
||||
name: row.Header?.ColData?.[0]?.value || '',
|
||||
ratePct: row.Header?.ColData?.[5]?.value || cd[5]?.value || '',
|
||||
totalSales: numOr(cd[1]?.value, 0),
|
||||
nontaxableSales: numOr(cd[2]?.value, 0),
|
||||
taxableSales: numOr(cd[3]?.value, 0),
|
||||
taxCollected: numOr(cd[4]?.value, 0)
|
||||
};
|
||||
}
|
||||
});
|
||||
|
||||
const grand = rows.find(isGrandTotalSection);
|
||||
if (!grand) return { ...EMPTY_TAX_DATA, districts };
|
||||
|
||||
const cd = grand.Summary?.ColData || [];
|
||||
return {
|
||||
totalSales: numOr(cd[1]?.value, 0),
|
||||
nontaxableSales: numOr(cd[2]?.value, 0),
|
||||
taxableSales: numOr(cd[3]?.value, 0),
|
||||
taxCollected: numOr(cd[4]?.value, 0),
|
||||
districts
|
||||
};
|
||||
}
|
||||
|
||||
/** JSONB-Spalte: pg liefert bereits ein Objekt, ältere Pfade evtl. einen String. */
|
||||
function parseBreakdownColumn(value) {
|
||||
if (!value) return null;
|
||||
let parsed = value;
|
||||
if (typeof value === 'string') {
|
||||
try { parsed = JSON.parse(value); } catch { return null; }
|
||||
}
|
||||
return { totalSales: 0, nontaxableSales: 0, taxableSales: 0, taxCollected: 0 };
|
||||
if (!Array.isArray(parsed) || !parsed.length) return null;
|
||||
return parsed.map(d => ({
|
||||
name: d?.name || '',
|
||||
ratePct: d?.ratePct || '',
|
||||
totalSales: numOr(d?.totalSales, 0),
|
||||
nontaxableSales: numOr(d?.nontaxableSales, 0),
|
||||
taxableSales: numOr(d?.taxableSales, 0),
|
||||
taxCollected: numOr(d?.taxCollected, 0)
|
||||
}));
|
||||
}
|
||||
|
||||
/** numeric aus der DB (pg liefert NUMERIC als String) — sonst Fallback. */
|
||||
@@ -433,40 +544,127 @@ function numOr(value, 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).
|
||||
*
|
||||
* districts kommt AUSSCHLIESSLICH aus district_breakdown — nie aus dem Live-Abruf.
|
||||
* Sonst würde ein erneutes Speichern einer Altperiode eine live berechnete
|
||||
* Aufschlüsselung neben den abweichenden gespeicherten Grand Total schreiben.
|
||||
*/
|
||||
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)
|
||||
taxCollected: numOr(period.tax_collected, fallback.taxCollected),
|
||||
districts: parseBreakdownColumn(period.district_breakdown) || []
|
||||
};
|
||||
}
|
||||
|
||||
function round2(n) { return Math.round(n * 100) / 100; }
|
||||
|
||||
/** Verkaufsspalten wie im Live-Report als ganze Dollar, damit beide Tabellen gleich aussehen. */
|
||||
function salesCell(n) { return String(Math.round(n)); }
|
||||
|
||||
const REPORT_COLUMNS = {
|
||||
Column: [
|
||||
{ ColTitle: '', ColType: 'Text' },
|
||||
{ ColTitle: 'Total Sales', ColType: 'Money' },
|
||||
{ ColTitle: 'Nontaxable', ColType: 'Money' },
|
||||
{ ColTitle: 'Taxable', ColType: 'Money' },
|
||||
{ ColTitle: 'Tax Collected', ColType: 'Money' },
|
||||
{ ColTitle: 'Tax Rate', ColType: 'Percent' }
|
||||
]
|
||||
};
|
||||
|
||||
/**
|
||||
* Baut aus dem gespeicherten Snapshot eine Report-Struktur für renderQboReport().
|
||||
* Distrikt-Zeilen und Grand Total stammen dann aus derselben DB-Zeile.
|
||||
*
|
||||
* Bleibt zwischen der Summe der Distriktbeträge und dem gespeicherten Grand Total
|
||||
* ein Cent-Rest stehen (die Distriktwerte wurden je Zeile einzeln gerundet, der
|
||||
* Grand Total einmal am Ende — accounting-service.js:461 vs. :491), wird er als
|
||||
* eigene Zeile ausgewiesen statt stillschweigend geschluckt.
|
||||
*/
|
||||
function reportFromSnapshot(districts, snapshot, startDate, endDate) {
|
||||
const rows = districts.map(d => ({
|
||||
type: 'Section',
|
||||
Header: {
|
||||
ColData: [
|
||||
{ value: d.name }, { value: '' }, { value: '' },
|
||||
{ value: '' }, { value: '' }, { value: d.ratePct || '' }
|
||||
]
|
||||
},
|
||||
Summary: {
|
||||
ColData: [
|
||||
{ value: 'Total' },
|
||||
{ value: salesCell(d.totalSales) },
|
||||
{ value: salesCell(d.nontaxableSales) },
|
||||
{ value: salesCell(d.taxableSales) },
|
||||
{ value: d.taxCollected.toFixed(2) },
|
||||
{ value: d.ratePct || '' }
|
||||
]
|
||||
}
|
||||
}));
|
||||
|
||||
const districtSum = round2(districts.reduce((s, d) => s + d.taxCollected, 0));
|
||||
const roundingRest = round2(snapshot.taxCollected - districtSum);
|
||||
if (roundingRest !== 0) {
|
||||
rows.push({
|
||||
ColData: [
|
||||
{ value: 'Rounding difference' }, { value: '' }, { value: '' },
|
||||
{ value: '' }, { value: roundingRest.toFixed(2) }, { value: '' }
|
||||
]
|
||||
});
|
||||
}
|
||||
|
||||
rows.push({
|
||||
type: 'Section',
|
||||
Header: {
|
||||
ColData: [
|
||||
{ value: 'GRAND TOTAL' }, { value: '' }, { value: '' },
|
||||
{ value: '' }, { value: '' }, { value: '' }
|
||||
]
|
||||
},
|
||||
Summary: {
|
||||
ColData: [
|
||||
{ value: 'Total' },
|
||||
{ value: salesCell(snapshot.totalSales) },
|
||||
{ value: salesCell(snapshot.nontaxableSales) },
|
||||
{ value: salesCell(snapshot.taxableSales) },
|
||||
{ value: snapshot.taxCollected.toFixed(2) },
|
||||
{ value: '' }
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
Header: {
|
||||
ReportName: 'Sales Tax Liability (saved snapshot)',
|
||||
StartPeriod: String(startDate).split('T')[0],
|
||||
EndPeriod: String(endDate).split('T')[0],
|
||||
ReportBasis: 'Accrual'
|
||||
},
|
||||
Columns: REPORT_COLUMNS,
|
||||
Rows: { Row: rows }
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* Kennzeichnet die GRAND-TOTAL-Zeile des Live-Reports als indikativ und lässt ihre
|
||||
* Live-Werte stehen. Die Tabelle bleibt damit in sich konsistent (Live-Zeilen
|
||||
* summieren sich auf ihren eigenen Live-Total); der maßgebliche gespeicherte Betrag
|
||||
* steht separat darunter. Bewusst KEINE Ersetzung durch den Snapshot-Wert — das
|
||||
* würde suggerieren, die Live-Zeilen summierten sich darauf auf.
|
||||
* Arbeitet auf einer flachen Kopie — taxData wird nicht mutiert.
|
||||
*/
|
||||
function withSnapshotGrandTotal(taxData, snapshot) {
|
||||
if (!taxData?.Rows?.Row || !Array.isArray(taxData.Rows.Row)) return taxData;
|
||||
function markLiveGrandTotalAsIndicative(taxData) {
|
||||
if (!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;
|
||||
if (!isGrandTotalSection(row)) 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 || '' }
|
||||
]
|
||||
}
|
||||
Header: { ColData: [{ value: 'LIVE TOTAL (INDICATIVE)' }, ...(row.Header.ColData.slice(1))] },
|
||||
Summary: { ColData: [{ value: 'Live total' }, ...cd.slice(1)] }
|
||||
};
|
||||
});
|
||||
return { ...taxData, Rows: { ...taxData.Rows, Row: rows } };
|
||||
|
||||
@@ -534,8 +534,8 @@ async function upsertTaxPeriod(period) {
|
||||
(period_start, period_end, total_sales, nontaxable_sales, taxable_sales, tax_collected,
|
||||
adjustment_amount, adjustment_reason, adjustment_account_id, adjustment_account_name,
|
||||
net_paid, bank_account_id, bank_account_name,
|
||||
sales_tax_payable_id, sales_tax_payable_name, status)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16)
|
||||
sales_tax_payable_id, sales_tax_payable_name, status, district_breakdown)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15,$16,$17)
|
||||
ON CONFLICT (period_start, period_end) DO UPDATE SET
|
||||
total_sales = EXCLUDED.total_sales,
|
||||
nontaxable_sales = EXCLUDED.nontaxable_sales,
|
||||
@@ -551,6 +551,9 @@ async function upsertTaxPeriod(period) {
|
||||
sales_tax_payable_id = COALESCE(sales_tax_periods.sales_tax_payable_id, EXCLUDED.sales_tax_payable_id),
|
||||
sales_tax_payable_name = COALESCE(sales_tax_periods.sales_tax_payable_name, EXCLUDED.sales_tax_payable_name),
|
||||
status = COALESCE(sales_tax_periods.status, EXCLUDED.status),
|
||||
-- Additiv: ein bereits festgehaltener Breakdown wird nie durch NULL
|
||||
-- ueberschrieben; Altperioden bleiben ohne Snapshot.
|
||||
district_breakdown = COALESCE(EXCLUDED.district_breakdown, sales_tax_periods.district_breakdown),
|
||||
updated_at = CURRENT_TIMESTAMP
|
||||
RETURNING *`,
|
||||
[
|
||||
@@ -560,7 +563,8 @@ async function upsertTaxPeriod(period) {
|
||||
period.adjustment_account_id || null, period.adjustment_account_name || null,
|
||||
period.net_paid || null, period.bank_account_id || null, period.bank_account_name || null,
|
||||
period.sales_tax_payable_id || null, period.sales_tax_payable_name || null,
|
||||
period.status || 'open'
|
||||
period.status || 'open',
|
||||
period.district_breakdown ? JSON.stringify(period.district_breakdown) : null
|
||||
]
|
||||
);
|
||||
return result.rows[0];
|
||||
|
||||
Reference in New Issue
Block a user