mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-05 23:01:07 +00:00
fix(taxes): correct compound tax document flows (#754)
This commit is contained in:
@@ -160,10 +160,22 @@ export const useEstimateStore = defineStore('estimate', {
|
||||
},
|
||||
|
||||
getNetTotal(): number {
|
||||
return this.getSubtotalWithDiscount - this.getTotalTax
|
||||
if (this.newEstimate.tax_included) {
|
||||
return this.getSubtotalWithDiscount - this.getTotalSimpleTax
|
||||
}
|
||||
|
||||
return this.getSubtotalWithDiscount
|
||||
},
|
||||
|
||||
getTotalSimpleTax(state): number {
|
||||
if (state.newEstimate.tax_per_item === 'YES') {
|
||||
return state.newEstimate.items.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.taxes ?? []).reduce((itemSum, tax) => {
|
||||
return tax.compound_tax ? itemSum : itemSum + (tax.amount ?? 0)
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return state.newEstimate.taxes.reduce(
|
||||
(sum: number, tax: DocumentTax) => {
|
||||
if (!tax.compound_tax) return sum + (tax.amount ?? 0)
|
||||
@@ -174,6 +186,14 @@ export const useEstimateStore = defineStore('estimate', {
|
||||
},
|
||||
|
||||
getTotalCompoundTax(state): number {
|
||||
if (state.newEstimate.tax_per_item === 'YES') {
|
||||
return state.newEstimate.items.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.taxes ?? []).reduce((itemSum, tax) => {
|
||||
return tax.compound_tax ? itemSum + (tax.amount ?? 0) : itemSum
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return state.newEstimate.taxes.reduce(
|
||||
(sum: number, tax: DocumentTax) => {
|
||||
if (tax.compound_tax) return sum + (tax.amount ?? 0)
|
||||
@@ -184,16 +204,7 @@ export const useEstimateStore = defineStore('estimate', {
|
||||
},
|
||||
|
||||
getTotalTax(): number {
|
||||
if (
|
||||
this.newEstimate.tax_per_item === 'NO' ||
|
||||
this.newEstimate.tax_per_item === null
|
||||
) {
|
||||
return this.getTotalSimpleTax + this.getTotalCompoundTax
|
||||
}
|
||||
return this.newEstimate.items.reduce(
|
||||
(sum: number, item: DocumentItem) => sum + (item.tax ?? 0),
|
||||
0,
|
||||
)
|
||||
return this.getTotalSimpleTax + this.getTotalCompoundTax
|
||||
},
|
||||
|
||||
getSubtotalWithDiscount(): number {
|
||||
@@ -202,7 +213,7 @@ export const useEstimateStore = defineStore('estimate', {
|
||||
|
||||
getTotal(): number {
|
||||
if (this.newEstimate.tax_included) {
|
||||
return this.getSubtotalWithDiscount
|
||||
return this.getSubtotalWithDiscount + this.getTotalCompoundTax
|
||||
}
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
@@ -508,6 +519,9 @@ export const useEstimateStore = defineStore('estimate', {
|
||||
|
||||
if (!isEdit && companySettings) {
|
||||
this.newEstimate.tax_per_item = companySettings.tax_per_item ?? null
|
||||
this.newEstimate.tax_included =
|
||||
companySettings.tax_included === 'YES' &&
|
||||
companySettings.tax_included_by_default === 'YES'
|
||||
this.newEstimate.sales_tax_type = companySettings.sales_tax_type ?? null
|
||||
this.newEstimate.sales_tax_address_type =
|
||||
companySettings.sales_tax_address_type ?? null
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
:currency="estimateStore.newEstimate.selectedCurrency"
|
||||
:is-loading="isLoadingContent"
|
||||
:item-validation-scope="estimateValidationScope"
|
||||
:tax-included-setting="companyStore.selectedCompanySettings.tax_included"
|
||||
:store="estimateStore"
|
||||
store-prop="newEstimate"
|
||||
/>
|
||||
@@ -115,6 +116,7 @@ import {
|
||||
} from '@vuelidate/validators'
|
||||
import useVuelidate from '@vuelidate/core'
|
||||
import { useEstimateStore } from '../store'
|
||||
import { useCompanyStore } from '@/scripts/stores/company.store'
|
||||
import { useNotificationStore } from '@/scripts/stores/notification.store'
|
||||
import {
|
||||
handleApiError,
|
||||
@@ -130,6 +132,7 @@ import {
|
||||
} from '../../../shared/document-form'
|
||||
|
||||
const estimateStore = useEstimateStore()
|
||||
const companyStore = useCompanyStore()
|
||||
const notificationStore = useNotificationStore()
|
||||
const { t } = useI18n()
|
||||
const route = useRoute()
|
||||
|
||||
@@ -171,10 +171,22 @@ export const useInvoiceStore = defineStore('invoice', {
|
||||
},
|
||||
|
||||
getNetTotal(): number {
|
||||
return this.getSubtotalWithDiscount - this.getTotalTax
|
||||
if (this.newInvoice.tax_included) {
|
||||
return this.getSubtotalWithDiscount - this.getTotalSimpleTax
|
||||
}
|
||||
|
||||
return this.getSubtotalWithDiscount
|
||||
},
|
||||
|
||||
getTotalSimpleTax(state): number {
|
||||
if (state.newInvoice.tax_per_item === 'YES') {
|
||||
return state.newInvoice.items.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.taxes ?? []).reduce((itemSum, tax) => {
|
||||
return tax.compound_tax ? itemSum : itemSum + (tax.amount ?? 0)
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return state.newInvoice.taxes.reduce(
|
||||
(sum: number, tax: DocumentTax) => {
|
||||
if (!tax.compound_tax) return sum + (tax.amount ?? 0)
|
||||
@@ -185,6 +197,14 @@ export const useInvoiceStore = defineStore('invoice', {
|
||||
},
|
||||
|
||||
getTotalCompoundTax(state): number {
|
||||
if (state.newInvoice.tax_per_item === 'YES') {
|
||||
return state.newInvoice.items.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.taxes ?? []).reduce((itemSum, tax) => {
|
||||
return tax.compound_tax ? itemSum + (tax.amount ?? 0) : itemSum
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return state.newInvoice.taxes.reduce(
|
||||
(sum: number, tax: DocumentTax) => {
|
||||
if (tax.compound_tax) return sum + (tax.amount ?? 0)
|
||||
@@ -195,16 +215,7 @@ export const useInvoiceStore = defineStore('invoice', {
|
||||
},
|
||||
|
||||
getTotalTax(): number {
|
||||
if (
|
||||
this.newInvoice.tax_per_item === 'NO' ||
|
||||
this.newInvoice.tax_per_item === null
|
||||
) {
|
||||
return this.getTotalSimpleTax + this.getTotalCompoundTax
|
||||
}
|
||||
return this.newInvoice.items.reduce(
|
||||
(sum: number, item: DocumentItem) => sum + (item.tax ?? 0),
|
||||
0,
|
||||
)
|
||||
return this.getTotalSimpleTax + this.getTotalCompoundTax
|
||||
},
|
||||
|
||||
getSubtotalWithDiscount(): number {
|
||||
@@ -213,7 +224,7 @@ export const useInvoiceStore = defineStore('invoice', {
|
||||
|
||||
getTotal(): number {
|
||||
if (this.newInvoice.tax_included) {
|
||||
return this.getSubtotalWithDiscount
|
||||
return this.getSubtotalWithDiscount + this.getTotalCompoundTax
|
||||
}
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
@@ -504,6 +515,9 @@ export const useInvoiceStore = defineStore('invoice', {
|
||||
|
||||
if (!isEdit && companySettings) {
|
||||
this.newInvoice.tax_per_item = companySettings.tax_per_item ?? null
|
||||
this.newInvoice.tax_included =
|
||||
companySettings.tax_included === 'YES' &&
|
||||
companySettings.tax_included_by_default === 'YES'
|
||||
this.newInvoice.sales_tax_type = companySettings.sales_tax_type ?? null
|
||||
this.newInvoice.sales_tax_address_type =
|
||||
companySettings.sales_tax_address_type ?? null
|
||||
|
||||
@@ -65,6 +65,7 @@
|
||||
:currency="invoiceStore.newInvoice.selectedCurrency"
|
||||
:is-loading="isLoadingContent"
|
||||
:item-validation-scope="invoiceValidationScope"
|
||||
:tax-included-setting="companyStore.selectedCompanySettings.tax_included"
|
||||
:store="invoiceStore"
|
||||
store-prop="newInvoice"
|
||||
/>
|
||||
|
||||
@@ -192,10 +192,22 @@ export const useRecurringInvoiceStore = defineStore('recurring-invoice', {
|
||||
},
|
||||
|
||||
getNetTotal(): number {
|
||||
return this.getSubtotalWithDiscount - this.getTotalTax
|
||||
if (this.newRecurringInvoice.tax_included) {
|
||||
return this.getSubtotalWithDiscount - this.getTotalSimpleTax
|
||||
}
|
||||
|
||||
return this.getSubtotalWithDiscount
|
||||
},
|
||||
|
||||
getTotalSimpleTax(state): number {
|
||||
if (state.newRecurringInvoice.tax_per_item === 'YES') {
|
||||
return state.newRecurringInvoice.items.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.taxes ?? []).reduce((itemSum, tax) => {
|
||||
return tax.compound_tax ? itemSum : itemSum + (tax.amount ?? 0)
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return state.newRecurringInvoice.taxes.reduce(
|
||||
(sum: number, tax: DocumentTax) => {
|
||||
if (!tax.compound_tax) return sum + (tax.amount ?? 0)
|
||||
@@ -206,6 +218,14 @@ export const useRecurringInvoiceStore = defineStore('recurring-invoice', {
|
||||
},
|
||||
|
||||
getTotalCompoundTax(state): number {
|
||||
if (state.newRecurringInvoice.tax_per_item === 'YES') {
|
||||
return state.newRecurringInvoice.items.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.taxes ?? []).reduce((itemSum, tax) => {
|
||||
return tax.compound_tax ? itemSum + (tax.amount ?? 0) : itemSum
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return state.newRecurringInvoice.taxes.reduce(
|
||||
(sum: number, tax: DocumentTax) => {
|
||||
if (tax.compound_tax) return sum + (tax.amount ?? 0)
|
||||
@@ -216,16 +236,7 @@ export const useRecurringInvoiceStore = defineStore('recurring-invoice', {
|
||||
},
|
||||
|
||||
getTotalTax(): number {
|
||||
if (
|
||||
this.newRecurringInvoice.tax_per_item === 'NO' ||
|
||||
this.newRecurringInvoice.tax_per_item === null
|
||||
) {
|
||||
return this.getTotalSimpleTax + this.getTotalCompoundTax
|
||||
}
|
||||
return this.newRecurringInvoice.items.reduce(
|
||||
(sum: number, item: DocumentItem) => sum + (item.tax ?? 0),
|
||||
0,
|
||||
)
|
||||
return this.getTotalSimpleTax + this.getTotalCompoundTax
|
||||
},
|
||||
|
||||
getSubtotalWithDiscount(): number {
|
||||
@@ -234,7 +245,7 @@ export const useRecurringInvoiceStore = defineStore('recurring-invoice', {
|
||||
|
||||
getTotal(): number {
|
||||
if (this.newRecurringInvoice.tax_included) {
|
||||
return this.getSubtotalWithDiscount
|
||||
return this.getSubtotalWithDiscount + this.getTotalCompoundTax
|
||||
}
|
||||
return this.getSubtotalWithDiscount + this.getTotalTax
|
||||
},
|
||||
@@ -511,6 +522,9 @@ export const useRecurringInvoiceStore = defineStore('recurring-invoice', {
|
||||
if (!isEdit && companySettings) {
|
||||
this.newRecurringInvoice.tax_per_item =
|
||||
companySettings.tax_per_item ?? null
|
||||
this.newRecurringInvoice.tax_included =
|
||||
companySettings.tax_included === 'YES' &&
|
||||
companySettings.tax_included_by_default === 'YES'
|
||||
this.newRecurringInvoice.discount_per_item =
|
||||
companySettings.discount_per_item ?? null
|
||||
this.newRecurringInvoice.sales_tax_type =
|
||||
|
||||
@@ -181,6 +181,7 @@
|
||||
:store-prop="storeProp"
|
||||
:discount="discount"
|
||||
@update="updateTax"
|
||||
@tax-type-created="onTaxTypeCreated"
|
||||
/>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -225,6 +226,7 @@ interface Emits {
|
||||
(e: 'update', data: Record<string, unknown>): void
|
||||
(e: 'remove', index: number): void
|
||||
(e: 'itemValidate', valid: boolean): void
|
||||
(e: 'taxTypeCreated', taxType: TaxType): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -391,6 +393,10 @@ function updateTax(data: { index: number; item: DocumentTax }): void {
|
||||
syncItemToStore()
|
||||
}
|
||||
|
||||
function onTaxTypeCreated(taxType: TaxType): void {
|
||||
emit('taxTypeCreated', taxType)
|
||||
}
|
||||
|
||||
function setDiscount(): void {
|
||||
const newValue = formData.value.items[props.index].discount
|
||||
const absoluteSubtotal = Math.abs(subtotal.value)
|
||||
|
||||
@@ -103,6 +103,7 @@ interface Props {
|
||||
interface Emits {
|
||||
(e: 'remove', index: number): void
|
||||
(e: 'update', payload: { index: number; item: DocumentTax }): void
|
||||
(e: 'taxTypeCreated', taxType: TaxType): void
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
@@ -246,9 +247,28 @@ function openTaxModal(): void {
|
||||
transaction_type: 'sales',
|
||||
},
|
||||
size: 'sm',
|
||||
refreshData: (...args: unknown[]) => {
|
||||
const taxType = args[0]
|
||||
if (isTaxType(taxType)) {
|
||||
selectedTax.value = taxType
|
||||
onSelectTax(taxType)
|
||||
emit('taxTypeCreated', taxType)
|
||||
}
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
function isTaxType(value: unknown): value is TaxType {
|
||||
return (
|
||||
typeof value === 'object' &&
|
||||
value !== null &&
|
||||
'id' in value &&
|
||||
typeof value.id === 'number' &&
|
||||
'name' in value &&
|
||||
typeof value.name === 'string'
|
||||
)
|
||||
}
|
||||
|
||||
function removeTax(index: number): void {
|
||||
const store = props.store as Record<string, Record<string, unknown>>
|
||||
const formData = store[props.storeProp] as DocumentFormData
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
<!-- Single shared item-create modal for the whole table (one instance, not one
|
||||
per row — stacked HeadlessUI dialogs would otherwise close each other). -->
|
||||
<ItemModal />
|
||||
<TaxTypeModal />
|
||||
|
||||
<!-- Tax Included Toggle -->
|
||||
<div
|
||||
@@ -96,6 +97,7 @@
|
||||
:can-add-tax="canAddTax"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
@tax-type-created="upsertAvailableTaxType"
|
||||
/>
|
||||
</template>
|
||||
</draggable>
|
||||
@@ -116,6 +118,7 @@ import { computed, onMounted, ref } from 'vue'
|
||||
import draggable from 'vuedraggable'
|
||||
import DocumentItemRow from './DocumentItemRow.vue'
|
||||
import ItemModal from '@/scripts/features/company/items/components/ItemModal.vue'
|
||||
import TaxTypeModal from '@/scripts/features/company/settings/components/TaxTypeModal.vue'
|
||||
import { useUserStore } from '../../../stores/user.store'
|
||||
import { taxTypeService } from '../../../api/services/tax-type.service'
|
||||
import { ABILITIES } from '../../../config/abilities'
|
||||
@@ -159,6 +162,17 @@ onMounted(async () => {
|
||||
}
|
||||
})
|
||||
|
||||
function upsertAvailableTaxType(taxType: TaxType): void {
|
||||
const index = availableTaxTypes.value.findIndex(({ id }) => id === taxType.id)
|
||||
|
||||
if (index === -1) {
|
||||
availableTaxTypes.value.push(taxType)
|
||||
return
|
||||
}
|
||||
|
||||
availableTaxTypes.value.splice(index, 1, taxType)
|
||||
}
|
||||
|
||||
const formData = computed<DocumentFormData>(() => {
|
||||
return props.store[props.storeProp] as DocumentFormData
|
||||
})
|
||||
|
||||
@@ -442,6 +442,8 @@ function selectPercentage(): void {
|
||||
}
|
||||
|
||||
function onSelectTax(selectedTax: TaxType): void {
|
||||
upsertAvailableTaxType(selectedTax)
|
||||
|
||||
const amount = calcTaxAmount(
|
||||
props.store.getSubtotalWithDiscount,
|
||||
selectedTax.percent,
|
||||
@@ -472,6 +474,17 @@ function onSelectTax(selectedTax: TaxType): void {
|
||||
recalculateGlobalTaxes()
|
||||
}
|
||||
|
||||
function upsertAvailableTaxType(taxType: TaxType): void {
|
||||
const index = availableTaxTypes.value.findIndex(({ id }) => id === taxType.id)
|
||||
|
||||
if (index === -1) {
|
||||
availableTaxTypes.value.push(taxType)
|
||||
return
|
||||
}
|
||||
|
||||
availableTaxTypes.value.splice(index, 1, taxType)
|
||||
}
|
||||
|
||||
function updateTax(data: DocumentTax): void {
|
||||
const tax = formData.value.taxes.find((t: DocumentTax) => t.id === data.id)
|
||||
if (tax) {
|
||||
|
||||
@@ -86,6 +86,14 @@ export function useDocumentCalculations(options: UseDocumentCalculationsOptions)
|
||||
})
|
||||
|
||||
const totalSimpleTax = computed<number>(() => {
|
||||
if (taxPerItem.value === 'YES') {
|
||||
return items.value.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.taxes ?? []).reduce((itemSum, tax) => {
|
||||
return tax.compound_tax ? itemSum : itemSum + (tax.amount ?? 0)
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return taxes.value.reduce((sum: number, tax: DocumentTax) => {
|
||||
if (!tax.compound_tax) {
|
||||
return sum + (tax.amount ?? 0)
|
||||
@@ -95,6 +103,14 @@ export function useDocumentCalculations(options: UseDocumentCalculationsOptions)
|
||||
})
|
||||
|
||||
const totalCompoundTax = computed<number>(() => {
|
||||
if (taxPerItem.value === 'YES') {
|
||||
return items.value.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.taxes ?? []).reduce((itemSum, tax) => {
|
||||
return tax.compound_tax ? itemSum + (tax.amount ?? 0) : itemSum
|
||||
}, 0)
|
||||
}, 0)
|
||||
}
|
||||
|
||||
return taxes.value.reduce((sum: number, tax: DocumentTax) => {
|
||||
if (tax.compound_tax) {
|
||||
return sum + (tax.amount ?? 0)
|
||||
@@ -104,12 +120,7 @@ export function useDocumentCalculations(options: UseDocumentCalculationsOptions)
|
||||
})
|
||||
|
||||
const totalTax = computed<number>(() => {
|
||||
if (taxPerItem.value === 'NO' || taxPerItem.value === null) {
|
||||
return totalSimpleTax.value + totalCompoundTax.value
|
||||
}
|
||||
return items.value.reduce((sum: number, item: DocumentItem) => {
|
||||
return sum + (item.tax ?? 0)
|
||||
}, 0)
|
||||
return totalSimpleTax.value + totalCompoundTax.value
|
||||
})
|
||||
|
||||
const subtotalWithDiscount = computed<number>(() => {
|
||||
@@ -117,12 +128,16 @@ export function useDocumentCalculations(options: UseDocumentCalculationsOptions)
|
||||
})
|
||||
|
||||
const netTotal = computed<number>(() => {
|
||||
return subtotalWithDiscount.value - totalTax.value
|
||||
if (taxIncluded.value) {
|
||||
return subtotalWithDiscount.value - totalSimpleTax.value
|
||||
}
|
||||
|
||||
return subtotalWithDiscount.value
|
||||
})
|
||||
|
||||
const total = computed<number>(() => {
|
||||
if (taxIncluded.value) {
|
||||
return subtotalWithDiscount.value
|
||||
return subtotalWithDiscount.value + totalCompoundTax.value
|
||||
}
|
||||
return subtotalWithDiscount.value + totalTax.value
|
||||
})
|
||||
@@ -164,8 +179,8 @@ export function calcItemTotal(subtotal: number, discountVal: number): number {
|
||||
/**
|
||||
* Calculate tax amount for a given total and tax config.
|
||||
*
|
||||
* A compound tax is charged on the base plus every simple (non-compound) tax
|
||||
* already applied, and is never backed out of a tax-inclusive total.
|
||||
* A compound tax is charged on top of an inclusive amount, or on the base plus
|
||||
* every simple (non-compound) tax when the amount is tax-exclusive.
|
||||
*
|
||||
* @param total Base amount in cents (document subtotal after discount, or an item total after discount)
|
||||
* @param percent Percentage rate, when the tax is percentage based
|
||||
@@ -189,6 +204,10 @@ export function calcTaxAmount(
|
||||
}
|
||||
if (!total || !percent) return 0
|
||||
if (compoundTax) {
|
||||
if (taxIncluded) {
|
||||
return Math.round((total * percent) / 100)
|
||||
}
|
||||
|
||||
return Math.round(((total + simpleTaxTotal) * percent) / 100)
|
||||
}
|
||||
if (taxIncluded) {
|
||||
|
||||
Reference in New Issue
Block a user