mirror of
https://github.com/InvoiceShelf/InvoiceShelf.git
synced 2026-04-07 13:41:23 +00:00
The issue was found during an Item creation inside the Invoice, Estimates or Recurring Invoice, the same fix that was applied into the Item creation view, now is needed into ItemModal. The root cause is that price + tax returns an amount as float making the database fail. Relates #377
276 lines
7.6 KiB
Vue
276 lines
7.6 KiB
Vue
<template>
|
|
<BaseModal :show="modalActive" @close="closeItemModal">
|
|
<template #header>
|
|
<div class="flex justify-between w-full">
|
|
{{ modalStore.title }}
|
|
<BaseIcon
|
|
name="XMarkIcon"
|
|
class="h-6 w-6 text-gray-500 cursor-pointer"
|
|
@click="closeItemModal"
|
|
/>
|
|
</div>
|
|
</template>
|
|
<div class="item-modal">
|
|
<form action="" @submit.prevent="submitItemData">
|
|
<div class="px-8 py-8 sm:p-6">
|
|
<BaseInputGrid layout="one-column">
|
|
<BaseInputGroup
|
|
:label="$t('items.name')"
|
|
required
|
|
:error="v$.name.$error && v$.name.$errors[0].$message"
|
|
>
|
|
<BaseInput
|
|
v-model="itemStore.currentItem.name"
|
|
type="text"
|
|
:invalid="v$.name.$error"
|
|
@input="v$.name.$touch()"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup :label="$t('items.price')">
|
|
<BaseMoney
|
|
:key="companyStore.selectedCompanyCurrency"
|
|
v-model="price"
|
|
:currency="companyStore.selectedCompanyCurrency"
|
|
class="
|
|
relative
|
|
w-full
|
|
focus:border focus:border-solid focus:border-primary
|
|
"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup :label="$t('items.unit')">
|
|
<BaseMultiselect
|
|
v-model="itemStore.currentItem.unit_id"
|
|
label="name"
|
|
:options="itemStore.itemUnits"
|
|
value-prop="id"
|
|
:can-deselect="false"
|
|
:can-clear="false"
|
|
:placeholder="$t('items.select_a_unit')"
|
|
searchable
|
|
track-by="name"
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup
|
|
v-if="isTaxPerItemEnabled"
|
|
:label="$t('items.taxes')"
|
|
>
|
|
<BaseMultiselect
|
|
v-model="taxes"
|
|
:options="getTaxTypes"
|
|
mode="tags"
|
|
label="tax_name"
|
|
value-prop="id"
|
|
class="w-full"
|
|
:can-deselect="false"
|
|
:can-clear="false"
|
|
searchable
|
|
track-by="tax_name"
|
|
object
|
|
/>
|
|
</BaseInputGroup>
|
|
|
|
<BaseInputGroup
|
|
:label="$t('items.description')"
|
|
:error="
|
|
v$.description.$error && v$.description.$errors[0].$message
|
|
"
|
|
>
|
|
<BaseTextarea
|
|
v-model="itemStore.currentItem.description"
|
|
rows="4"
|
|
cols="50"
|
|
:invalid="v$.description.$error"
|
|
@input="v$.description.$touch()"
|
|
/>
|
|
</BaseInputGroup>
|
|
</BaseInputGrid>
|
|
</div>
|
|
<div
|
|
class="z-0 flex justify-end p-4 border-t border-gray-200 border-solid"
|
|
>
|
|
<BaseButton
|
|
class="mr-3"
|
|
variant="primary-outline"
|
|
type="button"
|
|
@click="closeItemModal"
|
|
>
|
|
{{ $t('general.cancel') }}
|
|
</BaseButton>
|
|
<BaseButton
|
|
:loading="isLoading"
|
|
:disabled="isLoading"
|
|
variant="primary"
|
|
type="submit"
|
|
>
|
|
<template #left="slotProps">
|
|
<BaseIcon name="ArrowDownOnSquareIcon" :class="slotProps.class" />
|
|
</template>
|
|
{{ itemStore.isEdit ? $t('general.update') : $t('general.save') }}
|
|
</BaseButton>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</BaseModal>
|
|
</template>
|
|
|
|
<script setup>
|
|
import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
import { useI18n } from 'vue-i18n'
|
|
import { useRoute } from 'vue-router'
|
|
import {
|
|
required,
|
|
minLength,
|
|
maxLength,
|
|
minValue,
|
|
helpers,
|
|
alpha,
|
|
} from '@vuelidate/validators'
|
|
import useVuelidate from '@vuelidate/core'
|
|
import { useModalStore } from '@/scripts/stores/modal'
|
|
import { useCompanyStore } from '@/scripts/admin/stores/company'
|
|
import { useItemStore } from '@/scripts/admin/stores/item'
|
|
import { useTaxTypeStore } from '@/scripts/admin/stores/tax-type'
|
|
import { useNotificationStore } from '@/scripts/stores/notification'
|
|
import { useEstimateStore } from '@/scripts/admin/stores/estimate'
|
|
import { useInvoiceStore } from '@/scripts/admin/stores/invoice'
|
|
|
|
const emit = defineEmits(['newItem'])
|
|
|
|
const modalStore = useModalStore()
|
|
const itemStore = useItemStore()
|
|
const companyStore = useCompanyStore()
|
|
const taxTypeStore = useTaxTypeStore()
|
|
const estimateStore = useEstimateStore()
|
|
const notificationStore = useNotificationStore()
|
|
|
|
const { t } = useI18n()
|
|
const isLoading = ref(false)
|
|
const taxPerItemSetting = ref(companyStore.selectedCompanySettings.tax_per_item)
|
|
|
|
const modalActive = computed(
|
|
() => modalStore.active && modalStore.componentName === 'ItemModal'
|
|
)
|
|
|
|
const price = computed({
|
|
get: () => itemStore.currentItem.price / 100,
|
|
set: (value) => {
|
|
itemStore.currentItem.price = Math.round(value * 100)
|
|
},
|
|
})
|
|
|
|
const taxes = computed({
|
|
get: () =>
|
|
itemStore.currentItem.taxes.map((tax) => {
|
|
if (tax) {
|
|
return {
|
|
...tax,
|
|
tax_type_id: tax.id,
|
|
tax_name: tax.name + ' (' + (tax.calculation_type === 'fixed' ? tax.fixed_amount : tax.percent) + (tax.calculation_type === 'fixed' ? companyStore.selectedCompanyCurrency.symbol : '%') + ')',
|
|
}
|
|
}
|
|
}),
|
|
set: (value) => {
|
|
itemStore.$patch((state) => {
|
|
state.currentItem.taxes = value
|
|
})
|
|
},
|
|
})
|
|
|
|
const isTaxPerItemEnabled = computed(() => {
|
|
return taxPerItemSetting.value === 'YES'
|
|
})
|
|
|
|
const rules = {
|
|
name: {
|
|
required: helpers.withMessage(t('validation.required'), required),
|
|
minLength: helpers.withMessage(
|
|
t('validation.name_min_length', { count: 3 }),
|
|
minLength(3)
|
|
),
|
|
},
|
|
|
|
description: {
|
|
maxLength: helpers.withMessage(
|
|
t('validation.description_maxlength', { count: 255 }),
|
|
maxLength(255)
|
|
),
|
|
},
|
|
}
|
|
|
|
const v$ = useVuelidate(
|
|
rules,
|
|
computed(() => itemStore.currentItem)
|
|
)
|
|
|
|
const getTaxTypes = computed(() => {
|
|
return taxTypeStore.taxTypes.map((tax) => {
|
|
const amount = tax.calculation_type === 'fixed'
|
|
? new Intl.NumberFormat(undefined, {
|
|
style: 'currency',
|
|
currency: companyStore.selectedCompanyCurrency.code
|
|
}).format(tax.fixed_amount / 100)
|
|
: `${tax.percent}%`
|
|
|
|
return {
|
|
...tax,
|
|
tax_name: `${tax.name} (${amount})`
|
|
}
|
|
})
|
|
})
|
|
|
|
onMounted(() => {
|
|
v$.value.$reset()
|
|
itemStore.fetchItemUnits({ limit: 'all' })
|
|
})
|
|
|
|
async function submitItemData() {
|
|
v$.value.$touch()
|
|
|
|
if (v$.value.$invalid) {
|
|
return true
|
|
}
|
|
|
|
let data = {
|
|
...itemStore.currentItem,
|
|
taxes: itemStore.currentItem.taxes.map((tax) => {
|
|
return {
|
|
tax_type_id: tax.id,
|
|
amount: tax.calculation_type === 'fixed' ? tax.fixed_amount : Math.round(price.value * tax.percent),
|
|
percent: tax.percent,
|
|
fixed_amount: tax.fixed_amount,
|
|
calculation_type: tax.calculation_type,
|
|
name: tax.name,
|
|
collective_tax: 0,
|
|
}
|
|
}),
|
|
}
|
|
|
|
isLoading.value = true
|
|
|
|
const action = itemStore.isEdit ? itemStore.updateItem : itemStore.addItem
|
|
|
|
await action(data).then((res) => {
|
|
isLoading.value = false
|
|
if (res.data.data) {
|
|
if (modalStore.data) {
|
|
modalStore.refreshData(res.data.data)
|
|
}
|
|
}
|
|
closeItemModal()
|
|
})
|
|
}
|
|
|
|
function closeItemModal() {
|
|
modalStore.closeModal()
|
|
setTimeout(() => {
|
|
itemStore.resetCurrentItem()
|
|
modalStore.$reset()
|
|
v$.value.$reset()
|
|
}, 300)
|
|
}
|
|
</script>
|