fix(taxes): correct compound tax document flows (#754)

This commit is contained in:
Darko Gjorgjijoski
2026-08-14 00:42:01 +02:00
committed by GitHub
parent 13aa087faf
commit 93dd7df48a
22 changed files with 701 additions and 57 deletions
@@ -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) {