remove sales tax
This commit is contained in:
1
migrations/rename-external-to-paid.sql
Normal file
1
migrations/rename-external-to-paid.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
UPDATE sales_tax_periods SET status = 'paid' WHERE status = 'external';
|
||||||
@@ -149,11 +149,10 @@ const API = {
|
|||||||
upsertTaxPeriod: (data) => fetch('/api/accounting/sales-tax/periods', {
|
upsertTaxPeriod: (data) => fetch('/api/accounting/sales-tax/periods', {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
||||||
}).then(r => r.json()),
|
}).then(r => r.json()),
|
||||||
recordTaxPayment: (periodId, data) => fetch(`/api/accounting/sales-tax/periods/${periodId}/record`, {
|
markTaxPaidExternal: (periodId, paidDate) => fetch(`/api/accounting/sales-tax/periods/${periodId}/mark-external`, {
|
||||||
method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data)
|
method: 'POST',
|
||||||
}).then(r => r.json()),
|
headers: { 'Content-Type': 'application/json' },
|
||||||
markTaxPaidExternal: (periodId) => fetch(`/api/accounting/sales-tax/periods/${periodId}/mark-external`, {
|
body: JSON.stringify({ paidDate })
|
||||||
method: 'POST'
|
|
||||||
}).then(r => r.json()),
|
}).then(r => r.json()),
|
||||||
|
|
||||||
// Customer Revenue Report
|
// Customer Revenue Report
|
||||||
|
|||||||
@@ -35,7 +35,6 @@ let crEndDate = null;
|
|||||||
|
|
||||||
let stPeriods = [];
|
let stPeriods = [];
|
||||||
let stEditingPeriodId = null; // current period in detail dialog, null = new
|
let stEditingPeriodId = null; // current period in detail dialog, null = new
|
||||||
let stAccounts = []; // cached account list for pickers
|
|
||||||
|
|
||||||
let expStartDate = null;
|
let expStartDate = null;
|
||||||
let expEndDate = null;
|
let expEndDate = null;
|
||||||
@@ -879,14 +878,15 @@ function renderTaxPeriodsTable() {
|
|||||||
const pStatus = p.status || (p.qbo_journal_entry_id ? 'booked' : 'open');
|
const pStatus = p.status || (p.qbo_journal_entry_id ? 'booked' : 'open');
|
||||||
let statusHtml, statusColor;
|
let statusHtml, statusColor;
|
||||||
if (pStatus === 'booked') { statusHtml = 'Booked'; statusColor = 'bg-green-100 text-green-800'; }
|
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 if (pStatus === 'external' || pStatus === 'paid') { statusHtml = 'Paid'; statusColor = 'bg-green-100 text-green-800'; }
|
||||||
else { statusHtml = 'Open'; statusColor = 'bg-yellow-100 text-yellow-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 monthLabel = new Date(p.period_start).toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||||
const adj = parseFloat(p.adjustment_amount) || 0;
|
const adj = parseFloat(p.adjustment_amount) || 0;
|
||||||
const adjStr = adj !== 0 ? (adj > 0 ? `−$${adj.toFixed(2)}` : `+$${Math.abs(adj).toFixed(2)}`) : '—';
|
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 netPaid = parseFloat(p.net_paid) || parseFloat(p.tax_collected) || 0;
|
||||||
const paidOn = pStatus === 'external' ? 'Paid in QBO' : (p.booked_at ? formatDate(p.booked_at) : '—');
|
const paidOn = (pStatus === 'external' || pStatus === 'paid' || pStatus === 'booked')
|
||||||
|
? (p.booked_at ? formatDate(p.booked_at) : '—') : '—';
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<tr class="border-t hover:bg-gray-50">
|
<tr class="border-t hover:bg-gray-50">
|
||||||
@@ -953,13 +953,6 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!stAccounts.length) {
|
|
||||||
try {
|
|
||||||
const accts = await window.API.accounting.getAccounts() || [];
|
|
||||||
stAccounts = Array.isArray(accts) ? accts : [];
|
|
||||||
} catch (e) { stAccounts = []; }
|
|
||||||
}
|
|
||||||
|
|
||||||
const reportHtml = taxData?.Rows ? renderQboReport(taxData) : `<p class="text-sm text-gray-500">No tax data for this period.</p>`;
|
const reportHtml = taxData?.Rows ? renderQboReport(taxData) : `<p class="text-sm text-gray-500">No tax data for this period.</p>`;
|
||||||
|
|
||||||
const parsed = parseTaxDataFromReport(taxData);
|
const parsed = parseTaxDataFromReport(taxData);
|
||||||
@@ -975,21 +968,6 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
|||||||
const isOpen = periodStatus === 'open';
|
const isOpen = periodStatus === 'open';
|
||||||
const isEditable = isOpen;
|
const isEditable = isOpen;
|
||||||
|
|
||||||
const bankOpts = stAccounts
|
|
||||||
.filter(a => a.accountType === 'Bank' || a.accountType === 'Credit Card')
|
|
||||||
.map(a => `<option value="${a.id}" ${a.id === existingPeriod?.bank_account_id ? 'selected' : ''}>${escapeHtml(a.name)} (${a.accountType})</option>`).join('');
|
|
||||||
const liabilityOpts = stAccounts
|
|
||||||
.filter(a => a.accountType === 'Other Current Liability')
|
|
||||||
.map(a => `<option value="${a.id}" ${a.id === existingPeriod?.sales_tax_payable_id ? 'selected' : ''}>${escapeHtml(a.name)}</option>`).join('');
|
|
||||||
const adjustOpts = stAccounts
|
|
||||||
.filter(a => a.accountType === 'Income' || a.accountType === 'Expense' || a.classification === 'Revenue' || a.classification === 'Expense')
|
|
||||||
.map(a => {
|
|
||||||
const sel = a.id === existingPeriod?.adjustment_account_id ? 'selected' : '';
|
|
||||||
const name = (a.fullyQualifiedName || a.name || '');
|
|
||||||
const isDiscount = name.toLowerCase().includes('discount');
|
|
||||||
return `<option value="${a.id}" ${sel} data-hint="${isDiscount ? 'discount' : ''}">${escapeHtml(name)}${isDiscount ? ' ★' : ''}</option>`;
|
|
||||||
}).join('');
|
|
||||||
|
|
||||||
const today = todayISO();
|
const today = todayISO();
|
||||||
const [y, m] = startDate.split('-').map(Number);
|
const [y, m] = startDate.split('-').map(Number);
|
||||||
const monthLabel = new Date(y, m - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
const monthLabel = new Date(y, m - 1).toLocaleDateString('en-US', { year: 'numeric', month: 'long' });
|
||||||
@@ -1010,71 +988,43 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
|||||||
<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>
|
||||||
|
|
||||||
|
${isEditable ? `
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Adjustment Amount</label>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Adjustment Amount</label>
|
||||||
<input type="number" step="0.01" id="st-adjustment" value="${adjustments !== 0 ? adjustments : ''}" placeholder="e.g. 6.15 (discount)" ${isEditable ? '' : 'disabled'}
|
<input type="number" step="0.01" id="st-adjustment" value="${adjustments !== 0 ? adjustments : ''}" placeholder="e.g. 6.15 (discount)"
|
||||||
oninput="window.accountingView.updateTaxPreview()"
|
oninput="window.accountingView.updateTaxPreview()"
|
||||||
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||||
<p class="text-xs text-gray-400 mt-0.5">Positive = discount (reduces net due)</p>
|
<p class="text-xs text-gray-400 mt-0.5">Positive = discount (reduces net due)</p>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Reason</label>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Reason</label>
|
||||||
<input type="text" id="st-adjustment-reason" value="${escapeHtml(adjReason)}" placeholder="e.g. Timely filing discount" ${isEditable ? '' : 'disabled'}
|
<input type="text" id="st-adjustment-reason" value="${escapeHtml(adjReason)}" placeholder="e.g. Timely filing discount"
|
||||||
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Adjustment Account</label>
|
<label class="block text-xs font-medium text-gray-700 mb-1">Paid Date (QBO)</label>
|
||||||
<select id="st-adjustment-account" class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm" ${isEditable ? '' : 'disabled'}>
|
<input type="date" id="st-paid-date" value="${today}"
|
||||||
<option value="">— Select income account —</option>
|
|
||||||
${adjustOpts}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="grid grid-cols-1 md:grid-cols-3 gap-3 mb-3">
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Bank Account (payment source)</label>
|
|
||||||
<select id="st-bank-account" class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm" ${isEditable ? '' : 'disabled'}>
|
|
||||||
<option value="">— Select bank account —</option>
|
|
||||||
${bankOpts}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Sales Tax Payable Account</label>
|
|
||||||
<select id="st-payable-account" class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm" ${isEditable ? '' : 'disabled'}>
|
|
||||||
<option value="">— Select liability account —</option>
|
|
||||||
${liabilityOpts}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<label class="block text-xs font-medium text-gray-700 mb-1">Payment Date</label>
|
|
||||||
<input type="date" id="st-payment-date" value="${today}" ${isEditable ? '' : 'disabled'}
|
|
||||||
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
class="w-full px-3 py-1.5 border border-gray-300 rounded-md text-sm">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
${isEditable ? `
|
|
||||||
<div class="flex items-center justify-between border-t pt-3 mt-1">
|
<div class="flex items-center justify-between border-t pt-3 mt-1">
|
||||||
<div id="st-je-lines">
|
<div id="st-je-lines">
|
||||||
<p class="text-sm"><strong>Journal Entry lines:</strong></p>
|
<p class="text-sm"><strong>Summary:</strong></p>
|
||||||
<p class="text-xs text-gray-500 font-mono">Debit Sales Tax Payable: ${fmtMoney(taxCollected)}</p>
|
<p class="text-xs text-gray-500">Tax Collected: ${fmtMoney(taxCollected)}</p>
|
||||||
${adjustments > 0 ? `<p class="text-xs text-gray-500 font-mono">Credit Discount: ${fmtMoney(adjustments)}</p>` : ''}
|
${adjustments > 0 ? `<p class="text-xs text-gray-500">Discount: \u2212${fmtMoney(adjustments)}</p>` : ''}
|
||||||
${adjustments < 0 ? `<p class="text-xs text-gray-500 font-mono">Debit Penalty: ${fmtMoney(Math.abs(adjustments))}</p>` : ''}
|
${adjustments < 0 ? `<p class="text-xs text-gray-500">Penalty: +$${Math.abs(adjustments).toFixed(2)}</p>` : ''}
|
||||||
<p class="text-xs text-gray-500 font-mono" id="st-je-bank">Credit Bank: ${fmtMoney(netPaid)}</p>
|
<p class="text-xs text-gray-500 font-semibold">Net Due: ${fmtMoney(netPaid)}</p>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<button onclick="window.accountingView.saveTaxPeriodDraft()"
|
<button onclick="window.accountingView.saveTaxPeriodDraft()"
|
||||||
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">
|
class="px-3 py-1.5 bg-gray-100 hover:bg-gray-200 text-gray-700 rounded-md text-sm font-medium border border-gray-300">
|
||||||
💾 Save Draft
|
💾 Save Draft
|
||||||
</button>
|
</button>
|
||||||
<button onclick="window.accountingView.markTaxPaidExternal()"
|
<button onclick="window.accountingView.markPeriodPaid()"
|
||||||
class="px-3 py-1.5 bg-blue-100 hover:bg-blue-200 text-blue-800 rounded-md text-sm font-medium border border-blue-300">
|
|
||||||
📋 Mark as already paid (external)
|
|
||||||
</button>
|
|
||||||
<button onclick="window.accountingView.recordTaxPayment()"
|
|
||||||
class="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-semibold hover:bg-green-700">
|
class="px-4 py-2 bg-green-600 text-white rounded-md text-sm font-semibold hover:bg-green-700">
|
||||||
✅ Record Payment in QBO
|
✅ Mark as Paid
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -1084,7 +1034,7 @@ async function openTaxPeriodDetail(startDate, endDate, existingPeriod) {
|
|||||||
</div>
|
</div>
|
||||||
` : `
|
` : `
|
||||||
<div class="border-t pt-3 mt-1">
|
<div class="border-t pt-3 mt-1">
|
||||||
<p class="text-sm text-blue-700 font-semibold">📋 Marked as paid externally on ${formatDate(existingPeriod.booked_at)}</p>
|
<p class="text-sm text-green-700 font-semibold">✅ Paid on ${formatDate(existingPeriod.booked_at)}</p>
|
||||||
</div>
|
</div>
|
||||||
`}
|
`}
|
||||||
</div>
|
</div>
|
||||||
@@ -1125,28 +1075,17 @@ export function updateTaxPreview() {
|
|||||||
const jeLines = document.getElementById('st-je-lines');
|
const jeLines = document.getElementById('st-je-lines');
|
||||||
if (jeLines) {
|
if (jeLines) {
|
||||||
jeLines.innerHTML = `
|
jeLines.innerHTML = `
|
||||||
<p class="text-sm"><strong>Journal Entry lines:</strong></p>
|
<p class="text-sm"><strong>Summary:</strong></p>
|
||||||
<p class="text-xs text-gray-500 font-mono">Debit Sales Tax Payable: ${fmtMoney(taxCollected)}</p>
|
<p class="text-xs text-gray-500">Tax Collected: ${fmtMoney(taxCollected)}</p>
|
||||||
${adj > 0 ? `<p class="text-xs text-gray-500 font-mono">Credit Discount: ${fmtMoney(adj)}</p>` : ''}
|
${adj > 0 ? `<p class="text-xs text-gray-500">Discount: \u2212${fmtMoney(adj)}</p>` : ''}
|
||||||
${adj < 0 ? `<p class="text-xs text-gray-500 font-mono">Debit Penalty: ${fmtMoney(Math.abs(adj))}</p>` : ''}
|
${adj < 0 ? `<p class="text-xs text-gray-500">Penalty: +$${Math.abs(adj).toFixed(2)}</p>` : ''}
|
||||||
<p class="text-xs text-gray-500 font-mono">Credit Bank: ${fmtMoney(netPaid)}</p>`;
|
<p class="text-xs text-gray-500 font-semibold">Net Due: ${fmtMoney(netPaid)}</p>`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function saveTaxPeriodDraft() {
|
export async function saveTaxPeriodDraft() {
|
||||||
const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0;
|
const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0;
|
||||||
const adjReason = document.getElementById('st-adjustment-reason').value.trim();
|
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 (!stCurrentTaxData) return alert('No tax data loaded.');
|
if (!stCurrentTaxData) return alert('No tax data loaded.');
|
||||||
|
|
||||||
@@ -1160,13 +1099,8 @@ export async function saveTaxPeriodDraft() {
|
|||||||
tax_collected: stCurrentTaxData.taxCollected,
|
tax_collected: stCurrentTaxData.taxCollected,
|
||||||
adjustment_amount: adjAmount,
|
adjustment_amount: adjAmount,
|
||||||
adjustment_reason: adjReason || null,
|
adjustment_reason: adjReason || null,
|
||||||
adjustment_account_id: adjAccountId || null,
|
|
||||||
adjustment_account_name: adjAccountName || null,
|
|
||||||
net_paid: stCurrentTaxData.taxCollected - adjAmount,
|
net_paid: stCurrentTaxData.taxCollected - adjAmount,
|
||||||
bank_account_id: bankId || null,
|
status: 'open'
|
||||||
bank_account_name: bankName || null,
|
|
||||||
sales_tax_payable_id: payableId || null,
|
|
||||||
sales_tax_payable_name: payableName || null
|
|
||||||
});
|
});
|
||||||
await loadTaxPeriods();
|
await loadTaxPeriods();
|
||||||
if (period?.period_start) {
|
if (period?.period_start) {
|
||||||
@@ -1177,116 +1111,18 @@ export async function saveTaxPeriodDraft() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recordTaxPayment() {
|
export async function markPeriodPaid() {
|
||||||
const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0;
|
const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0;
|
||||||
const adjReason = document.getElementById('st-adjustment-reason').value.trim();
|
const adjReason = document.getElementById('st-adjustment-reason').value.trim();
|
||||||
const adjAccountEl = document.getElementById('st-adjustment-account');
|
const paidDate = document.getElementById('st-paid-date').value;
|
||||||
const adjAccountId = adjAccountEl.value;
|
if (!paidDate) return alert('Please select a paid date.');
|
||||||
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 : '';
|
|
||||||
|
|
||||||
const txnDate = document.getElementById('st-payment-date').value;
|
|
||||||
if (!txnDate) return alert('Please select a payment date.');
|
|
||||||
|
|
||||||
if (!stCurrentTaxData) return alert('No tax data loaded.');
|
if (!stCurrentTaxData) return alert('No tax data loaded.');
|
||||||
|
|
||||||
const taxCollected = stCurrentTaxData.taxCollected;
|
|
||||||
const netPaid = taxCollected - adjAmount;
|
|
||||||
|
|
||||||
if (!payableId) return alert('Please select the Sales Tax Payable account.');
|
|
||||||
if (!bankId) return alert('Please select a bank account.');
|
|
||||||
|
|
||||||
const preview = `Journal Entry on ${txnDate}:\n\n` +
|
|
||||||
`Debit Sales Tax Payable: $${taxCollected.toFixed(2)}\n` +
|
|
||||||
(adjAmount > 0 ? `Credit Discount: $${adjAmount.toFixed(2)} (${adjReason || 'Adjustment'})\n` : '') +
|
|
||||||
(adjAmount < 0 ? `Debit Penalty: $${Math.abs(adjAmount).toFixed(2)} (${adjReason || 'Adjustment'})\n` : '') +
|
|
||||||
`Credit Bank: $${netPaid.toFixed(2)}\n\n` +
|
|
||||||
(adjAmount > 0 ? `Debits (${taxCollected.toFixed(2)}) = Credits (${adjAmount.toFixed(2)} + ${netPaid.toFixed(2)} = ${(adjAmount + netPaid).toFixed(2)})` :
|
|
||||||
adjAmount < 0 ? `Debits (${taxCollected.toFixed(2)} + ${Math.abs(adjAmount).toFixed(2)} = ${(taxCollected + Math.abs(adjAmount)).toFixed(2)}) = Credits (${netPaid.toFixed(2)})` :
|
|
||||||
`Debits = Credits = ${taxCollected.toFixed(2)}`) +
|
|
||||||
'\n\nRecord this in QBO?';
|
|
||||||
|
|
||||||
if (!confirm(preview)) return;
|
|
||||||
|
|
||||||
let periodId = stEditingPeriodId;
|
|
||||||
try {
|
|
||||||
const period = 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: netPaid,
|
|
||||||
bank_account_id: bankId || null,
|
|
||||||
bank_account_name: bankName || null,
|
|
||||||
sales_tax_payable_id: payableId || null,
|
|
||||||
sales_tax_payable_name: payableName || null
|
|
||||||
});
|
|
||||||
periodId = period.id;
|
|
||||||
} catch (e) {
|
|
||||||
return alert('Failed to save period before payment: ' + e.message);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof showSpinner === 'function') showSpinner('Recording tax payment in QBO…');
|
|
||||||
try {
|
|
||||||
const result = await window.API.accounting.recordTaxPayment(periodId, {
|
|
||||||
txnDate,
|
|
||||||
taxCollected,
|
|
||||||
adjustmentAmount: adjAmount,
|
|
||||||
netPaid,
|
|
||||||
salesTaxPayableId: payableId,
|
|
||||||
salesTaxPayableName: payableName,
|
|
||||||
adjustmentAccountId: adjAccountId || null,
|
|
||||||
adjustmentAccountName: adjAccountName || null,
|
|
||||||
adjustmentReason: adjReason || null,
|
|
||||||
bankAccountId: bankId,
|
|
||||||
bankAccountName: bankName
|
|
||||||
});
|
|
||||||
if (result.error) return alert('Failed: ' + result.error);
|
|
||||||
await loadTaxPeriods();
|
|
||||||
const updated = stPeriods.find(p => p.id === periodId);
|
|
||||||
if (updated) {
|
|
||||||
await openTaxPeriodDetail(updated.period_start, updated.period_end, updated);
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
alert('QBO payment failed: ' + e.message);
|
|
||||||
} finally {
|
|
||||||
if (typeof hideSpinner === 'function') hideSpinner();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function markTaxPaidExternal() {
|
|
||||||
if (!stEditingPeriodId) return alert('Please save the period first (Save Draft).');
|
if (!stEditingPeriodId) return alert('Please save the period first (Save Draft).');
|
||||||
|
|
||||||
const adjAmount = parseFloat(document.getElementById('st-adjustment').value) || 0;
|
const netPaid = stCurrentTaxData.taxCollected - adjAmount;
|
||||||
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');
|
if (!confirm(`Mark this period as paid?\n\nPaid Date: ${paidDate}\nTax Collected: $${stCurrentTaxData.taxCollected.toFixed(2)}\nAdjustment: $${adjAmount.toFixed(2)}\nNet Paid: $${netPaid.toFixed(2)}\n\nThis does NOT write to QBO — record the payment in QBO first.`)) return;
|
||||||
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 {
|
try {
|
||||||
await window.API.accounting.upsertTaxPeriod({
|
await window.API.accounting.upsertTaxPeriod({
|
||||||
@@ -1298,13 +1134,7 @@ export async function markTaxPaidExternal() {
|
|||||||
tax_collected: stCurrentTaxData.taxCollected,
|
tax_collected: stCurrentTaxData.taxCollected,
|
||||||
adjustment_amount: adjAmount,
|
adjustment_amount: adjAmount,
|
||||||
adjustment_reason: adjReason || null,
|
adjustment_reason: adjReason || null,
|
||||||
adjustment_account_id: adjAccountId || null,
|
net_paid: netPaid,
|
||||||
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'
|
status: 'open'
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -1312,7 +1142,7 @@ export async function markTaxPaidExternal() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await window.API.accounting.markTaxPaidExternal(stEditingPeriodId);
|
await window.API.accounting.markTaxPaidExternal(stEditingPeriodId, paidDate);
|
||||||
await loadTaxPeriods();
|
await loadTaxPeriods();
|
||||||
const updated = stPeriods.find(p => p.id === stEditingPeriodId);
|
const updated = stPeriods.find(p => p.id === stEditingPeriodId);
|
||||||
if (updated) {
|
if (updated) {
|
||||||
@@ -1413,8 +1243,7 @@ window.accountingView = {
|
|||||||
closeTaxPeriodDetail,
|
closeTaxPeriodDetail,
|
||||||
updateTaxPreview,
|
updateTaxPreview,
|
||||||
saveTaxPeriodDraft,
|
saveTaxPeriodDraft,
|
||||||
recordTaxPayment,
|
markPeriodPaid,
|
||||||
markTaxPaidExternal,
|
|
||||||
loadCustomerRevenue,
|
loadCustomerRevenue,
|
||||||
exportCustomerRevenuePdf,
|
exportCustomerRevenuePdf,
|
||||||
exportProfitLossPdf,
|
exportProfitLossPdf,
|
||||||
|
|||||||
@@ -114,18 +114,9 @@ router.post('/sales-tax/periods', async (req, res) => {
|
|||||||
} catch (err) { handleQboError(err, res, 'sales-tax-periods'); }
|
} catch (err) { handleQboError(err, res, 'sales-tax-periods'); }
|
||||||
});
|
});
|
||||||
|
|
||||||
router.post('/sales-tax/periods/:id/record', async (req, res) => {
|
|
||||||
try {
|
|
||||||
res.json(await accountingService.createTaxPaymentJE({
|
|
||||||
periodId: req.params.id,
|
|
||||||
...req.body
|
|
||||||
}));
|
|
||||||
} catch (err) { handleQboError(err, res, 'sales-tax-record'); }
|
|
||||||
});
|
|
||||||
|
|
||||||
router.post('/sales-tax/periods/:id/mark-external', async (req, res) => {
|
router.post('/sales-tax/periods/:id/mark-external', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
res.json(await accountingService.markTaxPaidExternal(req.params.id));
|
res.json(await accountingService.markTaxPaidExternal(req.params.id, req.body.paidDate));
|
||||||
} catch (err) { handleQboError(err, res, 'sales-tax-mark-external'); }
|
} catch (err) { handleQboError(err, res, 'sales-tax-mark-external'); }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -566,101 +566,13 @@ async function upsertTaxPeriod(period) {
|
|||||||
return result.rows[0];
|
return result.rows[0];
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createTaxPaymentJE({
|
async function markTaxPaidExternal(periodId, paidDate = null) {
|
||||||
periodId, txnDate, taxCollected, adjustmentAmount, netPaid,
|
|
||||||
salesTaxPayableId, salesTaxPayableName,
|
|
||||||
adjustmentAccountId, adjustmentAccountName, adjustmentReason,
|
|
||||||
bankAccountId, bankAccountName
|
|
||||||
}) {
|
|
||||||
const { companyId, baseUrl } = getClientInfo();
|
|
||||||
const url = withMinorVersion(`${baseUrl}/v3/company/${companyId}/journalentry`);
|
|
||||||
|
|
||||||
const lines = [
|
|
||||||
{
|
|
||||||
DetailType: 'JournalEntryLineDetail',
|
|
||||||
Amount: taxCollected,
|
|
||||||
JournalEntryLineDetail: {
|
|
||||||
PostingType: 'Debit',
|
|
||||||
AccountRef: { value: String(salesTaxPayableId), name: salesTaxPayableName }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
];
|
|
||||||
|
|
||||||
if (adjustmentAmount > 0) {
|
|
||||||
lines.push({
|
|
||||||
DetailType: 'JournalEntryLineDetail',
|
|
||||||
Amount: adjustmentAmount,
|
|
||||||
Description: adjustmentReason || undefined,
|
|
||||||
JournalEntryLineDetail: {
|
|
||||||
PostingType: 'Credit',
|
|
||||||
AccountRef: { value: String(adjustmentAccountId), name: adjustmentAccountName }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (adjustmentAmount < 0) {
|
|
||||||
lines.push({
|
|
||||||
DetailType: 'JournalEntryLineDetail',
|
|
||||||
Amount: Math.abs(adjustmentAmount),
|
|
||||||
Description: adjustmentReason || undefined,
|
|
||||||
JournalEntryLineDetail: {
|
|
||||||
PostingType: 'Debit',
|
|
||||||
AccountRef: { value: String(adjustmentAccountId), name: adjustmentAccountName }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
lines.push({
|
|
||||||
DetailType: 'JournalEntryLineDetail',
|
|
||||||
Amount: netPaid,
|
|
||||||
JournalEntryLineDetail: {
|
|
||||||
PostingType: 'Credit',
|
|
||||||
AccountRef: { value: String(bankAccountId), name: bankAccountName }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
const payload = {
|
|
||||||
TxnDate: txnDate,
|
|
||||||
DocNumber: `STP-${periodId}`,
|
|
||||||
Line: lines,
|
|
||||||
PrivateNote: adjustmentReason ? `Adjustment: ${adjustmentReason}` : undefined
|
|
||||||
};
|
|
||||||
|
|
||||||
let qboResponse;
|
|
||||||
try {
|
|
||||||
const response = await makeQboApiCall({
|
|
||||||
url,
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify(payload)
|
|
||||||
});
|
|
||||||
qboResponse = response;
|
|
||||||
} catch (e) {
|
|
||||||
const errMsg = e.originalMessage || e.message || String(e);
|
|
||||||
throw new Error(`QBO JournalEntry failed: ${errMsg}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = getJson(qboResponse);
|
|
||||||
throwIfFault(data, 'JournalEntry create');
|
|
||||||
|
|
||||||
const je = data.JournalEntry;
|
|
||||||
if (!je || !je.Id) throw new Error('QBO did not return a JournalEntry ID');
|
|
||||||
|
|
||||||
await pool.query(
|
|
||||||
`UPDATE sales_tax_periods
|
|
||||||
SET qbo_journal_entry_id = $1, status = 'booked', booked_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
|
||||||
WHERE id = $2`,
|
|
||||||
[je.Id, periodId]
|
|
||||||
);
|
|
||||||
|
|
||||||
return { qbo_journal_entry_id: je.Id, qbo_sync_token: je.SyncToken };
|
|
||||||
}
|
|
||||||
|
|
||||||
async function markTaxPaidExternal(periodId) {
|
|
||||||
const result = await pool.query(
|
const result = await pool.query(
|
||||||
`UPDATE sales_tax_periods
|
`UPDATE sales_tax_periods
|
||||||
SET status = 'external', booked_at = CURRENT_TIMESTAMP, updated_at = CURRENT_TIMESTAMP
|
SET status = 'paid', booked_at = $2, updated_at = CURRENT_TIMESTAMP
|
||||||
WHERE id = $1 AND status = 'open'
|
WHERE id = $1 AND status = 'open'
|
||||||
RETURNING *`,
|
RETURNING *`,
|
||||||
[periodId]
|
[periodId, paidDate || new Date().toISOString().split('T')[0]]
|
||||||
);
|
);
|
||||||
if (result.rows.length === 0) {
|
if (result.rows.length === 0) {
|
||||||
const existing = await pool.query('SELECT status FROM sales_tax_periods WHERE id = $1', [periodId]);
|
const existing = await pool.query('SELECT status FROM sales_tax_periods WHERE id = $1', [periodId]);
|
||||||
@@ -1724,7 +1636,6 @@ module.exports = {
|
|||||||
getTaxSummary,
|
getTaxSummary,
|
||||||
getTaxPeriods,
|
getTaxPeriods,
|
||||||
upsertTaxPeriod,
|
upsertTaxPeriod,
|
||||||
createTaxPaymentJE,
|
|
||||||
markTaxPaidExternal,
|
markTaxPaidExternal,
|
||||||
normalizeTransactionListReport,
|
normalizeTransactionListReport,
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user