feat: Tax included (#370)

* feat: Tax included

* Added a toggle switch in tax settings to enable the feature.
* Database migration adding tax_included field into estimates, invoices
  and recurring invoices table.
* Toggle switch to enable and store the tax_included by estimates,
  invoices and recurring invoices.
* In case of tax included enabled, total taxes will be recalculated and
  the invoices, estimates and recurring invoices total won't be sum with
  taxes.
* Apply tax included when discount_per_item/tax_per_item item is enabled.
* Custom component to show the net total when tax included is enabled.
* Update invoice and estimates pdfs with net total.

* chore: Tax included by default

A switch button inside the tax settings to enable the tax included by
default in invoices, estimates and recurring invoices.
This commit is contained in:
Fabio Ribeiro
2025-08-28 10:28:24 +02:00
committed by GitHub
parent 08e1bb2e22
commit d69a56e2d5
32 changed files with 582 additions and 83 deletions

View File

@@ -169,6 +169,9 @@ const taxAmount = computed(() => {
if (taxPerItemEnabled && !discountPerItemEnabled){
return getTaxAmount()
}
if (props.store[props.storeProp].tax_included) {
return Math.round(props.discountedTotal - (props.discountedTotal / (1 + (localTax.percent / 100))))
}
return (props.discountedTotal * localTax.percent) / 100
}
return 0
@@ -261,6 +264,7 @@ function getTaxAmount() {
const itemTotal = props.discountedTotal
const modelDiscount = props.store[props.storeProp].discount ? props.store[props.storeProp].discount : 0
const type = props.store[props.storeProp].discount_type
let discountedTotal = props.discountedTotal
if (modelDiscount > 0) {
props.store[props.storeProp].items.forEach((_i) => {
total += _i.total
@@ -268,10 +272,14 @@ function getTaxAmount() {
const proportion = (itemTotal / total).toFixed(2)
discount = type === 'fixed' ? modelDiscount * 100 : (total * modelDiscount) / 100
const itemDiscount = Math.round(discount * proportion)
const discounted = itemTotal - itemDiscount
return Math.round((discounted * localTax.percent) / 100)
discountedTotal = itemTotal - itemDiscount
}
return Math.round((props.discountedTotal * localTax.percent) / 100)
if (props.store[props.storeProp].tax_included) {
return Math.round(discountedTotal - (discountedTotal / (1 + (localTax.percent / 100))))
}
return Math.round((discountedTotal * localTax.percent) / 100)
}
</script>

View File

@@ -1,4 +1,27 @@
<template>
<!-- Tax Included -->
<div
v-if="companyStore.selectedCompanySettings.tax_included === 'YES'"
class="
flex
items-center
justify-end
w-full
px-6
text-base
border border-b-0 border-gray-200 border-solid
cursor-pointer
text-primary-400
bg-white
"
>
<BaseSwitchSection
v-model="taxIncludedField"
:title="$t('settings.tax_types.tax_included')"
:store="store"
:store-prop="storeProp"
/>
</div>
<table class="text-center item-table min-w-full">
<colgroup>
<col style="width: 40%; min-width: 280px" />
@@ -191,4 +214,14 @@ const defaultCurrency = computed(() => {
return companyStore.selectedCompanyCurrency
}
})
const taxIncludedField = computed({
get: () => {
return props.store[props.storeProp].tax_included
},
set: async (value) => {
props.store[props.storeProp].tax_included = value
},
})
</script>

View File

@@ -38,6 +38,17 @@
</label>
</div>
<div
v-if="store[storeProp].tax_per_item === 'YES'"
>
<NetTotal
:currency="currency"
:store="store"
:storeProp="storeProp"
:isLoading="isLoading"
/>
</div>
<div
v-for="tax in itemWiseTaxes"
:key="tax.tax_type_id"
@@ -135,6 +146,21 @@
</div>
</div>
<div
v-if="
store[storeProp].tax_per_item === 'NO' ||
store[storeProp].tax_per_item === null
"
class="flex items-center justify-between w-full mt-2"
>
<NetTotal
:currency="currency"
:store="store"
:storeProp="storeProp"
:isLoading="isLoading"
/>
</div>
<div
v-if="
store[storeProp].tax_per_item === 'NO' ||
@@ -149,6 +175,7 @@
:taxes="taxes"
:currency="currency"
:store="store"
:storeProp="storeProp"
@remove="removeTax"
@update="updateTax"
/>
@@ -198,6 +225,7 @@
<script setup>
import { computed, inject, ref, watch } from 'vue'
import Guid from 'guid'
import NetTotal from './NetTotal.vue'
import Tax from './CreateTotalTaxes.vue'
import TaxStub from '@/scripts/admin/stub/abilities'
import SelectTaxPopup from './SelectTaxPopup.vue'

View File

@@ -42,6 +42,10 @@ const props = defineProps({
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
data: {
type: String,
default: '',
@@ -64,6 +68,13 @@ const taxAmount = computed(() => {
100
)
}
if (props.store.getSubtotalWithDiscount && props.tax.percent && props.store[props.storeProp].tax_included) {
return Math.round(
props.store.getSubtotalWithDiscount - (
props.store.getSubtotalWithDiscount / (1 + (props.tax.percent / 100))
)
)
}
if (props.store.getSubtotalWithDiscount && props.tax.percent) {
return Math.round(
(props.store.getSubtotalWithDiscount * props.tax.percent) / 100
@@ -81,6 +92,13 @@ watchEffect(() => {
}
})
watch(
() => props.store[props.storeProp].tax_included,
(val) => {
updateTax()
}, { deep: true },
)
function updateTax() {
emit('update', {
...props.tax,

View File

@@ -0,0 +1,54 @@
<template>
<div
v-if="store[storeProp].tax_included"
class="flex items-center justify-between w-full"
>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else
class="text-sm font-semibold leading-5 text-gray-500 uppercase"
>
{{ $t('estimates.net_total') }}
</label>
<BaseContentPlaceholders v-if="isLoading">
<BaseContentPlaceholdersText :lines="1" class="w-16 h-5" />
</BaseContentPlaceholders>
<label
v-else
class="flex items-center justify-center m-0 text-lg text-black uppercase "
>
<BaseFormatMoney
:amount="store.getNetTotal"
:currency="currency"
/>
</label>
</div>
</template>
<script setup>
import BaseContentPlaceholdersText from '@/scripts/components/base/BaseContentPlaceholdersText.vue'
import BaseContentPlaceholders from '@/scripts/components/base/BaseContentPlaceholders.vue'
import BaseFormatMoney from '@/scripts/components/base/BaseFormatMoney.vue'
const props = defineProps({
store: {
type: Object,
default: null,
},
storeProp: {
type: String,
default: '',
},
currency: {
type: [Object, String],
default: '',
},
isLoading: {
type: Boolean,
default: false,
},
})
</script>