mirror of
				https://github.com/mokuappio/serverless-invoices.git
				synced 2025-10-31 09:51:08 -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:
		| @ -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)); | ||||
|     }, | ||||
|  | ||||
| @ -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; | ||||
|     }, | ||||
|   }, | ||||
| }; | ||||
|  | ||||
| @ -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') | ||||
|  | ||||
| @ -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(''), | ||||
|  | ||||
							
								
								
									
										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 { 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(''), | ||||
|     }; | ||||
|  | ||||
| @ -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) {} | ||||
| } | ||||
|  | ||||
| @ -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), | ||||
|  | ||||
| @ -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({ | ||||
|  | ||||
		Reference in New Issue
	
	Block a user