mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-09-05 14:51:07 +00:00
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:
@@ -171,11 +171,12 @@
|
||||
:tax-data="tax"
|
||||
:taxes="itemData.taxes ?? []"
|
||||
:discounted-total="total"
|
||||
:total-tax="totalSimpleTax"
|
||||
:total-simple-tax="totalSimpleTax"
|
||||
:total="subtotal"
|
||||
:currency="currency"
|
||||
:update-items="syncItemToStore"
|
||||
:ability="'create-invoice'"
|
||||
:tax-types="taxTypes"
|
||||
:can-add-tax="canAddTax"
|
||||
:store="store"
|
||||
:store-prop="storeProp"
|
||||
:discount="discount"
|
||||
@@ -199,6 +200,7 @@ import DocumentItemRowTax from './DocumentItemRowTax.vue'
|
||||
import DragIcon from '@/scripts/components/icons/DragIcon.vue'
|
||||
import { generateClientId } from '../../../utils'
|
||||
import type { Currency } from '../../../types/domain/currency'
|
||||
import type { TaxType } from '../../../types/domain/tax'
|
||||
import type { DocumentItem, DocumentFormData, DocumentTax } from './use-document-calculations'
|
||||
|
||||
interface Props {
|
||||
@@ -215,6 +217,8 @@ interface Props {
|
||||
currency: Currency | Record<string, unknown>
|
||||
invoiceItems: DocumentItem[]
|
||||
itemValidationScope?: string
|
||||
taxTypes?: TaxType[]
|
||||
canAddTax?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
@@ -227,6 +231,8 @@ const props = withDefaults(defineProps<Props>(), {
|
||||
type: '',
|
||||
loading: false,
|
||||
itemValidationScope: '',
|
||||
taxTypes: () => [],
|
||||
canAddTax: false,
|
||||
})
|
||||
|
||||
const emit = defineEmits<Emits>()
|
||||
@@ -286,16 +292,33 @@ const showRemoveButton = computed<boolean>(() => {
|
||||
return formData.value.items.length > 1
|
||||
})
|
||||
|
||||
// Base handed down to the tax rows: only the non-compound taxes count, so a
|
||||
// compound row can never widen its own base through this value.
|
||||
const totalSimpleTax = computed<number>(() => {
|
||||
const taxes = props.itemData.taxes ?? []
|
||||
return Math.round(
|
||||
taxes.reduce((sum: number, tax: Partial<DocumentTax>) => {
|
||||
if (tax.compound_tax) {
|
||||
return sum
|
||||
}
|
||||
return sum + (tax.amount ?? 0)
|
||||
}, 0),
|
||||
)
|
||||
})
|
||||
|
||||
const totalTax = computed<number>(() => totalSimpleTax.value)
|
||||
const totalCompoundTax = computed<number>(() => {
|
||||
const taxes = props.itemData.taxes ?? []
|
||||
return Math.round(
|
||||
taxes.reduce((sum: number, tax: Partial<DocumentTax>) => {
|
||||
if (tax.compound_tax) {
|
||||
return sum + (tax.amount ?? 0)
|
||||
}
|
||||
return sum
|
||||
}, 0),
|
||||
)
|
||||
})
|
||||
|
||||
const totalTax = computed<number>(() => totalSimpleTax.value + totalCompoundTax.value)
|
||||
|
||||
const companyCurrency = computed(() => companyStore.selectedCompanyCurrency)
|
||||
|
||||
@@ -436,6 +459,7 @@ function syncItemToStore(): void {
|
||||
total: total.value,
|
||||
sub_total: subtotal.value,
|
||||
totalSimpleTax: totalSimpleTax.value,
|
||||
totalCompoundTax: totalCompoundTax.value,
|
||||
totalTax: totalTax.value,
|
||||
tax: totalTax.value,
|
||||
taxes: [...itemTaxes],
|
||||
|
||||
Reference in New Issue
Block a user