diff --git a/migrations/add-sales-tax-periods-status.sql b/migrations/add-sales-tax-periods-status.sql
new file mode 100644
index 0000000..5ec5801
--- /dev/null
+++ b/migrations/add-sales-tax-periods-status.sql
@@ -0,0 +1,2 @@
+ALTER TABLE sales_tax_periods ADD COLUMN status VARCHAR(20) DEFAULT 'open';
+UPDATE sales_tax_periods SET status = 'booked' WHERE qbo_journal_entry_id IS NOT NULL;
diff --git a/public/js/utils/api.js b/public/js/utils/api.js
index a079b40..7447ebb 100644
--- a/public/js/utils/api.js
+++ b/public/js/utils/api.js
@@ -152,6 +152,9 @@ const API = {
recordTaxPayment: (periodId, data) => fetch(`/api/accounting/sales-tax/periods/${periodId}/record`, {
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
}).then(r => r.json()),
+ markTaxPaidExternal: (periodId) => fetch(`/api/accounting/sales-tax/periods/${periodId}/mark-external`, {
+ method: 'POST'
+ }).then(r => r.json()),
// Phase 2 Lieferung 1 — Sync + Cache-Reads
syncAccounts: () => fetch('/api/accounting/sync-accounts', { method: 'POST' }).then(r => r.json()),
diff --git a/public/js/views/accounting-view.js b/public/js/views/accounting-view.js
index 5a73c86..5f43fbe 100644
--- a/public/js/views/accounting-view.js
+++ b/public/js/views/accounting-view.js
@@ -762,22 +762,28 @@ function renderTaxPeriodsTable() {
}
const rows = stPeriods.map(p => {
- const status = p.qbo_journal_entry_id
- ? `Paid`
- : `Open`;
+ const pStatus = p.status || (p.qbo_journal_entry_id ? 'booked' : 'open');
+ let statusHtml, statusColor;
+ if (pStatus === 'booked') { statusHtml = 'Booked'; statusColor = 'bg-green-100 text-green-800'; }
+ else if (pStatus === 'external') { statusHtml = 'External'; statusColor = 'bg-blue-100 text-blue-800'; }
+ else { statusHtml = 'Open'; statusColor = 'bg-yellow-100 text-yellow-800'; }
+
const monthLabel = new Date(p.period_start).toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
const adj = parseFloat(p.adjustment_amount) || 0;
const adjStr = adj !== 0 ? (adj > 0 ? `−$${adj.toFixed(2)}` : `+$${Math.abs(adj).toFixed(2)}`) : '—';
const netPaid = parseFloat(p.net_paid) || parseFloat(p.tax_collected) || 0;
+ const paidOn = pStatus === 'external' ? 'Paid in QBO' : (p.booked_at ? formatDate(p.booked_at) : '—');
return `
| ${monthLabel} |
- ${status} |
+
+ ${statusHtml}
+ |
${fmtMoney(parseFloat(p.tax_collected) || 0)} |
${adjStr} |
${fmtMoney(netPaid)} |
- ${p.booked_at ? formatDate(p.booked_at) : '—'} |
+ ${paidOn} |
@@ -851,7 +857,9 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
const adjustments = existingPeriod?.adjustment_amount != null ? parseFloat(existingPeriod.adjustment_amount) || 0 : 0;
const adjReason = existingPeriod?.adjustment_reason || '';
const netPaid = taxCollected - adjustments;
- const isPaid = existingPeriod && existingPeriod.qbo_journal_entry_id;
+ const periodStatus = existingPeriod?.status || (existingPeriod?.qbo_journal_entry_id ? 'booked' : 'open');
+ const isOpen = periodStatus === 'open';
+ const isEditable = isOpen;
const bankOpts = stAccounts
.filter(a => a.accountType === 'Bank' || a.accountType === 'Credit Card')
@@ -891,19 +899,19 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
-
-
@@ -1138,6 +1153,62 @@ export async function recordTaxPayment() {
}
}
+export async function markTaxPaidExternal() {
+ if (!stEditingPeriodId) return alert('Please save the period first (Save Draft).');
+
+ const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0;
+ const adjReason = document.getElementById('st-adjustment-reason').value.trim();
+ const adjAccountEl = document.getElementById('st-adjustment-account');
+ const adjAccountId = adjAccountEl.value;
+ const adjAccountName = adjAccountId ? adjAccountEl.options[adjAccountEl.selectedIndex]?.text : '';
+
+ const bankEl = document.getElementById('st-bank-account');
+ const bankId = bankEl.value;
+ const bankName = bankId ? bankEl.options[bankEl.selectedIndex]?.text : '';
+
+ const payableEl = document.getElementById('st-payable-account');
+ const payableId = payableEl.value;
+ const payableName = payableId ? payableEl.options[payableEl.selectedIndex]?.text : '';
+
+ if (!confirm(`Mark this period as already paid in QBO (external)?\n\nNo Journal Entry will be created — this only records the status in the app.\n\nTax Collected: $${stCurrentTaxData.taxCollected.toFixed(2)}\nAdjustment: $${adjAmount.toFixed(2)}\nNet Due: $${(stCurrentTaxData.taxCollected - adjAmount).toFixed(2)}`)) return;
+
+ if (!stCurrentTaxData) return alert('No tax data loaded.');
+
+ try {
+ await window.API.accounting.upsertTaxPeriod({
+ period_start: stCurrentTaxData.startDate,
+ period_end: stCurrentTaxData.endDate,
+ total_sales: stCurrentTaxData.totalSales,
+ nontaxable_sales: stCurrentTaxData.nontaxableSales,
+ taxable_sales: stCurrentTaxData.taxableSales,
+ tax_collected: stCurrentTaxData.taxCollected,
+ adjustment_amount: adjAmount,
+ adjustment_reason: adjReason || null,
+ adjustment_account_id: adjAccountId || null,
+ adjustment_account_name: adjAccountName || null,
+ net_paid: stCurrentTaxData.taxCollected - adjAmount,
+ bank_account_id: bankId || null,
+ bank_account_name: bankName || null,
+ sales_tax_payable_id: payableId || null,
+ sales_tax_payable_name: payableName || null,
+ status: 'open'
+ });
+ } catch (e) {
+ return alert('Failed to save period: ' + e.message);
+ }
+
+ try {
+ const result = await window.API.accounting.markTaxPaidExternal(stEditingPeriodId);
+ await loadTaxPeriods();
+ const updated = stPeriods.find(p => p.id === stEditingPeriodId);
+ if (updated) {
+ await openTaxPeriodDetail(updated.period_start, updated.period_end, updated);
+ }
+ } catch (e) {
+ alert('Failed: ' + e.message);
+ }
+}
+
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 : [];
@@ -1228,5 +1299,6 @@ window.accountingView = {
closeTaxPeriodDetail,
updateTaxPreview,
saveTaxPeriodDraft,
- recordTaxPayment
+ recordTaxPayment,
+ markTaxPaidExternal
};
\ No newline at end of file
diff --git a/schema.sql b/schema.sql
index 6d314c1..029a162 100644
--- a/schema.sql
+++ b/schema.sql
@@ -378,6 +378,7 @@ CREATE TABLE public.sales_tax_periods (
sales_tax_payable_id character varying(50),
sales_tax_payable_name character varying(200),
qbo_journal_entry_id character varying(50),
+ status character varying(20) DEFAULT 'open',
booked_at timestamp without time zone,
created_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
updated_at timestamp without time zone DEFAULT CURRENT_TIMESTAMP,
diff --git a/src/routes/accounting.js b/src/routes/accounting.js
index 4ad4be9..0623f1b 100644
--- a/src/routes/accounting.js
+++ b/src/routes/accounting.js
@@ -118,6 +118,12 @@ router.post('/sales-tax/periods/:id/record', async (req, res) => {
} catch (err) { handleQboError(err, res, 'sales-tax-record'); }
});
+router.post('/sales-tax/periods/:id/mark-external', async (req, res) => {
+ try {
+ res.json(await accountingService.markTaxPaidExternal(req.params.id));
+ } catch (err) { handleQboError(err, res, 'sales-tax-mark-external'); }
+});
+
// ════════════════════════════════════════════════════════════════════
// Phase 2 Lieferung 1 — Sync + Cache-Reads
// ════════════════════════════════════════════════════════════════════
diff --git a/src/services/accounting-service.js b/src/services/accounting-service.js
index 6dbd882..dec635f 100644
--- a/src/services/accounting-service.js
+++ b/src/services/accounting-service.js
@@ -527,8 +527,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)
- VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14,$15)
+ 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)
ON CONFLICT (period_start, period_end) DO UPDATE SET
total_sales = EXCLUDED.total_sales,
nontaxable_sales = EXCLUDED.nontaxable_sales,
@@ -543,6 +543,7 @@ async function upsertTaxPeriod(period) {
bank_account_name = COALESCE(sales_tax_periods.bank_account_name, EXCLUDED.bank_account_name),
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),
updated_at = CURRENT_TIMESTAMP
RETURNING *`,
[
@@ -551,7 +552,8 @@ async function upsertTaxPeriod(period) {
period.adjustment_amount || 0, period.adjustment_reason || null,
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.sales_tax_payable_id || null, period.sales_tax_payable_name || null,
+ period.status || 'open'
]
);
return result.rows[0];
@@ -637,7 +639,7 @@ async function createTaxPaymentJE({
await pool.query(
`UPDATE sales_tax_periods
- SET qbo_journal_entry_id = $1, booked_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
+ SET qbo_journal_entry_id = $1, status = 'booked', booked_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
WHERE id = $2`,
[je.Id, periodId]
);
@@ -645,6 +647,22 @@ async function createTaxPaymentJE({
return { qbo_journal_entry_id: je.Id, qbo_sync_token: je.SyncToken };
}
+async function markTaxPaidExternal(periodId) {
+ const result = await pool.query(
+ `UPDATE sales_tax_periods
+ SET status = 'external', booked_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
+ WHERE id = $1 AND status = 'open'
+ RETURNING *`,
+ [periodId]
+ );
+ if (result.rows.length === 0) {
+ const existing = await pool.query('SELECT status FROM sales_tax_periods WHERE id = $1', [periodId]);
+ const currentStatus = existing.rows[0]?.status || 'unknown';
+ throw new Error(`Cannot mark as paid: period is already ${currentStatus}`);
+ }
+ return result.rows[0];
+}
+
// ════════════════════════════════════════════════════════════════════
// Phase 2 Lieferung 1 — Caches und Sync
// ════════════════════════════════════════════════════════════════════
@@ -1700,6 +1718,7 @@ module.exports = {
getTaxPeriods,
upsertTaxPeriod,
createTaxPaymentJE,
+ markTaxPaidExternal,
normalizeTransactionListReport,
// Phase 2 Lieferung 1 — Sync
|