229 lines
6.1 KiB
Vue
229 lines
6.1 KiB
Vue
<template>
|
|
<div>
|
|
<div class="flex justify-end mb-3">
|
|
<Button
|
|
icon-left="download"
|
|
class="shrink-0"
|
|
@click="$resources.downloadInvoiceAsCSV.submit"
|
|
>
|
|
<span class="text-sm">Download as CSV</span>
|
|
</Button>
|
|
</div>
|
|
<div v-if="pg" class="overflow-x-auto">
|
|
<table
|
|
class="text w-full border-separate border-spacing-y-2 text-base font-normal text-gray-900"
|
|
>
|
|
<thead class="bg-gray-100">
|
|
<tr class="text-gray-600">
|
|
<th class="rounded-l p-2 text-left font-normal">Description</th>
|
|
<th class="whitespace-nowrap p-2 text-right font-normal">Rate</th>
|
|
<th class="whitespace-nowrap p-2 text-right font-normal">
|
|
Quantity
|
|
</th>
|
|
<th class="rounded-r p-2 text-right font-normal">Amount</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody>
|
|
<template v-for="(items, type) in groupedLineItems" :key="type">
|
|
<tr class="mt-1 bg-gray-50">
|
|
<td colspan="100" class="rounded p-2 text-base font-medium">
|
|
{{ type }}
|
|
</td>
|
|
</tr>
|
|
<tr v-for="(row, i) in items" :key="row.idx">
|
|
<td class="py-1 pl-2 pr-2">
|
|
{{ row.document_name }}
|
|
<span v-if="row.plan" class="text-gray-700">
|
|
({{ formatPlan(row.plan) }})
|
|
</span>
|
|
</td>
|
|
<td class="py-1 pl-2 pr-2 text-right">
|
|
{{ formatCurrency(row.rate) }}
|
|
</td>
|
|
<td class="py-1 pl-2 pr-2 text-right">
|
|
{{ row.quantity }}
|
|
|
|
{{
|
|
[
|
|
'Site',
|
|
'Release Group',
|
|
'Server',
|
|
'Database Server',
|
|
].includes(row.document_type) && !row.plan.includes('hour')
|
|
? $format.plural(row.quantity, 'day', 'days')
|
|
: 'hours'
|
|
}}
|
|
</td>
|
|
<td class="py-1 pl-2 pr-2 text-right font-medium">
|
|
{{ formatCurrency(row.amount) }}
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tbody>
|
|
<tfoot>
|
|
<tr v-if="pg.total_discount_amount > 0">
|
|
<td></td>
|
|
<td></td>
|
|
<td class="pb-2 pr-2 pt-4 text-right font-medium">
|
|
Total Without Discount
|
|
</td>
|
|
<td class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-medium">
|
|
{{ formatCurrency(pg.total_before_discount) }}
|
|
</td>
|
|
</tr>
|
|
<tr v-if="pg.total_discount_amount > 0">
|
|
<td></td>
|
|
<td></td>
|
|
<td class="pb-2 pr-2 pt-4 text-right font-medium">
|
|
Total Discount Amount
|
|
</td>
|
|
<td class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-medium">
|
|
{{
|
|
$team.pg.erpnext_partner
|
|
? formatCurrency(pg.total_discount_amount)
|
|
: formatCurrency(0)
|
|
}}
|
|
</td>
|
|
</tr>
|
|
<tr v-if="pg.gst > 0">
|
|
<td></td>
|
|
<td></td>
|
|
<td class="pb-2 pr-2 pt-4 text-right font-medium">
|
|
Total (Without Tax)
|
|
</td>
|
|
<td class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-medium">
|
|
{{ formatCurrency(pg.total) }}
|
|
</td>
|
|
</tr>
|
|
<tr v-if="pg.gst > 0">
|
|
<td></td>
|
|
<td></td>
|
|
<td class="pb-2 pr-2 pt-4 text-right font-medium">
|
|
IGST @ {{ Number(gstPercentage * 100) }}%
|
|
</td>
|
|
<td class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-medium">
|
|
{{ pg.gst }}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td></td>
|
|
<td></td>
|
|
<td class="pb-2 pr-2 pt-4 text-right font-medium">Grand Total</td>
|
|
<td class="whitespace-nowrap pb-2 pr-2 pt-4 text-right font-medium">
|
|
{{ formatCurrency(pg.total + pg.gst) }}
|
|
</td>
|
|
</tr>
|
|
<template
|
|
v-if="
|
|
pg.total !== pg.amount_due &&
|
|
['Paid', 'Unpaid'].includes(pg.status)
|
|
"
|
|
>
|
|
<tr>
|
|
<td></td>
|
|
<td></td>
|
|
<td class="pr-2 text-right font-medium">Applied Balance</td>
|
|
<td class="whitespace-nowrap py-3 pr-2 text-right font-medium">
|
|
- {{ formatCurrency(pg.applied_credits) }}
|
|
</td>
|
|
</tr>
|
|
<tr>
|
|
<td></td>
|
|
<td></td>
|
|
<td class="pr-2 text-right font-medium">Amount Due</td>
|
|
<td class="whitespace-nowrap py-3 pr-2 text-right font-medium">
|
|
{{ formatCurrency(pg.amount_due) }}
|
|
</td>
|
|
</tr>
|
|
</template>
|
|
</tfoot>
|
|
</table>
|
|
</div>
|
|
<div class="py-20 text-center" v-if="$resources.invoice.loading">
|
|
<Button :loading="true">Loading</Button>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
<script>
|
|
import { getPlans } from '../data/plans';
|
|
|
|
export default {
|
|
name: 'InvoiceTable',
|
|
props: ['invoiceId'],
|
|
resources: {
|
|
invoice() {
|
|
return {
|
|
type: 'document',
|
|
doctype: 'Invoice',
|
|
name: this.invoiceId,
|
|
};
|
|
},
|
|
downloadInvoiceAsCSV() {
|
|
return {
|
|
url: 'jcloude.api.billing.fetch_invoice_items',
|
|
makeParams() {
|
|
return {
|
|
invoice: this.invoiceId,
|
|
};
|
|
},
|
|
onSuccess(data) {
|
|
const filename = `${this.invoiceId}.csv`;
|
|
this.downloadAsCSV(data, filename);
|
|
},
|
|
};
|
|
},
|
|
},
|
|
computed: {
|
|
groupedLineItems() {
|
|
if (!this.pg) return {};
|
|
const groupedLineItems = {};
|
|
for (let item of this.pg.items) {
|
|
groupedLineItems[item.document_type] =
|
|
groupedLineItems[item.document_type] || [];
|
|
groupedLineItems[item.document_type].push(item);
|
|
}
|
|
return groupedLineItems;
|
|
},
|
|
pg() {
|
|
return this.$resources.invoice.pg;
|
|
},
|
|
gstPercentage() {
|
|
return this.$team.pg.billing_info.gst_percentage;
|
|
},
|
|
},
|
|
methods: {
|
|
formatPlan(plan) {
|
|
let planDoc = getPlans().find((p) => p.name === plan);
|
|
if (planDoc) {
|
|
let india = this.$team.pg.currency === 'INR';
|
|
return this.$format.userCurrency(
|
|
india ? planDoc.price_inr : planDoc.price_usd,
|
|
);
|
|
}
|
|
return plan;
|
|
},
|
|
formatCurrency(value) {
|
|
if (!this.pg) return;
|
|
let currency = this.pg.currency;
|
|
return this.$format.currency(value, currency);
|
|
},
|
|
downloadAsCSV(data, filename) {
|
|
if (!data || data.length === 0) return;
|
|
let result = [];
|
|
result[0] = Object.keys(data[0]);
|
|
data.forEach((row) => {
|
|
result.push(Object.values(row));
|
|
});
|
|
const csv = result.map((row) => Object.values(row).join(',')).join('\n');
|
|
const blob = new Blob([csv], { type: 'text/csv' });
|
|
const url = window.URL.createObjectURL(blob);
|
|
const a = document.createElement('a');
|
|
a.href = url;
|
|
a.download = filename;
|
|
a.click();
|
|
window.URL.revokeObjectURL(url);
|
|
},
|
|
},
|
|
};
|
|
</script>
|