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:
HenriT
2021-04-14 15:58:55 +03:00
parent 8bfb088f30
commit 2e57464679
21 changed files with 196 additions and 82 deletions

View File

@ -56,7 +56,7 @@ export default {
},
async updateClient({ dispatch }, payload) {
if (payload.props) {
await dispatch('clientProps', payload.props);
await dispatch('clientProps', payload);
}
return ClientService.updateClient(getClientById(payload.clientId));
},

View File

@ -1,14 +1,16 @@
import InvoiceRow from '@/store/models/invoice-row';
import InvoiceRowTax from '@/store/models/invoice-row-tax';
import { flatten, uniqBy } from 'lodash';
export default {
namespaced: true,
state: {
},
mutations: {
},
state: {},
mutations: {},
actions: {
init() {},
terminate() {},
init() {
},
terminate() {
},
invoiceRowProps(store, payload) {
return InvoiceRow.update({
where: payload.id,
@ -21,16 +23,48 @@ export default {
invoiceId: payload.invoiceId,
}, { root: true });
},
async addRow(store, invoiceId) {
async addRow({ getters, rootGetters }, invoiceId) {
const row = await InvoiceRow.createNew();
const rowCount = InvoiceRow.query().where('invoice_id', invoiceId).count();
row.$update({
await row.$update({
invoice_id: invoiceId,
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) {
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;
},
},
};

View File

@ -6,7 +6,7 @@ import Errors from '@/utils/errors';
function getInvoice(invoiceId) {
return Invoice.query()
.with(['client', 'client_fields', 'team_fields'])
.with(['client', 'client_fields', 'team_fields', 'rows.taxes'])
.with('rows', query => query.orderBy('order', 'asc'))
.find(invoiceId);
}
@ -76,9 +76,6 @@ export default {
client_email: 'invoice_email',
currency: 'currency',
});
if ('vat_rate' in payload.props) {
clientProps.has_vat = payload.props.vat_rate > 0;
}
const invoice = getInvoice(payload.invoiceId);
if (Object.keys(clientProps).length > 0 && invoice.client_id) {
@ -100,7 +97,6 @@ export default {
from_website: 'website',
from_email: 'contact_email',
from_phone: 'contact_phone',
vat_rate: 'vat_rate',
});
const invoice = getInvoice(payload.invoiceId);
@ -108,13 +104,6 @@ export default {
teamProps.invoice_due_days = dayjs(invoice.due_at)
.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) {
dispatch('teams/updateTeam', teamProps, { root: true });
@ -173,7 +162,6 @@ export default {
client_country: client.company_country,
client_email: client.invoice_email,
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_account_no: client.bank_account ? client.bank_account.account_no : null,
},
@ -213,7 +201,6 @@ export default {
from_website: team.website,
from_email: team.contact_email,
from_phone: team.contact_phone,
vat_rate: team.vat_rate || 0,
currency: team.currency || 'USD',
},
});
@ -226,7 +213,7 @@ export default {
all() {
return Invoice.query()
.where('$isNew', false)
.with(['client'])
.with(['client', 'rows.taxes'])
.with('rows', query => query.orderBy('order', 'asc')) // TODO: do we need this?
.orderBy('issued_at', 'desc')
.orderBy('number', 'desc')

View File

@ -16,7 +16,7 @@ export default class Client extends Model {
company_country: this.attr(''),
company_county: this.attr(''),
company_city: this.attr(''),
has_vat: this.attr(null),
has_tax: this.attr(true),
currency: this.attr(null),
rate: this.attr(null),
invoice_email: this.attr(''),

View 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(''),
};
}
}

View File

@ -1,6 +1,7 @@
import { Model } from '@vuex-orm/core';
import { uuidv4 } from '@/utils/helpers';
import Invoice from '@/store/models/invoice';
import InvoiceRowTax from '@/store/models/invoice-row-tax';
export default class InvoiceRow extends Model {
// 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),
unit: this.attr(''),
order: this.attr(null),
taxes: this.hasMany(InvoiceRowTax, 'row_id'),
updated_at: this.attr(''),
created_at: this.attr(''),
};

View File

@ -17,7 +17,6 @@ export default class Invoice extends Model {
issued_at: this.attr(''),
due_at: this.attr(''),
late_fee: this.attr(''),
vat_rate: this.attr(''),
currency: this.attr(''),
from_name: this.attr(''),
from_address: this.attr(''),
@ -57,16 +56,36 @@ export default class Invoice extends Model {
set subTotal(val) {}
get total() {
return this.subTotal + this.totalVat;
return this.subTotal + this.taxTotal;
}
// eslint-disable-next-line no-empty-function
set total(val) {}
get totalVat() {
return (this.vat_rate / 100) * this.subTotal;
get taxTotal() {
return Object.values(this.taxes).reduce((carr, tax) => (tax.total + carr), 0);
}
// 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) {}
}

View File

@ -18,7 +18,6 @@ export default class Team extends Model {
website: this.attr(''),
contact_email: this.attr(''),
contact_phone: this.attr(''),
vat_rate: this.attr(null),
currency: this.attr(null),
invoice_late_fee: this.attr(null),
invoice_due_days: this.attr(null),

View File

@ -24,6 +24,7 @@ import TeamField from '@/store/models/team-field';
import InvoiceClientField from '@/store/models/invoice-client-field';
import InvoiceTeamField from '@/store/models/invoice-team-field';
import Tax from '@/store/models/tax';
import InvoiceRowTax from '@/store/models/invoice-row-tax';
Vue.use(Vuex);
@ -39,6 +40,7 @@ database.register(Invoice);
database.register(InvoiceClientField);
database.register(InvoiceTeamField);
database.register(InvoiceRow);
database.register(InvoiceRowTax);
database.register(BankAccount);
export default new Vuex.Store({