Add compound tax support (#753)

* test(taxes): cover compound_tax API semantics and validate it as boolean

* feat(taxes): restore the compound tax toggle in tax type settings

* feat(taxes): compute document-level compound tax amounts

Two-pass recalculation: simple taxes on the discounted subtotal, compound
taxes on the subtotal plus all simple taxes (v2 parity; branch order
fixed -> compound -> inclusive back-out -> simple). Amounts now also
recalculate on tax add/remove and on the tax-inclusive toggle, which
previously never triggered recalculation.

* feat(taxes): support compound taxes in per-item mode

Per-item tax rows now carry the compound flag and charge compound taxes
on the item's discounted base plus its simple taxes, through the same
calcTaxAmount branch order as document-level taxes. The item row splits
its tax sums into simple and compound so a compound row can never widen
its own base.

Also fixes two pre-existing bugs: the per-item tax dropdown read
window.__taxTypes, which nothing ever assigned, so it always rendered
empty (tax types are now fetched by the items table and passed down);
and removing a tax row hard-zeroed the item's totals instead of
re-syncing them.

* fix(invoices): skip placeholder tax rows when persisting item taxes

The document form keeps one empty placeholder tax row per item in
per-item tax mode. Its empty name is nullified by the framework's
empty-string middleware, so inserting it violated the NOT NULL
constraint and turned every per-item save into a 500. The equivalent
v2 guard keyed on a null amount, which the v3 stub (amount: 0) evades;
keying on the missing tax_type_id catches it.
This commit is contained in:
Darko Gjorgjijoski
2026-08-14 00:17:52 +02:00
committed by GitHub
parent 844d2c82ed
commit 13aa087faf
15 changed files with 384 additions and 105 deletions
@@ -92,6 +92,8 @@
:currency="defaultCurrency"
:item-validation-scope="itemValidationScope"
:invoice-items="formData.items"
:tax-types="availableTaxTypes"
:can-add-tax="canAddTax"
:store="store"
:store-prop="storeProp"
/>
@@ -110,11 +112,15 @@
</template>
<script setup lang="ts">
import { computed } from 'vue'
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 { useUserStore } from '../../../stores/user.store'
import { taxTypeService } from '../../../api/services/tax-type.service'
import { ABILITIES } from '../../../config/abilities'
import type { Currency } from '../../../types/domain/currency'
import type { TaxType } from '../../../types/domain/tax'
import type { DocumentFormData } from './use-document-calculations'
interface Props {
@@ -134,6 +140,25 @@ const props = withDefaults(defineProps<Props>(), {
taxIncludedSetting: 'NO',
})
const userStore = useUserStore()
const availableTaxTypes = ref<TaxType[]>([])
const canAddTax = computed<boolean>(() => {
return userStore.hasAbilities(ABILITIES.CREATE_TAX_TYPE)
})
onMounted(async () => {
try {
const response = await taxTypeService.list({
limit: 'all',
transaction_type: 'sales',
})
availableTaxTypes.value = response.data
} catch {
// Silently fail
}
})
const formData = computed<DocumentFormData>(() => {
return props.store[props.storeProp] as DocumentFormData
})