mirror of
https://github.com/mokuappio/serverless-invoices.git
synced 2025-10-27 16:01:07 -04:00
Added multiple taxes for invoice rows. Client has_vat => has_tax.
Abstracted add row button to separate component. Abstracted invoice row headers to separate component. Remove vat related things, now replaced with custom taxes. Invoice tax totals are calculated per tax based on invoice rows.
This commit is contained in:
7
package-lock.json
generated
7
package-lock.json
generated
@ -7378,10 +7378,9 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lodash": {
|
"lodash": {
|
||||||
"version": "4.17.20",
|
"version": "4.17.21",
|
||||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz",
|
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
|
||||||
"integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==",
|
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
|
||||||
"dev": true
|
|
||||||
},
|
},
|
||||||
"lodash.defaultsdeep": {
|
"lodash.defaultsdeep": {
|
||||||
"version": "4.6.1",
|
"version": "4.6.1",
|
||||||
|
|||||||
@ -16,6 +16,7 @@
|
|||||||
"dayjs": "^1.10.3",
|
"dayjs": "^1.10.3",
|
||||||
"es6-promise": "^4.2.6",
|
"es6-promise": "^4.2.6",
|
||||||
"localforage": "^1.9.0",
|
"localforage": "^1.9.0",
|
||||||
|
"lodash": "^4.17.21",
|
||||||
"vue": "^2.6.10",
|
"vue": "^2.6.10",
|
||||||
"vue-autosuggest": "^2.2.0",
|
"vue-autosuggest": "^2.2.0",
|
||||||
"vue-multiselect": "^2.1.6",
|
"vue-multiselect": "^2.1.6",
|
||||||
|
|||||||
@ -41,8 +41,8 @@
|
|||||||
label="Currency" field="currency" :errors="errors" class="col-sm-4"/>
|
label="Currency" field="currency" :errors="errors" class="col-sm-4"/>
|
||||||
<AppInput :value="client.rate" @change="updateProp({ rate: $event })"
|
<AppInput :value="client.rate" @change="updateProp({ rate: $event })"
|
||||||
label="Hourly rate" field="rate" :errors="errors" class="col-sm-4"/>
|
label="Hourly rate" field="rate" :errors="errors" class="col-sm-4"/>
|
||||||
<AppCheckbox :value="client.has_vat" @input="updateProp({ has_vat: $event })"
|
<AppCheckbox :value="client.has_tax" @input="updateProp({ has_tax: $event })"
|
||||||
label="Apply VAT" field="has_vat" :errors="errors" class="col-sm-4"/>
|
label="Apply taxes" field="has_tax" :errors="errors" class="col-sm-4"/>
|
||||||
<AppSelect :value="client.bank_account"
|
<AppSelect :value="client.bank_account"
|
||||||
track-by="id"
|
track-by="id"
|
||||||
label="Bank account"
|
label="Bank account"
|
||||||
|
|||||||
34
src/components/invoices/InvoiceAddRowBtn.vue
Normal file
34
src/components/invoices/InvoiceAddRowBtn.vue
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<tr class="d-print-none">
|
||||||
|
<td :colspan="colspan">
|
||||||
|
<button class="btn btn-sm" @click="addRow">
|
||||||
|
<i class="material-icons md-18 pointer">add</i>
|
||||||
|
</button>
|
||||||
|
<AppError :errors="errors" field="rows"/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
<script>
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
import AppError from '@/components/form/AppError';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['invoice', 'errors'],
|
||||||
|
components: {
|
||||||
|
AppError,
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
taxes: 'invoiceRows/taxes',
|
||||||
|
}),
|
||||||
|
colspan() {
|
||||||
|
return 5 + this.taxes.length;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
addRow() {
|
||||||
|
this.$store.dispatch('invoiceRows/addRow', this.invoice.id);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -20,26 +20,11 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<table class="table">
|
<table class="table">
|
||||||
<thead>
|
<InvoiceRowsHeader :invoice="invoice"/>
|
||||||
<tr>
|
|
||||||
<th>Item</th>
|
|
||||||
<th>Quantity</th>
|
|
||||||
<th>Unit</th>
|
|
||||||
<th>Price</th>
|
|
||||||
<th class="text-right">Sum</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
<tbody>
|
||||||
<InvoiceRow v-for="(row, index) in invoice.rows" :errors="errors"
|
<InvoiceRow v-for="(row, index) in invoice.rows" :errors="errors"
|
||||||
:row="row" :index="index" :key="row.id"/>
|
:row="row" :index="index" :key="row.id"/>
|
||||||
<tr class="d-print-none">
|
<InvoiceAddRowBtn :invoice="invoice" :errors="errors"/>
|
||||||
<td colspan="5">
|
|
||||||
<button class="btn btn-sm" @click="addRow">
|
|
||||||
<i class="material-icons md-18 pointer">add</i>
|
|
||||||
</button>
|
|
||||||
<AppError :errors="errors" field="rows"/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
</tbody>
|
</tbody>
|
||||||
<InvoiceTotals :invoice="invoice" :errors="errors" @update="updateProp"/>
|
<InvoiceTotals :invoice="invoice" :errors="errors" @update="updateProp"/>
|
||||||
</table>
|
</table>
|
||||||
@ -64,11 +49,13 @@ import InvoiceContactDetails from '@/components/invoices/InvoiceContactDetails';
|
|||||||
import InvoiceHeader from '@/components/invoices/InvoiceHeader';
|
import InvoiceHeader from '@/components/invoices/InvoiceHeader';
|
||||||
import InvoiceTotals from '@/components/invoices/InvoiceTotals';
|
import InvoiceTotals from '@/components/invoices/InvoiceTotals';
|
||||||
import AppEditable from '@/components/form/AppEditable';
|
import AppEditable from '@/components/form/AppEditable';
|
||||||
import AppError from '@/components/form/AppError';
|
|
||||||
import TeamLogo from '@/components/team/TeamLogo';
|
import TeamLogo from '@/components/team/TeamLogo';
|
||||||
|
import InvoiceRowsHeader from '@/components/invoices/InvoiceRowsHeader';
|
||||||
|
import InvoiceAddRowBtn from '@/components/invoices/InvoiceAddRowBtn';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: {
|
components: {
|
||||||
|
InvoiceAddRowBtn,
|
||||||
TeamLogo,
|
TeamLogo,
|
||||||
InvoiceTotals,
|
InvoiceTotals,
|
||||||
InvoiceHeader,
|
InvoiceHeader,
|
||||||
@ -76,9 +63,9 @@ export default {
|
|||||||
InvoiceBankDetails,
|
InvoiceBankDetails,
|
||||||
InvoiceCompanyDetails,
|
InvoiceCompanyDetails,
|
||||||
InvoiceRow,
|
InvoiceRow,
|
||||||
|
InvoiceRowsHeader,
|
||||||
InvoiceClientDetails,
|
InvoiceClientDetails,
|
||||||
AppEditable,
|
AppEditable,
|
||||||
AppError,
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapState({
|
...mapState({
|
||||||
@ -106,9 +93,6 @@ export default {
|
|||||||
invoiceId: this.invoice.id,
|
invoiceId: this.invoice.id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
addRow() {
|
|
||||||
this.$store.dispatch('invoiceRows/addRow', this.invoice.id);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -28,6 +28,14 @@
|
|||||||
placeholder="Enter price"
|
placeholder="Enter price"
|
||||||
@change="updateProp({ price: $event })"/>
|
@change="updateProp({ price: $event })"/>
|
||||||
</td>
|
</td>
|
||||||
|
<td v-for="(tax, taxIndex) in row.taxes" :title="tax.label">
|
||||||
|
<AppEditable v-if="tax.row_id"
|
||||||
|
:value="tax.value | currency"
|
||||||
|
:errors="errors"
|
||||||
|
:field="`rows.${index}.taxes.${taxIndex}.value`"
|
||||||
|
placeholder="Enter tax"
|
||||||
|
@change="updateTaxProp({ value: $event }, tax)"/>
|
||||||
|
</td>
|
||||||
<td class="text-right position-relative">
|
<td class="text-right position-relative">
|
||||||
{{ (row.quantity * row.price) | currency }}
|
{{ (row.quantity * row.price) | currency }}
|
||||||
<button class="btn btn-sm remove-invoice-row d-print-none" @click="removeRow(row)">
|
<button class="btn btn-sm remove-invoice-row d-print-none" @click="removeRow(row)">
|
||||||
@ -58,6 +66,13 @@ export default {
|
|||||||
invoiceId: this.row.invoice_id,
|
invoiceId: this.row.invoice_id,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
updateTaxProp(props, tax) {
|
||||||
|
this.$store.dispatch('invoiceRows/updateInvoiceRowTax', {
|
||||||
|
props,
|
||||||
|
invoiceId: this.row.invoice_id,
|
||||||
|
taxId: tax.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
async removeRow(row) {
|
async removeRow(row) {
|
||||||
await this.$store.dispatch('invoiceRows/removeRow', row.id);
|
await this.$store.dispatch('invoiceRows/removeRow', row.id);
|
||||||
this.updateProp();
|
this.updateProp();
|
||||||
|
|||||||
27
src/components/invoices/InvoiceRowsHeader.vue
Normal file
27
src/components/invoices/InvoiceRowsHeader.vue
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
<template>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Item</th>
|
||||||
|
<th>Quantity</th>
|
||||||
|
<th>Unit</th>
|
||||||
|
<th>Price</th>
|
||||||
|
<th v-for="tax in taxes" :key="tax.id">
|
||||||
|
{{ tax.label }} %
|
||||||
|
</th>
|
||||||
|
<th class="text-right">Sum</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
|
||||||
|
import { mapGetters } from 'vuex';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
taxes: 'invoiceRows/taxes',
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
@ -1,22 +1,18 @@
|
|||||||
<template>
|
<template>
|
||||||
<tfoot>
|
<tfoot>
|
||||||
<tr class="text-right">
|
<tr class="text-right">
|
||||||
<td colspan="4">Subtotal</td>
|
<td :colspan="colspan">Subtotal</td>
|
||||||
<td>{{ invoice.subTotal | currency }}</td>
|
<td>{{ invoice.subTotal | currency }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="text-right">
|
<tr class="text-right" v-for="tax in invoice.taxes" :key="tax.label">
|
||||||
<td colspan="4">
|
<td :colspan="colspan">
|
||||||
VAT
|
{{ tax.label }}
|
||||||
(<AppEditable :value="invoice.vat_rate | currency"
|
<!--({{ tax.rate | currency }}%)-->
|
||||||
suffix="%"
|
|
||||||
placeholder="Add VAT"
|
|
||||||
@change="updateProp({ vat_rate: $event })"/>)
|
|
||||||
<AppError :errors="errors" field="vat_rate"/>
|
|
||||||
</td>
|
</td>
|
||||||
<td>{{ invoice.totalVat | currency }}</td>
|
<td>{{ tax.total | currency }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr class="text-right">
|
<tr class="text-right">
|
||||||
<th colspan="4">
|
<th :colspan="colspan">
|
||||||
Total
|
Total
|
||||||
<AppEditable :value="invoice.currency"
|
<AppEditable :value="invoice.currency"
|
||||||
:errors="errors"
|
:errors="errors"
|
||||||
@ -29,7 +25,7 @@
|
|||||||
</tfoot>
|
</tfoot>
|
||||||
</template>
|
</template>
|
||||||
<script>
|
<script>
|
||||||
import AppError from '@/components/form/AppError';
|
import { mapGetters } from 'vuex';
|
||||||
import AppEditable from '../form/AppEditable';
|
import AppEditable from '../form/AppEditable';
|
||||||
import { formatDate } from '../../filters/date.filter';
|
import { formatDate } from '../../filters/date.filter';
|
||||||
import { formatCurrency } from '../../filters/currency.filter';
|
import { formatCurrency } from '../../filters/currency.filter';
|
||||||
@ -38,12 +34,19 @@ export default {
|
|||||||
props: ['invoice', 'errors'],
|
props: ['invoice', 'errors'],
|
||||||
components: {
|
components: {
|
||||||
AppEditable,
|
AppEditable,
|
||||||
AppError,
|
|
||||||
},
|
},
|
||||||
filters: {
|
filters: {
|
||||||
date: formatDate,
|
date: formatDate,
|
||||||
currency: formatCurrency,
|
currency: formatCurrency,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
...mapGetters({
|
||||||
|
taxes: 'invoiceRows/taxes',
|
||||||
|
}),
|
||||||
|
colspan() {
|
||||||
|
return 4 + this.taxes.length;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateProp(props) {
|
updateProp(props) {
|
||||||
this.$emit('update', props);
|
this.$emit('update', props);
|
||||||
|
|||||||
@ -20,8 +20,8 @@
|
|||||||
<td>{{ invoice.client ? invoice.client.company_name : '' }}</td>
|
<td>{{ invoice.client ? invoice.client.company_name : '' }}</td>
|
||||||
<td>{{ invoice.issued_at | date('D MMM YYYY', 'YYYY-MM-DD') }}</td>
|
<td>{{ invoice.issued_at | date('D MMM YYYY', 'YYYY-MM-DD') }}</td>
|
||||||
<td>
|
<td>
|
||||||
{{ invoice.total | currency }}
|
{{ invoice.subTotal | currency }}
|
||||||
<small v-if="invoice.vat_rate"><br>({{ totalWithVat(invoice) | currency }})</small>
|
<small v-if="invoice.taxTotal"><br>({{ invoice.total | currency }})</small>
|
||||||
</td>
|
</td>
|
||||||
<td class="text-right text-capitalize">
|
<td class="text-right text-capitalize">
|
||||||
<i class="material-icons material-icons-round md-18 mr-2 text-warning"
|
<i class="material-icons material-icons-round md-18 mr-2 text-warning"
|
||||||
@ -72,9 +72,6 @@ export default {
|
|||||||
params: { id: invoice.id },
|
params: { id: invoice.id },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
totalWithVat(invoice) {
|
|
||||||
return (invoice.vat_rate / 100 * invoice.total) + invoice.total;
|
|
||||||
},
|
|
||||||
isOverDue(invoice) {
|
isOverDue(invoice) {
|
||||||
return invoice.status === 'sent' && invoice.due_at < dayjs()
|
return invoice.status === 'sent' && invoice.due_at < dayjs()
|
||||||
.format();
|
.format();
|
||||||
|
|||||||
@ -32,8 +32,6 @@
|
|||||||
|
|
||||||
<b-tab title="Invoicing" class="col-12">
|
<b-tab title="Invoicing" class="col-12">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<AppInput :value="team.vat_rate" @change="updateProp({ vat_rate: $event })" type="number"
|
|
||||||
label="VAT rate" field="vat_rate" :errors="errors" class="col-sm-4"/>
|
|
||||||
<AppInput :value="team.invoice_late_fee" @change="updateProp({ invoice_late_fee: $event })"
|
<AppInput :value="team.invoice_late_fee" @change="updateProp({ invoice_late_fee: $event })"
|
||||||
type="number"
|
type="number"
|
||||||
label="Late fee (%)" field="invoice_late_fee" :errors="errors" class="col-sm-4"/>
|
label="Late fee (%)" field="invoice_late_fee" :errors="errors" class="col-sm-4"/>
|
||||||
|
|||||||
@ -22,7 +22,6 @@ class InvoiceService {
|
|||||||
async updateInvoice(invoice) {
|
async updateInvoice(invoice) {
|
||||||
const requiredFields = {
|
const requiredFields = {
|
||||||
currency: 'Currency',
|
currency: 'Currency',
|
||||||
vat_rate: 'Vat Rate',
|
|
||||||
late_fee: 'Late Fee',
|
late_fee: 'Late Fee',
|
||||||
issued_at: 'Issued At',
|
issued_at: 'Issued At',
|
||||||
due_at: 'Due At',
|
due_at: 'Due At',
|
||||||
@ -47,7 +46,6 @@ class InvoiceService {
|
|||||||
async bookInvoice(invoice) {
|
async bookInvoice(invoice) {
|
||||||
const requiredFields = {
|
const requiredFields = {
|
||||||
currency: 'Currency',
|
currency: 'Currency',
|
||||||
vat_rate: 'Vat rate',
|
|
||||||
late_fee: 'Late fee',
|
late_fee: 'Late fee',
|
||||||
issued_at: 'Issued at',
|
issued_at: 'Issued at',
|
||||||
due_at: 'Due at',
|
due_at: 'Due at',
|
||||||
|
|||||||
@ -14,7 +14,6 @@ class TeamService {
|
|||||||
website: null,
|
website: null,
|
||||||
contact_email: null,
|
contact_email: null,
|
||||||
contact_phone: null,
|
contact_phone: null,
|
||||||
vat_rate: null,
|
|
||||||
invoice_late_fee: null,
|
invoice_late_fee: null,
|
||||||
invoice_due_days: null,
|
invoice_due_days: null,
|
||||||
updated_at: null,
|
updated_at: null,
|
||||||
|
|||||||
@ -56,7 +56,7 @@ export default {
|
|||||||
},
|
},
|
||||||
async updateClient({ dispatch }, payload) {
|
async updateClient({ dispatch }, payload) {
|
||||||
if (payload.props) {
|
if (payload.props) {
|
||||||
await dispatch('clientProps', payload.props);
|
await dispatch('clientProps', payload);
|
||||||
}
|
}
|
||||||
return ClientService.updateClient(getClientById(payload.clientId));
|
return ClientService.updateClient(getClientById(payload.clientId));
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,14 +1,16 @@
|
|||||||
import InvoiceRow from '@/store/models/invoice-row';
|
import InvoiceRow from '@/store/models/invoice-row';
|
||||||
|
import InvoiceRowTax from '@/store/models/invoice-row-tax';
|
||||||
|
import { flatten, uniqBy } from 'lodash';
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
namespaced: true,
|
namespaced: true,
|
||||||
state: {
|
state: {},
|
||||||
},
|
mutations: {},
|
||||||
mutations: {
|
|
||||||
},
|
|
||||||
actions: {
|
actions: {
|
||||||
init() {},
|
init() {
|
||||||
terminate() {},
|
},
|
||||||
|
terminate() {
|
||||||
|
},
|
||||||
invoiceRowProps(store, payload) {
|
invoiceRowProps(store, payload) {
|
||||||
return InvoiceRow.update({
|
return InvoiceRow.update({
|
||||||
where: payload.id,
|
where: payload.id,
|
||||||
@ -21,16 +23,48 @@ export default {
|
|||||||
invoiceId: payload.invoiceId,
|
invoiceId: payload.invoiceId,
|
||||||
}, { root: true });
|
}, { root: true });
|
||||||
},
|
},
|
||||||
async addRow(store, invoiceId) {
|
async addRow({ getters, rootGetters }, invoiceId) {
|
||||||
const row = await InvoiceRow.createNew();
|
const row = await InvoiceRow.createNew();
|
||||||
const rowCount = InvoiceRow.query().where('invoice_id', invoiceId).count();
|
const rowCount = InvoiceRow.query().where('invoice_id', invoiceId).count();
|
||||||
row.$update({
|
await row.$update({
|
||||||
invoice_id: invoiceId,
|
invoice_id: invoiceId,
|
||||||
order: rowCount,
|
order: rowCount,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const client = rootGetters['invoices/invoice'].client;
|
||||||
|
if (client && client.has_tax) {
|
||||||
|
const taxes = getters.taxes.length > 0
|
||||||
|
? getters.taxes
|
||||||
|
: rootGetters['taxes/all'];
|
||||||
|
taxes.forEach((tax) => {
|
||||||
|
const rowTax = new InvoiceRowTax();
|
||||||
|
rowTax.label = tax.label;
|
||||||
|
rowTax.value = tax.value;
|
||||||
|
rowTax.row_id = row.id;
|
||||||
|
rowTax.$save();
|
||||||
|
});
|
||||||
|
}
|
||||||
},
|
},
|
||||||
async removeRow(store, rowId) {
|
async removeRow(store, rowId) {
|
||||||
await InvoiceRow.delete(rowId);
|
await InvoiceRow.delete(rowId);
|
||||||
},
|
},
|
||||||
|
async updateInvoiceRowTax({ dispatch }, payload) {
|
||||||
|
await InvoiceRowTax.update({
|
||||||
|
where: payload.taxId,
|
||||||
|
data: payload.props,
|
||||||
|
});
|
||||||
|
return dispatch('invoices/updateInvoice', {
|
||||||
|
invoiceId: payload.invoiceId,
|
||||||
|
}, { root: true });
|
||||||
|
},
|
||||||
|
},
|
||||||
|
getters: {
|
||||||
|
taxes(state, getters, rootState, rootGetters) {
|
||||||
|
let taxes = rootGetters['invoices/invoice'].rows.map(row => row.taxes);
|
||||||
|
taxes = flatten(taxes);
|
||||||
|
taxes = uniqBy(taxes, 'label');
|
||||||
|
taxes = taxes.filter(tax => !!tax.label);
|
||||||
|
return taxes;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|||||||
@ -6,7 +6,7 @@ import Errors from '@/utils/errors';
|
|||||||
|
|
||||||
function getInvoice(invoiceId) {
|
function getInvoice(invoiceId) {
|
||||||
return Invoice.query()
|
return Invoice.query()
|
||||||
.with(['client', 'client_fields', 'team_fields'])
|
.with(['client', 'client_fields', 'team_fields', 'rows.taxes'])
|
||||||
.with('rows', query => query.orderBy('order', 'asc'))
|
.with('rows', query => query.orderBy('order', 'asc'))
|
||||||
.find(invoiceId);
|
.find(invoiceId);
|
||||||
}
|
}
|
||||||
@ -76,9 +76,6 @@ export default {
|
|||||||
client_email: 'invoice_email',
|
client_email: 'invoice_email',
|
||||||
currency: 'currency',
|
currency: 'currency',
|
||||||
});
|
});
|
||||||
if ('vat_rate' in payload.props) {
|
|
||||||
clientProps.has_vat = payload.props.vat_rate > 0;
|
|
||||||
}
|
|
||||||
const invoice = getInvoice(payload.invoiceId);
|
const invoice = getInvoice(payload.invoiceId);
|
||||||
|
|
||||||
if (Object.keys(clientProps).length > 0 && invoice.client_id) {
|
if (Object.keys(clientProps).length > 0 && invoice.client_id) {
|
||||||
@ -100,7 +97,6 @@ export default {
|
|||||||
from_website: 'website',
|
from_website: 'website',
|
||||||
from_email: 'contact_email',
|
from_email: 'contact_email',
|
||||||
from_phone: 'contact_phone',
|
from_phone: 'contact_phone',
|
||||||
vat_rate: 'vat_rate',
|
|
||||||
});
|
});
|
||||||
const invoice = getInvoice(payload.invoiceId);
|
const invoice = getInvoice(payload.invoiceId);
|
||||||
|
|
||||||
@ -108,13 +104,6 @@ export default {
|
|||||||
teamProps.invoice_due_days = dayjs(invoice.due_at)
|
teamProps.invoice_due_days = dayjs(invoice.due_at)
|
||||||
.diff(invoice.issued_at, 'days');
|
.diff(invoice.issued_at, 'days');
|
||||||
}
|
}
|
||||||
if ('vat_rate' in payload.props) {
|
|
||||||
// You can only set VAT to 0 if setting it directly under settings
|
|
||||||
// This is to avoid setting general VAT to 0, if only changing per invoice
|
|
||||||
if (parseFloat(teamProps.vat_rate) === 0) {
|
|
||||||
delete teamProps.vat_rate;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Object.keys(teamProps).length > 0) {
|
if (Object.keys(teamProps).length > 0) {
|
||||||
dispatch('teams/updateTeam', teamProps, { root: true });
|
dispatch('teams/updateTeam', teamProps, { root: true });
|
||||||
@ -173,7 +162,6 @@ export default {
|
|||||||
client_country: client.company_country,
|
client_country: client.company_country,
|
||||||
client_email: client.invoice_email,
|
client_email: client.invoice_email,
|
||||||
currency: client.currency || rootGetters['teams/team'].currency || 'USD',
|
currency: client.currency || rootGetters['teams/team'].currency || 'USD',
|
||||||
vat_rate: client.has_vat ? rootGetters['teams/team'].vat_rate : 0,
|
|
||||||
bank_name: client.bank_account ? client.bank_account.bank_name : null,
|
bank_name: client.bank_account ? client.bank_account.bank_name : null,
|
||||||
bank_account_no: client.bank_account ? client.bank_account.account_no : null,
|
bank_account_no: client.bank_account ? client.bank_account.account_no : null,
|
||||||
},
|
},
|
||||||
@ -213,7 +201,6 @@ export default {
|
|||||||
from_website: team.website,
|
from_website: team.website,
|
||||||
from_email: team.contact_email,
|
from_email: team.contact_email,
|
||||||
from_phone: team.contact_phone,
|
from_phone: team.contact_phone,
|
||||||
vat_rate: team.vat_rate || 0,
|
|
||||||
currency: team.currency || 'USD',
|
currency: team.currency || 'USD',
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@ -226,7 +213,7 @@ export default {
|
|||||||
all() {
|
all() {
|
||||||
return Invoice.query()
|
return Invoice.query()
|
||||||
.where('$isNew', false)
|
.where('$isNew', false)
|
||||||
.with(['client'])
|
.with(['client', 'rows.taxes'])
|
||||||
.with('rows', query => query.orderBy('order', 'asc')) // TODO: do we need this?
|
.with('rows', query => query.orderBy('order', 'asc')) // TODO: do we need this?
|
||||||
.orderBy('issued_at', 'desc')
|
.orderBy('issued_at', 'desc')
|
||||||
.orderBy('number', 'desc')
|
.orderBy('number', 'desc')
|
||||||
|
|||||||
@ -16,7 +16,7 @@ export default class Client extends Model {
|
|||||||
company_country: this.attr(''),
|
company_country: this.attr(''),
|
||||||
company_county: this.attr(''),
|
company_county: this.attr(''),
|
||||||
company_city: this.attr(''),
|
company_city: this.attr(''),
|
||||||
has_vat: this.attr(null),
|
has_tax: this.attr(true),
|
||||||
currency: this.attr(null),
|
currency: this.attr(null),
|
||||||
rate: this.attr(null),
|
rate: this.attr(null),
|
||||||
invoice_email: this.attr(''),
|
invoice_email: this.attr(''),
|
||||||
|
|||||||
16
src/store/models/invoice-row-tax.js
Normal file
16
src/store/models/invoice-row-tax.js
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { Model } from '@vuex-orm/core';
|
||||||
|
import { uuidv4 } from '@/utils/helpers';
|
||||||
|
|
||||||
|
export default class InvoiceRowTax extends Model {
|
||||||
|
// This is the name used as module name of the Vuex Store.
|
||||||
|
static entity = 'invoice_row_taxes';
|
||||||
|
|
||||||
|
static fields() {
|
||||||
|
return {
|
||||||
|
id: this.attr(() => uuidv4()),
|
||||||
|
row_id: this.attr(null),
|
||||||
|
label: this.attr(''),
|
||||||
|
value: this.attr(''),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,6 +1,7 @@
|
|||||||
import { Model } from '@vuex-orm/core';
|
import { Model } from '@vuex-orm/core';
|
||||||
import { uuidv4 } from '@/utils/helpers';
|
import { uuidv4 } from '@/utils/helpers';
|
||||||
import Invoice from '@/store/models/invoice';
|
import Invoice from '@/store/models/invoice';
|
||||||
|
import InvoiceRowTax from '@/store/models/invoice-row-tax';
|
||||||
|
|
||||||
export default class InvoiceRow extends Model {
|
export default class InvoiceRow extends Model {
|
||||||
// This is the name used as module name of the Vuex Store.
|
// This is the name used as module name of the Vuex Store.
|
||||||
@ -16,6 +17,7 @@ export default class InvoiceRow extends Model {
|
|||||||
price: this.attr(null),
|
price: this.attr(null),
|
||||||
unit: this.attr(''),
|
unit: this.attr(''),
|
||||||
order: this.attr(null),
|
order: this.attr(null),
|
||||||
|
taxes: this.hasMany(InvoiceRowTax, 'row_id'),
|
||||||
updated_at: this.attr(''),
|
updated_at: this.attr(''),
|
||||||
created_at: this.attr(''),
|
created_at: this.attr(''),
|
||||||
};
|
};
|
||||||
|
|||||||
@ -17,7 +17,6 @@ export default class Invoice extends Model {
|
|||||||
issued_at: this.attr(''),
|
issued_at: this.attr(''),
|
||||||
due_at: this.attr(''),
|
due_at: this.attr(''),
|
||||||
late_fee: this.attr(''),
|
late_fee: this.attr(''),
|
||||||
vat_rate: this.attr(''),
|
|
||||||
currency: this.attr(''),
|
currency: this.attr(''),
|
||||||
from_name: this.attr(''),
|
from_name: this.attr(''),
|
||||||
from_address: this.attr(''),
|
from_address: this.attr(''),
|
||||||
@ -57,16 +56,36 @@ export default class Invoice extends Model {
|
|||||||
set subTotal(val) {}
|
set subTotal(val) {}
|
||||||
|
|
||||||
get total() {
|
get total() {
|
||||||
return this.subTotal + this.totalVat;
|
return this.subTotal + this.taxTotal;
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty-function
|
// eslint-disable-next-line no-empty-function
|
||||||
set total(val) {}
|
set total(val) {}
|
||||||
|
|
||||||
get totalVat() {
|
get taxTotal() {
|
||||||
return (this.vat_rate / 100) * this.subTotal;
|
return Object.values(this.taxes).reduce((carr, tax) => (tax.total + carr), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line no-empty-function
|
// eslint-disable-next-line no-empty-function
|
||||||
set totalVat(val) {}
|
set taxTotal(val) {}
|
||||||
|
|
||||||
|
get taxes() {
|
||||||
|
const taxes = {};
|
||||||
|
|
||||||
|
this.rows.forEach(row => row.taxes.forEach((tax) => {
|
||||||
|
if (!taxes.hasOwnProperty(tax.label)) {
|
||||||
|
taxes[tax.label] = {
|
||||||
|
total: 0,
|
||||||
|
label: tax.label,
|
||||||
|
rate: tax.value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
taxes[tax.label].total += (row.quantity * row.price) * tax.value / 100;
|
||||||
|
}));
|
||||||
|
|
||||||
|
return taxes;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line no-empty-function
|
||||||
|
set taxes(val) {}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -18,7 +18,6 @@ export default class Team extends Model {
|
|||||||
website: this.attr(''),
|
website: this.attr(''),
|
||||||
contact_email: this.attr(''),
|
contact_email: this.attr(''),
|
||||||
contact_phone: this.attr(''),
|
contact_phone: this.attr(''),
|
||||||
vat_rate: this.attr(null),
|
|
||||||
currency: this.attr(null),
|
currency: this.attr(null),
|
||||||
invoice_late_fee: this.attr(null),
|
invoice_late_fee: this.attr(null),
|
||||||
invoice_due_days: this.attr(null),
|
invoice_due_days: this.attr(null),
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import TeamField from '@/store/models/team-field';
|
|||||||
import InvoiceClientField from '@/store/models/invoice-client-field';
|
import InvoiceClientField from '@/store/models/invoice-client-field';
|
||||||
import InvoiceTeamField from '@/store/models/invoice-team-field';
|
import InvoiceTeamField from '@/store/models/invoice-team-field';
|
||||||
import Tax from '@/store/models/tax';
|
import Tax from '@/store/models/tax';
|
||||||
|
import InvoiceRowTax from '@/store/models/invoice-row-tax';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
|
|
||||||
@ -39,6 +40,7 @@ database.register(Invoice);
|
|||||||
database.register(InvoiceClientField);
|
database.register(InvoiceClientField);
|
||||||
database.register(InvoiceTeamField);
|
database.register(InvoiceTeamField);
|
||||||
database.register(InvoiceRow);
|
database.register(InvoiceRow);
|
||||||
|
database.register(InvoiceRowTax);
|
||||||
database.register(BankAccount);
|
database.register(BankAccount);
|
||||||
|
|
||||||
export default new Vuex.Store({
|
export default new Vuex.Store({
|
||||||
|
|||||||
Reference in New Issue
Block a user