diff --git a/CHANGELOG.md b/CHANGELOG.md index 03812d252..e110e4cd6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,87 @@ +# Version 24.50 - 2024-12-10 + +### Added 🆕 + +- feat: add reportFileName option by:Javier Segarra +- feat: all clients just with global series by:jgallego +- feat: improve Merge branch 'test' into dev by:Javier Segarra +- feat: manual invoice in two lines by:jgallego +- feat: manualInvoice with address by:jgallego +- feat: randomize functions and example by:Javier Segarra +- feat: refs #6999 added search when user tabs on a filter with value by:Jon +- feat: refs #6999 added tab to search in VnTable filter by:Jon +- feat: refs #7346 #7346 improve form by:Javier Segarra +- feat: refs #7346 address ordered by:jgallego +- feat: refs #7346 radioButton by:jgallego +- feat: refs #7346 style radioButton by:jgallego +- feat: refs #7346 traducciones en cammelCase (7346-manualInvoice) by:jgallego +- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon +- feat: refs #8061 #8061 updates by:Javier Segarra +- feat: refs #8087 reactive data by:jorgep +- feat: refs #8087 refs#8087 Redadas en travel by:Carlos Andrés +- feat: refs #8138 add component ticket problems by:pablone +- feat: refs #8163 add max length and more tests by:wbuezas +- feat: refs #8163 add prop by:wbuezas +- feat: refs #8163 add VnInput insert functionality and e2e test by:wbuezas +- feat: refs #8163 limit with maxLength by:Javier Segarra +- feat: refs #8163 maxLength SupplierFD account by:Javier Segarra +- feat: refs #8163 maxLengthVnInput by:Javier Segarra +- feat: refs #8163 use VnAccountNumber in VnAccountNumber by:Javier Segarra +- feat: refs #8166 show notification by:jorgep + +### Changed 📦 + +- feat: refs #8038 added new functionality in VnSelect and refactor styles by:Jon +- perf: add dataCy by:Javier Segarra +- perf: refs #7346 #7346 Imrpove interface dialog by:Javier Segarra +- perf: refs #7346 #7346 use v-show instead v-if by:Javier Segarra +- perf: refs #8036 currentFilter by:alexm +- perf: refs #8061 filter autonomy by:Javier Segarra +- perf: refs #8061 solve conflicts and random posCode it by:Javier Segarra +- perf: refs #8061 use opts from VnSelect by:Javier Segarra +- perf: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra +- perf: remove console by:Javier Segarra +- perf: remove timeout by:Javier Segarra +- perf: test command fillInForm by:Javier Segarra +- refactor: refs #8162 remove comment by:wbuezas +- refactor: remove unnecesary things by:wbuezas + +### Fixed 🛠️ + +- fix: #8016 fetching data by:Javier Segarra +- fix: icons by:jgallego +- fix: refs #7229 download file by:jorgep +- fix: refs #7229 remove catch by:jorgep +- fix: refs #7229 set url by:jorgep +- fix: refs #7229 test by:jorgep +- fix: refs #7229 url by:jorgep +- fix: refs #7229 url + test by:jorgep +- fix: refs #7304 7304 clean warning by:carlossa +- fix: refs #7304 fix list by:carlossa +- fix: refs #7304 fix warning by:carlossa +- fix: refs #7346 traslations by:jgallego +- fix: refs #7529 add save by:carlossa +- fix: refs #7529 fix e2e by:carlossa +- fix: refs #7529 fix front by:carlossa +- fix: refs #7529 fix scss by:carlossa +- fix: refs #7529 fix te2e by:carlossa +- fix: refs #7529 fix workerPit e2e by:carlossa +- fix: refs #7529 front by:carlossa +- fix: refs #8036 apply exprBuilder after save filters by:alexm +- fix: refs #8036 only add where when required by:alexm +- fix: refs #8038 solve conflicts by:Jon +- fix: refs #8061 improve code dependencies (origin/8061_improve_newCP) by:Javier Segarra +- fix: refs #8138 move component from ui folder by:pablone +- fix: refs #8138 sme minor issues by:pablone +- fix: refs #8163 #8061 createNewPostCodeForm by:Javier Segarra +- fix: refs #8163 minor problem when keypress by:Javier Segarra +- fix: refs #8166 show zone error by:jorgep +- fix: removed selectedClient by:jgallego +- refs #7529 fix workerPit by:carlossa +- revert: refs #8061 test #8061 updates by:Javier Segarra +- test: fix own test by:Javier Segarra +- test: refs #8162 #8162 fix TicketList spec by:Javier Segarra + # Version 24.48 - 2024-11-25 ### Added 🆕 diff --git a/Jenkinsfile b/Jenkinsfile index 1766e3aea..c20da8ab2 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -4,7 +4,8 @@ def PROTECTED_BRANCH def BRANCH_ENV = [ test: 'test', - master: 'production' + master: 'production', + beta: 'production' ] node { @@ -15,7 +16,8 @@ node { PROTECTED_BRANCH = [ 'dev', 'test', - 'master' + 'master', + 'beta' ].contains(env.BRANCH_NAME) // https://www.jenkins.io/doc/book/pipeline/jenkinsfile/#using-environment-variables diff --git a/package.json b/package.json index 04b75a0b0..b5e62af11 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "salix-front", - "version": "24.50.0", + "version": "25.02.0", "description": "Salix frontend", "productName": "Salix", "author": "Verdnatura", @@ -64,4 +64,4 @@ "vite": "^5.1.4", "vitest": "^0.31.1" } -} \ No newline at end of file +} diff --git a/src/boot/mainShortcutMixin.js b/src/boot/mainShortcutMixin.js new file mode 100644 index 000000000..481077e37 --- /dev/null +++ b/src/boot/mainShortcutMixin.js @@ -0,0 +1,36 @@ +import routes from 'src/router/modules'; +import { useRouter } from 'vue-router'; + +let isNotified = false; + +export default { + created: function () { + const router = useRouter(); + const keyBindingMap = routes + .filter((route) => route.meta.keyBinding) + .reduce((map, route) => { + map['Key' + route.meta.keyBinding.toUpperCase()] = route.path; + return map; + }, {}); + + const handleKeyDown = (event) => { + const { ctrlKey, altKey, code } = event; + + if (ctrlKey && altKey && keyBindingMap[code] && !isNotified) { + event.preventDefault(); + router.push(keyBindingMap[code]); + isNotified = true; + } + }; + + const handleKeyUp = (event) => { + const { ctrlKey, altKey } = event; + if (!ctrlKey || !altKey) { + isNotified = false; + } + }; + + window.addEventListener('keydown', handleKeyDown); + window.addEventListener('keyup', handleKeyUp); + }, +}; diff --git a/src/boot/qformMixin.js b/src/boot/qformMixin.js index fc7852369..187ca6dbc 100644 --- a/src/boot/qformMixin.js +++ b/src/boot/qformMixin.js @@ -1,30 +1,51 @@ -import { getCurrentInstance } from 'vue'; - +function focusFirstInput(input) { + input.focus(); +} export default { mounted: function () { - const vm = getCurrentInstance(); - if (vm.type.name === 'QForm') { - if (!['searchbarForm', 'filterPanelForm'].includes(this.$el?.id)) { - // TODO: AUTOFOCUS IS NOT FOCUSING - const that = this; - this.$el.addEventListener('keyup', function (evt) { - if (evt.key === 'Enter') { - const input = evt.target; - if (input.type == 'textarea' && evt.shiftKey) { - evt.preventDefault(); - let { selectionStart, selectionEnd } = input; - input.value = - input.value.substring(0, selectionStart) + - '\n' + - input.value.substring(selectionEnd); - selectionStart = selectionEnd = selectionStart + 1; - return; - } - evt.preventDefault(); - that.onSubmit(); - } - }); + const that = this; + + const form = document.querySelector('.q-form#formModel'); + if (!form) return; + try { + const inputsFormCard = form.querySelectorAll( + `input:not([disabled]):not([type="checkbox"])` + ); + if (inputsFormCard.length) { + focusFirstInput(inputsFormCard[0]); } + const textareas = document.querySelectorAll( + 'textarea:not([disabled]), [contenteditable]:not([disabled])' + ); + if (textareas.length) { + focusFirstInput(textareas[textareas.length - 1]); + } + const inputs = document.querySelectorAll( + 'form#formModel input:not([disabled]):not([type="checkbox"])' + ); + const input = inputs[0]; + if (!input) return; + + focusFirstInput(input); + } catch (error) { + console.error(error); } + form.addEventListener('keyup', function (evt) { + if (evt.key === 'Enter') { + const input = evt.target; + if (input.type == 'textarea' && evt.shiftKey) { + evt.preventDefault(); + let { selectionStart, selectionEnd } = input; + input.value = + input.value.substring(0, selectionStart) + + '\n' + + input.value.substring(selectionEnd); + selectionStart = selectionEnd = selectionStart + 1; + return; + } + evt.preventDefault(); + that.onSubmit(); + } + }); }, }; diff --git a/src/boot/quasar.js b/src/boot/quasar.js index 01fe68d8b..547517682 100644 --- a/src/boot/quasar.js +++ b/src/boot/quasar.js @@ -1,15 +1,18 @@ +import axios from 'axios'; import { boot } from 'quasar/wrappers'; import qFormMixin from './qformMixin'; import keyShortcut from './keyShortcut'; -import useNotify from 'src/composables/useNotify.js'; -import { CanceledError } from 'axios'; - -const { notify } = useNotify(); +import { QForm } from 'quasar'; +import { QLayout } from 'quasar'; +import mainShortcutMixin from './mainShortcutMixin'; +import { useCau } from 'src/composables/useCau'; export default boot(({ app }) => { - app.mixin(qFormMixin); + QForm.mixins = [qFormMixin]; + QLayout.mixins = [mainShortcutMixin]; + app.directive('shortcut', keyShortcut); - app.config.errorHandler = (error) => { + app.config.errorHandler = async (error) => { let message; const response = error.response; const responseData = response?.data; @@ -40,12 +43,12 @@ export default boot(({ app }) => { } console.error(error); - if (error instanceof CanceledError) { + if (error instanceof axios.CanceledError) { const env = process.env.NODE_ENV; if (env && env !== 'development') return; message = 'Duplicate request'; } - notify(message ?? 'globals.error', 'negative', 'error'); + await useCau(response, message); }; }); diff --git a/src/components/CreateNewCityForm.vue b/src/components/CreateNewCityForm.vue index e51a18ef8..bf3830522 100644 --- a/src/components/CreateNewCityForm.vue +++ b/src/components/CreateNewCityForm.vue @@ -40,6 +40,7 @@ const onDataSaved = (...args) => { url-create="towns" model="city" @on-data-saved="onDataSaved" + data-cy="newCityForm" > @@ -47,11 +48,15 @@ const onDataSaved = (...args) => { :label="t('Name')" v-model="data.name" :rules="validate('city.name')" + required + data-cy="cityName" /> diff --git a/src/components/CreateNewPostcodeForm.vue b/src/components/CreateNewPostcodeForm.vue index 907e49499..c656fcb2f 100644 --- a/src/components/CreateNewPostcodeForm.vue +++ b/src/components/CreateNewPostcodeForm.vue @@ -21,15 +21,13 @@ const postcodeFormData = reactive({ provinceFk: null, townFk: null, }); - const townsFetchDataRef = ref(false); -const countriesFetchDataRef = ref(false); -const provincesFetchDataRef = ref(false); -const countriesOptions = ref([]); +const townFilter = ref({}); + +const countriesRef = ref(false); const provincesOptions = ref([]); const townsOptions = ref([]); const town = ref({}); -const townFilter = ref({}); const countryFilter = ref({}); function onDataSaved(formData) { @@ -42,32 +40,71 @@ function onDataSaved(formData) { ({ id }) => id === formData.provinceFk ); newPostcode.province = provinceObject?.name; - const countryObject = countriesOptions.value.find( + const countryObject = countriesRef.value.opts.find( ({ id }) => id === formData.countryFk ); newPostcode.country = countryObject?.name; emit('onDataSaved', newPostcode); } +async function setCountry(countryFk, data) { + data.townFk = null; + data.provinceFk = null; + data.countryFk = countryFk; + await fetchTowns(); +} + +// Province + +async function handleProvinces(data) { + provincesOptions.value = data; + if (postcodeFormData.countryFk) { + await fetchTowns(); + } +} +async function setProvince(id, data) { + if (data.provinceFk === id) return; + const newProvince = provincesOptions.value.find((province) => province.id == id); + if (newProvince) data.countryFk = newProvince.countryFk; + postcodeFormData.provinceFk = id; + await fetchTowns(); +} +async function onProvinceCreated(data) { + postcodeFormData.provinceFk = data.id; +} +function provinceByCountry(countryFk = postcodeFormData.countryFk) { + return provincesOptions.value + .filter((province) => province.countryFk === countryFk) + .map(({ id }) => id); +} + +// Town +async function handleTowns(data) { + townsOptions.value = data; +} +function setTown(newTown, data) { + town.value = newTown; + data.provinceFk = newTown?.provinceFk ?? newTown; + data.countryFk = newTown?.province?.countryFk ?? newTown; +} async function onCityCreated(newTown, formData) { - await provincesFetchDataRef.value.fetch(); newTown.province = provincesOptions.value.find( (province) => province.id === newTown.provinceFk ); formData.townFk = newTown; setTown(newTown, formData); } - -function setTown(newTown, data) { - town.value = newTown; - data.provinceFk = newTown?.provinceFk ?? newTown; - data.countryFk = newTown?.province?.countryFk ?? newTown; -} - -async function setCountry(countryFk, data) { - data.townFk = null; - data.provinceFk = null; - data.countryFk = countryFk; +async function fetchTowns(countryFk = postcodeFormData.countryFk) { + if (!countryFk) return; + const provinces = postcodeFormData.provinceFk + ? [postcodeFormData.provinceFk] + : provinceByCountry(); + townFilter.value.where = { + provinceFk: { + inq: provinces, + }, + }; + await townsFetchDataRef.value?.fetch(); } async function filterTowns(name) { @@ -80,65 +117,9 @@ async function filterTowns(name) { await townsFetchDataRef.value?.fetch(); } } -async function filterCountries(name) { - if (name !== '') { - countryFilter.value.where = { - name: { - like: `%${name}%`, - }, - }; - await countriesFetchDataRef.value?.fetch(); - } -} - -async function fetchTowns(countryFk) { - if (!countryFk) return; - townFilter.value.where = { - provinceFk: { - inq: provincesOptions.value.map(({ id }) => id), - }, - }; - await townsFetchDataRef.value?.fetch(); -} - -async function handleProvinces(data) { - provincesOptions.value = data; - if (postcodeFormData.countryFk) { - await fetchTowns(postcodeFormData.countryFk); - } -} -async function handleTowns(data) { - townsOptions.value = data; -} - -async function handleCountries(data) { - countriesOptions.value = data; -} - -async function setProvince(id, data) { - const newProvince = provincesOptions.value.find((province) => province.id == id); - if (!newProvince) return; - - data.countryFk = newProvince.countryFk; -} - -async function onProvinceCreated(data) { - await provincesFetchDataRef.value.fetch({ - where: { countryFk: postcodeFormData.countryFk }, - }); - postcodeFormData.provinceFk = data.id; -} - - @@ -217,21 +192,31 @@ async function onProvinceCreated(data) { :country-fk="data.countryFk" :province-selected="data.provinceFk" @update:model-value="(value) => setProvince(value, data)" + @update:options=" + (data) => { + provincesOptions = data; + } + " v-model="data.provinceFk" - @on-province-fetched="handleProvinces" @on-province-created="onProvinceCreated" + required /> setCountry(value, data)" + data-cy="locationCountry" /> diff --git a/src/components/CreateNewProvinceForm.vue b/src/components/CreateNewProvinceForm.vue index a0e681f77..d35690eeb 100644 --- a/src/components/CreateNewProvinceForm.vue +++ b/src/components/CreateNewProvinceForm.vue @@ -2,7 +2,6 @@ import { computed, reactive, ref } from 'vue'; import { useI18n } from 'vue-i18n'; -import FetchData from 'components/FetchData.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; @@ -21,15 +20,11 @@ const $props = defineProps({ type: Number, default: null, }, - provinces: { - type: Array, - default: () => [], - }, }); -const autonomiesOptions = ref([]); +const autonomiesRef = ref([]); const onDataSaved = (dataSaved, requestResponse) => { - requestResponse.autonomy = autonomiesOptions.value.find( + requestResponse.autonomy = autonomiesRef.value.opts.find( (autonomy) => autonomy.id == requestResponse.autonomyFk ); emit('onDataSaved', dataSaved, requestResponse); @@ -43,16 +38,6 @@ const where = computed(() => { - (autonomiesOptions = data)" - auto-load - :filter="{ - where, - }" - url="Autonomies/location" - :sort-by="['name ASC']" - :limit="30" - /> { :label="t('Name')" v-model="data.name" :rules="validate('province.name')" + required + data-cy="provinceName" /> { hide-selected option-label="label" v-model="selectedField" + data-cy="field-to-edit" /> diff --git a/src/components/FormModel.vue b/src/components/FormModel.vue index bfb470c48..c569f2553 100644 --- a/src/components/FormModel.vue +++ b/src/components/FormModel.vue @@ -110,6 +110,7 @@ const originalData = ref({}); const formData = computed(() => state.get(modelValue)); const defaultButtons = computed(() => ({ save: { + dataCy: 'saveDefaultBtn', color: 'primary', icon: 'save', label: 'globals.save', @@ -117,6 +118,7 @@ const defaultButtons = computed(() => ({ type: 'submit', }, reset: { + dataCy: 'resetDefaultBtn', color: 'primary', icon: 'restart_alt', label: 'globals.reset', @@ -207,7 +209,9 @@ async function save() { isLoading.value = true; try { formData.value = trimData(formData.value); - const body = $props.mapper ? $props.mapper(formData.value) : formData.value; + const body = $props.mapper + ? $props.mapper(formData.value, originalData.value) + : formData.value; const method = $props.urlCreate ? 'post' : 'patch'; const url = $props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url; @@ -289,6 +293,7 @@ defineExpose({ class="q-pa-md" :style="maxWidth ? 'max-width: ' + maxWidth : ''" id="formModel" + :prevent-submit="$attrs['prevent-submit']" > diff --git a/src/components/ItemsFilterPanel.vue b/src/components/ItemsFilterPanel.vue index 405577095..084feb377 100644 --- a/src/components/ItemsFilterPanel.vue +++ b/src/components/ItemsFilterPanel.vue @@ -9,6 +9,8 @@ import VnSelect from 'components/common/VnSelect.vue'; import VnFilterPanelChip from 'components/ui/VnFilterPanelChip.vue'; import axios from 'axios'; +import { getParamWhere } from 'src/filters'; +import { useRoute } from 'vue-router'; const { t } = useI18n(); const props = defineProps({ @@ -26,28 +28,21 @@ const props = defineProps({ }, }); -const itemCategories = ref([]); -const selectedCategoryFk = ref(null); -const selectedTypeFk = ref(null); +const route = useRoute(); + const itemTypesOptions = ref([]); const suppliersOptions = ref([]); const tagOptions = ref([]); const tagValues = ref([]); +const categoryList = ref(null); +const selectedCategoryFk = ref(getParamWhere(route.query.table, 'categoryFk', false)); +const selectedTypeFk = ref(getParamWhere(route.query.table, 'typeFk', false)); -const categoryList = computed(() => { - return (itemCategories.value || []) - .filter((category) => category.display) - .map((category) => ({ - ...category, - icon: `vn:${(category.icon || '').split('-')[1]}`, - })); -}); - -const selectedCategory = computed(() => - (itemCategories.value || []).find( +const selectedCategory = computed(() => { + return (categoryList.value || []).find( (category) => category?.id === selectedCategoryFk.value - ) -); + ); +}); const selectedType = computed(() => { return (itemTypesOptions.value || []).find( @@ -87,7 +82,7 @@ const applyTags = (params, search) => { search(); }; -const fetchItemTypes = async (id) => { +const fetchItemTypes = async (id = selectedCategoryFk.value) => { const filter = { fields: ['id', 'name', 'categoryFk'], where: { categoryFk: id }, @@ -126,15 +121,19 @@ const removeTag = (index, params, search) => { (tagValues.value || []).splice(index, 1); applyTags(params, search); }; +const setCategoryList = (data) => { + categoryList.value = (data || []) + .filter((category) => category.display) + .map((category) => ({ + ...category, + icon: `vn:${(category.icon || '').split('-')[1]}`, + })); + fetchItemTypes(); +}; - (itemCategories = data)" - /> + diff --git a/src/components/VnSelectProvince.vue b/src/components/VnSelectProvince.vue index bfc4ab7b7..d73ee964e 100644 --- a/src/components/VnSelectProvince.vue +++ b/src/components/VnSelectProvince.vue @@ -7,7 +7,7 @@ import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import FetchData from 'components/FetchData.vue'; import CreateNewProvinceForm from './CreateNewProvinceForm.vue'; -const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched']); +const emit = defineEmits(['onProvinceCreated', 'onProvinceFetched', 'update:options']); const $props = defineProps({ countryFk: { type: Number, @@ -41,6 +41,7 @@ async function onProvinceCreated(_, data) { } async function handleProvinces(data) { provincesOptions.value = data; + emit('update:options', data); } watch( @@ -64,11 +65,11 @@ watch( auto-load /> { : $props.defaultMode; stateStore.rightDrawer = quasar.screen.gt.xs; columnsVisibilitySkipped.value = [ - ...splittedColumns.value.columns.filter((c) => !c.visible).map((c) => c.name), + ...splittedColumns.value.columns + .filter((c) => c.visible === false) + .map((c) => c.name), ...['tableActions'], ]; createForm.value = $props.create; @@ -610,6 +612,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) { $props.rowClick && $props.rowClick(row); } " + style="height: 100%" > { diff --git a/src/components/common/VnDateBadge.vue b/src/components/common/VnDateBadge.vue new file mode 100644 index 000000000..fd6c9e8a4 --- /dev/null +++ b/src/components/common/VnDateBadge.vue @@ -0,0 +1,31 @@ + + + + {{ formatShippedDate(date) }} + + diff --git a/src/components/common/VnInputNumber.vue b/src/components/common/VnInputNumber.vue index 1cad6c245..165cfae3d 100644 --- a/src/components/common/VnInputNumber.vue +++ b/src/components/common/VnInputNumber.vue @@ -1,13 +1,28 @@ - - + { + const val = evt.target.value; + if (positive && val < 0) return (model = 0); + const [, decimal] = val.split('.'); + if (val && decimal?.length > decimalPlaces) + model = parseFloat(val).toFixed(decimalPlaces); + } + " + /> diff --git a/src/components/common/VnLocation.vue b/src/components/common/VnLocation.vue index 6de402a26..a8840f243 100644 --- a/src/components/common/VnLocation.vue +++ b/src/components/common/VnLocation.vue @@ -2,7 +2,7 @@ import CreateNewPostcode from 'src/components/CreateNewPostcodeForm.vue'; import VnSelectDialog from 'components/common/VnSelectDialog.vue'; import { useI18n } from 'vue-i18n'; -import { ref } from 'vue'; +import { computed } from 'vue'; import { useAttrs } from 'vue'; import { useRequired } from 'src/composables/useRequired'; const { t } = useI18n(); @@ -43,7 +43,7 @@ const formatLocation = (obj, properties) => { return filteredParts.join(', '); }; -const modelValue = ref( +const modelValue = computed(() => props.location ? formatLocation(props.location, locationProperties) : null ); @@ -75,7 +75,6 @@ const handleModelValue = (data) => { :input-debounce="300" :class="{ required: isRequired }" v-bind="$attrs" - clearable :emit-value="false" :tooltip="t('Create new location')" :rules="mixinRules" diff --git a/src/components/common/VnLog.vue b/src/components/common/VnLog.vue index 8c71c0997..9eca3c711 100644 --- a/src/components/common/VnLog.vue +++ b/src/components/common/VnLog.vue @@ -238,6 +238,7 @@ async function openPointRecord(id, modelLog) { pointRecord.value = parseProps(propNames, locale, data); } async function setLogTree(data) { + if (!data) return; logTree.value = getLogTree(data); } diff --git a/src/components/common/VnSelect.vue b/src/components/common/VnSelect.vue index 571faad64..758fb9228 100644 --- a/src/components/common/VnSelect.vue +++ b/src/components/common/VnSelect.vue @@ -4,11 +4,19 @@ import { useI18n } from 'vue-i18n'; import { useArrayData } from 'src/composables/useArrayData'; import { useRequired } from 'src/composables/useRequired'; import dataByOrder from 'src/utils/dataByOrder'; +import { QItemLabel } from 'quasar'; const emit = defineEmits(['update:modelValue', 'update:options', 'remove']); const $attrs = useAttrs(); const { t } = useI18n(); -const { isRequired, requiredFieldRule } = useRequired($attrs); + +const isRequired = computed(() => { + return useRequired($attrs).isRequired; +}); +const requiredFieldRule = computed(() => { + return useRequired($attrs).requiredFieldRule; +}); + const $props = defineProps({ modelValue: { type: [String, Number, Object], @@ -26,6 +34,10 @@ const $props = defineProps({ type: String, default: 'id', }, + optionCaption: { + type: String, + default: null, + }, optionFilter: { type: String, default: null, @@ -94,6 +106,10 @@ const $props = defineProps({ type: String, default: null, }, + isOutlined: { + type: Boolean, + default: false, + }, }); const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])]; @@ -108,6 +124,15 @@ const noOneOpt = ref({ [optionValue.value]: false, [optionLabel.value]: noOneText, }); +const styleAttrs = computed(() => { + return $props.isOutlined + ? { + dense: true, + outlined: true, + rounded: true, + } + : {}; +}); const isLoading = ref(false); const useURL = computed(() => $props.url); const value = computed({ @@ -261,7 +286,7 @@ async function onScroll({ to, direction, from, index }) { defineExpose({ opts: myOptions }); function handleKeyDown(event) { - if (event.key === 'Tab') { + if (event.key === 'Tab' && !event.shiftKey) { event.preventDefault(); const inputValue = vnSelectRef.value?.inputValue; @@ -279,6 +304,17 @@ function handleKeyDown(event) { } vnSelectRef.value?.hidePopup(); } + + const focusableElements = document.querySelectorAll( + 'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])' + ); + const currentIndex = Array.prototype.indexOf.call( + focusableElements, + event.target + ); + if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) { + focusableElements[currentIndex + 1].focus(); + } } } @@ -289,9 +325,8 @@ function handleKeyDown(event) { :options="myOptions" :option-label="optionLabel" :option-value="optionValue" - v-bind="$attrs" + v-bind="{ ...$attrs, ...styleAttrs }" @filter="filterHandler" - @keydown="handleKeyDown" :emit-value="nullishToTrue($attrs['emit-value'])" :map-options="nullishToTrue($attrs['map-options'])" :use-input="nullishToTrue($attrs['use-input'])" @@ -306,13 +341,15 @@ function handleKeyDown(event) { :input-debounce="useURL ? '300' : '0'" :loading="isLoading" @virtual-scroll="onScroll" + @keydown="handleKeyDown" :data-cy="$attrs.dataCy ?? $attrs.label + '_select'" + :data-url="url" > - + { value = null; emit('remove'); @@ -323,7 +360,37 @@ function handleKeyDown(event) { /> - + + { + value = null; + emit('remove'); + } + " + class="cursor-pointer" + size="xs" + /> + + + + + + + + {{ opt[optionLabel] }} + + + + {{ opt[optionLabel] }} + + + {{ `#${opt[optionCaption] || opt[optionValue]}` }} + + + diff --git a/src/components/common/VnSelectDialog.vue b/src/components/common/VnSelectDialog.vue index 350aa9272..12322c3fa 100644 --- a/src/components/common/VnSelectDialog.vue +++ b/src/components/common/VnSelectDialog.vue @@ -43,6 +43,7 @@ const isAllowedToCreate = computed(() => { > +import { computed, useAttrs } from 'vue'; +import VnSelect from 'components/common/VnSelect.vue'; +import VnAvatar from 'src/components/ui/VnAvatar.vue'; + +const emit = defineEmits(['update:modelValue']); +const $props = defineProps({ + hasAvatar: { + type: Boolean, + default: false, + }, + hasInfo: { + type: Boolean, + default: false, + }, + modelValue: { + type: [String, Number, Object], + default: null, + }, +}); + +const $attrs = useAttrs(); + +const value = computed({ + get() { + return $props.modelValue; + }, + set(val) { + emit('update:modelValue', val); + }, +}); + +const url = computed(() => { + let url = 'Workers/search'; + const { departmentCodes } = $attrs.params ?? {}; + if (!departmentCodes) return url; + const params = new URLSearchParams({ + departmentCodes: JSON.stringify(departmentCodes), + }); + + return url.concat(`?${params.toString()}`); +}); + + + + + + + + + + {{ $t($props.hasInfo) }} + + + + + + + {{ scope.opt.name }} + + + {{ scope.opt.nickname }} + + + {{ scope.opt.nickname }}, {{ scope.opt.code }} + + + + + + + + +es: + Responsible for approving invoices: Responsable de aprobar las facturas + diff --git a/src/components/ui/CardDescriptor.vue b/src/components/ui/CardDescriptor.vue index 83af77442..f4c0091d2 100644 --- a/src/components/ui/CardDescriptor.vue +++ b/src/components/ui/CardDescriptor.vue @@ -222,8 +222,8 @@ const toModule = computed(() => /> - - diff --git a/src/pages/Claim/Card/ClaimSummary.vue b/src/pages/Claim/Card/ClaimSummary.vue index edfa52b4b..8939a0785 100644 --- a/src/pages/Claim/Card/ClaimSummary.vue +++ b/src/pages/Claim/Card/ClaimSummary.vue @@ -120,13 +120,13 @@ const developmentColumns = ref([ { name: 'claimReason', label: 'claim.reason', - field: (row) => row.claimReason.description, + field: (row) => row.claimReason?.description, sortable: true, }, { name: 'claimResult', label: 'claim.result', - field: (row) => row.claimResult.description, + field: (row) => row.claimResult?.description, sortable: true, }, { @@ -345,12 +345,9 @@ function claimUrl(section) { {{ t(col.value) }} - {{ col.value }} + {{ + t(col.value) + }} { const { id } = route.params; - getAddressesData(id); getClientData(id); }); @@ -60,23 +60,10 @@ watch( () => route.params.id, (newValue) => { if (!newValue) return; - getAddressesData(newValue); getClientData(newValue); } ); -const getAddressesData = async (id) => { - try { - const { data } = await axios.get(`Clients/${id}/addresses`, { - params: { filter: JSON.stringify(addressFilter) }, - }); - addresses.value = data; - sortAddresses(); - } catch (error) { - return error; - } -}; - const getClientData = async (id) => { try { const { data } = await axios.get(`Clients/${id}`); @@ -101,9 +88,9 @@ const setDefault = (address) => { }); }; -const sortAddresses = () => { - if (!client.value || !addresses.value) return; - addresses.value = addresses.value.sort((a, b) => { +const sortAddresses = (data) => { + if (!client.value || !data) return; + addresses.value = data.sort((a, b) => { return isDefaultAddress(b) - isDefaultAddress(a); }); }; @@ -124,8 +111,17 @@ const toCustomerAddressEdit = (addressId) => { + - + ({ or: [ { id: val }, @@ -30,6 +30,13 @@ const exprBuilder = (param, value) => { and: [{ active: { neq: false } }, handleSalesModelValue(value)], }; }; + +function onBeforeSave(formData, originalData) { + return getUpdatedValues( + Object.keys(getDifferences(formData, originalData)), + formData + ); +} { @on-fetch="(data) => (businessTypes = data)" auto-load /> - + { :rules="validate('client.phone')" clearable v-model="data.phone" + data-cy="customerPhone" /> { /> - - - - - - - - {{ scope.opt?.name }} - {{ scope.opt?.nickname }}, - {{ scope.opt?.code }} - - - - + /> { url="Clients" :input-debounce="0" :label="t('customer.basicData.previousClient')" - :options="clients" :rules="validate('client.transferorFk')" emit-value map-options diff --git a/src/pages/Customer/Card/CustomerBillingData.vue b/src/pages/Customer/Card/CustomerBillingData.vue index a968d0ec8..48f729e29 100644 --- a/src/pages/Customer/Card/CustomerBillingData.vue +++ b/src/pages/Customer/Card/CustomerBillingData.vue @@ -28,12 +28,7 @@ const getBankEntities = (data, formData) => { - + [ - diff --git a/src/pages/Customer/Card/CustomerDescriptor.vue b/src/pages/Customer/Card/CustomerDescriptor.vue index 0753bee9e..98e53d568 100644 --- a/src/pages/Customer/Card/CustomerDescriptor.vue +++ b/src/pages/Customer/Card/CustomerDescriptor.vue @@ -36,7 +36,10 @@ const entityId = computed(() => { }); const data = ref(useCardDescription()); -const setData = (entity) => (data.value = useCardDescription(entity?.name, entity?.id)); +const setData = (entity) => { + data.value = useCardDescription(entity?.name, entity?.id); + if (customer.value) customer.value.webAccess = data.value?.account?.isActive; +}; const debtWarning = computed(() => { return customer.value?.debt > customer.value?.credit ? 'negative' : 'primary'; }); diff --git a/src/pages/Customer/Card/CustomerFiscalData.vue b/src/pages/Customer/Card/CustomerFiscalData.vue index 6c5086149..aff7deda4 100644 --- a/src/pages/Customer/Card/CustomerFiscalData.vue +++ b/src/pages/Customer/Card/CustomerFiscalData.vue @@ -34,7 +34,6 @@ function handleLocation(data, location) { /> @@ -68,6 +67,7 @@ function handleLocation(data, location) { option-label="vat" option-value="id" v-model="data.sageTaxTypeFk" + data-cy="sageTaxTypeFk" :required="data.isTaxDataChecked" /> @@ -109,7 +110,7 @@ function handleLocation(data, location) { - + {{ t('whenActivatingIt') }} @@ -168,7 +169,6 @@ es: Active: Activo Frozen: Congelado Has to invoice: Factura - Vies: Vies Notify by email: Notificar vía e-mail Invoice by address: Facturar por consignatario Is equalizated: Recargo de equivalencia diff --git a/src/pages/Customer/Card/CustomerSummary.vue b/src/pages/Customer/Card/CustomerSummary.vue index 4fa7b9bdc..6650ea395 100644 --- a/src/pages/Customer/Card/CustomerSummary.vue +++ b/src/pages/Customer/Card/CustomerSummary.vue @@ -1,12 +1,11 @@ - - - - - - - - - - + { + const unpaid = data.length == 1; + initialData = { ...data[0], unpaid }; + } + " + /> + + + - - - + - + - + - - €€ - - - + + + diff --git a/src/pages/Customer/Card/CustomerWebAccess.vue b/src/pages/Customer/Card/CustomerWebAccess.vue index 1db32c752..3c4106846 100644 --- a/src/pages/Customer/Card/CustomerWebAccess.vue +++ b/src/pages/Customer/Card/CustomerWebAccess.vue @@ -25,12 +25,12 @@ async function hasCustomerRole() { - - + + diff --git a/src/pages/Customer/Card/CustomerWebPayment.vue b/src/pages/Customer/Card/CustomerWebPayment.vue index 482582078..0e2e690cc 100644 --- a/src/pages/Customer/Card/CustomerWebPayment.vue +++ b/src/pages/Customer/Card/CustomerWebPayment.vue @@ -1,9 +1,9 @@ - - - - - - - - {{ props.value }} - - - - - + (rows = data)" + auto-load + url="Clients/transactions" + /> + + + + + + + {{ props.value }} + + + + + - - {{ t('globals.noResults') }} - - - + + {{ t('globals.noResults') }} + + diff --git a/src/pages/Customer/CustomerFilter.vue b/src/pages/Customer/CustomerFilter.vue index 79d48a667..96f670542 100644 --- a/src/pages/Customer/CustomerFilter.vue +++ b/src/pages/Customer/CustomerFilter.vue @@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n'; import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnSelect from 'components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); defineProps({ @@ -65,19 +66,14 @@ const exprBuilder = (param, value) => { - { outlined rounded :input-debounce="0" - > - - - - {{ opt.name }} - - {{ opt.nickname }},{{ opt.code }} - - - - + /> - + { /> - + - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + { outlined rounded auto-load + /> + + + + - - - - - - - + + @@ -203,7 +186,6 @@ es: Salesperson: Comercial Province: Provincia City: Ciudad - More options: Más opciones Phone: Teléfono Email: Email Zone: Zona diff --git a/src/pages/Customer/CustomerList.vue b/src/pages/Customer/CustomerList.vue index 3cb17332c..69122f200 100644 --- a/src/pages/Customer/CustomerList.vue +++ b/src/pages/Customer/CustomerList.vue @@ -2,7 +2,6 @@ import { ref, computed, markRaw } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRouter } from 'vue-router'; -import VnSelect from 'src/components/common/VnSelect.vue'; import VnTable from 'components/VnTable/VnTable.vue'; import VnLocation from 'src/components/common/VnLocation.vue'; import VnSearchbar from 'components/ui/VnSearchbar.vue'; @@ -12,6 +11,7 @@ import RightMenu from 'src/components/common/RightMenu.vue'; import VnLinkPhone from 'src/components/ui/VnLinkPhone.vue'; import { toDate } from 'src/filters'; import CustomerFilter from './CustomerFilter.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const router = useRouter(); @@ -263,7 +263,7 @@ const columns = computed(() => [ }, { align: 'left', - label: t('customer.extendedList.tableVisibleColumns.isVies'), + label: t('globals.isVies'), name: 'isVies', columnFilter: { inWhere: true, @@ -421,40 +421,17 @@ function handleLocation(data, location) { auto-load > - - - - - - - - {{ scope.opt?.name }} - {{ scope.opt?.nickname }}, - {{ scope.opt?.code }} - - - - - + /> -import { onBeforeMount, reactive, ref } from 'vue'; +import { reactive, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import { useRoute, useRouter } from 'vue-router'; -import axios from 'axios'; import VnLocation from 'src/components/common/VnLocation.vue'; import FetchData from 'components/FetchData.vue'; import FormModel from 'components/FormModel.vue'; @@ -19,26 +18,10 @@ const router = useRouter(); const formInitialData = reactive({ isDefaultAddress: false }); -const urlCreate = ref(''); - const agencyModes = ref([]); const incoterms = ref([]); const customsAgents = ref([]); -onBeforeMount(() => { - urlCreate.value = `Clients/${route.params.id}/createAddress`; - getCustomsAgents(); -}); - -const getCustomsAgents = async () => { - const { data } = await axios.get('CustomsAgents'); - customsAgents.value = data; -}; - -const refreshData = () => { - getCustomsAgents(); -}; - const toCustomerAddress = () => { router.push({ name: 'CustomerAddress', @@ -54,9 +37,19 @@ function handleLocation(data, location) { data.provinceFk = provinceFk; data.countryFk = countryFk; } + +function onAgentCreated({ id, fiscalName }, data) { + customsAgents.value.push({ id, fiscalName }); + data.customsAgentFk = id; +} + (customsAgents = data)" + auto-load + url="CustomsAgents" + /> (agencyModes = data)" auto-load @@ -67,7 +60,7 @@ function handleLocation(data, location) { @@ -139,6 +132,7 @@ function handleLocation(data, location) { /> - + onAgentCreated(requestResponse, data) + " + /> diff --git a/src/pages/Customer/components/CustomerAddressEdit.vue b/src/pages/Customer/components/CustomerAddressEdit.vue index 0ea8315d6..150ef3b84 100644 --- a/src/pages/Customer/components/CustomerAddressEdit.vue +++ b/src/pages/Customer/components/CustomerAddressEdit.vue @@ -12,6 +12,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; const { t } = useI18n(); const route = useRoute(); @@ -144,7 +145,6 @@ function handleLocation(data, location) { :url="`Addresses/${route.params.addressId}`" @on-data-saved="onDataSaved()" auto-load - model="client" > - - - - - - - - - - + + + + + + + + + + {{ t('Notes') }} diff --git a/src/pages/Customer/components/CustomerNewPayment.vue b/src/pages/Customer/components/CustomerNewPayment.vue index 291f28642..c2c38b55a 100644 --- a/src/pages/Customer/components/CustomerNewPayment.vue +++ b/src/pages/Customer/components/CustomerNewPayment.vue @@ -189,6 +189,7 @@ async function getAmountPaid() { :url-create="urlCreate" :mapper="onBeforeSave" @on-data-saved="onDataSaved" + :prevent-submit="true" > @@ -303,7 +304,7 @@ async function getAmountPaid() { :label="t('globals.save')" :loading="formModelRef.isLoading" color="primary" - type="submit" + @click="formModelRef.save()" /> diff --git a/src/pages/Customer/locale/en.yml b/src/pages/Customer/locale/en.yml index 07ec53964..18cee7309 100644 --- a/src/pages/Customer/locale/en.yml +++ b/src/pages/Customer/locale/en.yml @@ -88,7 +88,6 @@ customer: businessTypeFk: Business type sageTaxTypeFk: Sage tax type sageTransactionTypeFk: Sage tr. type - isVies: Vies isTaxDataChecked: Verified data isFreezed: Freezed hasToInvoice: Invoice diff --git a/src/pages/Customer/locale/es.yml b/src/pages/Customer/locale/es.yml index 36a266497..b544f8ad7 100644 --- a/src/pages/Customer/locale/es.yml +++ b/src/pages/Customer/locale/es.yml @@ -90,7 +90,6 @@ customer: businessTypeFk: Tipo de negocio sageTaxTypeFk: Tipo de impuesto Sage sageTransactionTypeFk: Tipo tr. sage - isVies: Vies isTaxDataChecked: Datos comprobados isFreezed: Congelado hasToInvoice: Factura diff --git a/src/pages/Department/Card/DepartmentBasicData.vue b/src/pages/Department/Card/DepartmentBasicData.vue index 07bccd971..22ce06821 100644 --- a/src/pages/Department/Card/DepartmentBasicData.vue +++ b/src/pages/Department/Card/DepartmentBasicData.vue @@ -6,6 +6,7 @@ import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const route = useRoute(); const { t } = useI18n(); @@ -48,14 +49,9 @@ const { t } = useI18n(); /> - - + { dash /> diff --git a/src/pages/Entry/EntryBuysTableDialog.vue b/src/pages/Entry/EntryBuysTableDialog.vue index c885c2485..3975bff19 100644 --- a/src/pages/Entry/EntryBuysTableDialog.vue +++ b/src/pages/Entry/EntryBuysTableDialog.vue @@ -82,11 +82,11 @@ const entriesTableColumns = computed(() => [ @@ -126,7 +126,9 @@ const entriesTableColumns = computed(() => [ " unelevated > - {{ t('viewLabel') }} + {{ + t('myEntries.viewLabel') + }} diff --git a/src/pages/Entry/MyEntries.vue b/src/pages/Entry/MyEntries.vue index 2c37c2c42..91a29b190 100644 --- a/src/pages/Entry/MyEntries.vue +++ b/src/pages/Entry/MyEntries.vue @@ -101,7 +101,7 @@ const columns = computed(() => [ name: 'tableActions', actions: [ { - title: t('printLabels'), + title: t('myEntries.printLabels'), icon: 'print', isPrimary: true, action: (row) => printBuys(row.id), diff --git a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue index 209681b7c..fd5ea0931 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInBasicData.vue @@ -249,6 +249,7 @@ function deleteFile(dmsFk) { :options="currencies" option-value="id" option-label="code" + sort-by="id" /> -import { ref, computed } from 'vue'; -import { useRouter } from 'vue-router'; +import { ref, computed, capitalize } from 'vue'; +import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; import { useArrayData } from 'src/composables/useArrayData'; -import { useCapitalize } from 'src/composables/useCapitalize'; import CrudModel from 'src/components/CrudModel.vue'; import FetchData from 'src/components/FetchData.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; -const { push, currentRoute } = useRouter(); +const route = useRoute(); const { t } = useI18n(); -const invoiceId = +currentRoute.value.params.id; const arrayData = useArrayData(); const invoiceIn = computed(() => arrayData.store.data); const invoiceInCorrectionRef = ref(); const filter = { include: { relation: 'invoiceIn' }, - where: { correctingFk: invoiceId }, + where: { correctingFk: route.params.id }, }; const columns = computed(() => [ { @@ -31,7 +29,7 @@ const columns = computed(() => [ }, { name: 'type', - label: useCapitalize(t('globals.type')), + label: capitalize(t('globals.type')), field: (row) => row.cplusRectificationTypeFk, options: cplusRectificationTypes.value, model: 'cplusRectificationTypeFk', @@ -43,10 +41,10 @@ const columns = computed(() => [ }, { name: 'class', - label: useCapitalize(t('globals.class')), - field: (row) => row.siiTypeInvoiceOutFk, - options: siiTypeInvoiceOuts.value, - model: 'siiTypeInvoiceOutFk', + label: capitalize(t('globals.class')), + field: (row) => row.siiTypeInvoiceInFk, + options: siiTypeInvoiceIns.value, + model: 'siiTypeInvoiceInFk', optionValue: 'id', optionLabel: 'code', sortable: true, @@ -55,7 +53,7 @@ const columns = computed(() => [ }, { name: 'reason', - label: useCapitalize(t('globals.reason')), + label: capitalize(t('globals.reason')), field: (row) => row.invoiceCorrectionTypeFk, options: invoiceCorrectionTypes.value, model: 'invoiceCorrectionTypeFk', @@ -67,13 +65,10 @@ const columns = computed(() => [ }, ]); const cplusRectificationTypes = ref([]); -const siiTypeInvoiceOuts = ref([]); +const siiTypeInvoiceIns = ref([]); const invoiceCorrectionTypes = ref([]); -const rowsSelected = ref([]); const requiredFieldRule = (val) => val || t('globals.requiredField'); - -const onSave = (data) => data.deletes && push(`/invoice-in/${invoiceId}/summary`); data.deletes && push(`/invoice-in/${invoiceId}/summary` auto-load /> (siiTypeInvoiceOuts = data)" + @on-fetch="(data) => (siiTypeInvoiceIns = data)" auto-load /> data.deletes && push(`/invoice-in/${invoiceId}/summary` url="InvoiceInCorrections" :filter="filter" auto-load - v-model:selected="rowsSelected" primary-key="correctingFk" - @save-changes="onSave" + :default-remove="false" > @@ -121,8 +113,17 @@ const onSave = (data) => data.deletes && push(`/invoice-in/${invoiceId}/summary` :options="col.options" :option-value="col.optionValue" :option-label="col.optionLabel" - :readonly="row.invoiceIn.isBooked" - /> + :disable="row.invoiceIn.isBooked" + :filter-options="['description']" + > + + + + {{ opt.description }} + + + + @@ -134,8 +135,20 @@ const onSave = (data) => data.deletes && push(`/invoice-in/${invoiceId}/summary` :option-value="col.optionValue" :option-label="col.optionLabel" :rules="[requiredFieldRule]" - :readonly="row.invoiceIn.isBooked" - /> + :filter-options="['code', 'description']" + :disable="row.invoiceIn.isBooked" + > + + + + {{ opt.code }} - + {{ opt.description }} + + + + @@ -147,7 +160,7 @@ const onSave = (data) => data.deletes && push(`/invoice-in/${invoiceId}/summary` :option-value="col.optionValue" :option-label="col.optionLabel" :rules="[requiredFieldRule]" - :readonly="row.invoiceIn.isBooked" + :disable="row.invoiceIn.isBooked" /> @@ -155,7 +168,6 @@ const onSave = (data) => data.deletes && push(`/invoice-in/${invoiceId}/summary` - es: Original invoice: Factura origen diff --git a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue index 92f3fffca..cb8a45833 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInDescriptor.vue @@ -1,5 +1,5 @@ { @@ -138,8 +130,8 @@ const formatOpt = (row, { model, options }, prop) => { @@ -154,7 +146,7 @@ const formatOpt = (row, { model, options }, prop) => { {{ getTotal(rows, 'net') }} - {{ getTotal(rows, 'stems') }} + {{ getTotal(rows, 'stems', { decimalPlaces: 0 }) }} @@ -174,7 +166,9 @@ const formatOpt = (row, { model, options }, prop) => { v-model="props.row['intrastatFk']" :options="intrastats" option-value="id" - option-label="description" + :option-label=" + (row) => `${row.id}:${row.description}` + " :filter-options="['id', 'description']" > @@ -248,11 +242,6 @@ const formatOpt = (row, { model, options }, prop) => { } } - en: amount: Amount @@ -261,7 +250,7 @@ const formatOpt = (row, { model, options }, prop) => { country: Country es: Code: Código - amount: Cantidad + amount: Valor mercancía net: Neto stems: Tallos country: País diff --git a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue index 08fc11f69..115a33208 100644 --- a/src/pages/InvoiceIn/Card/InvoiceInSummary.vue +++ b/src/pages/InvoiceIn/Card/InvoiceInSummary.vue @@ -26,14 +26,14 @@ const intrastatTotals = ref({ amount: 0, net: 0, stems: 0 }); const vatColumns = ref([ { name: 'expense', - label: 'invoiceIn.summary.expense', + label: 'InvoiceIn.summary.expense', field: (row) => row.expenseFk, sortable: true, align: 'left', }, { name: 'landed', - label: 'invoiceIn.summary.taxableBase', + label: 'InvoiceIn.summary.taxableBase', field: (row) => row.taxableBase, format: (value) => toCurrency(value), sortable: true, @@ -41,7 +41,7 @@ const vatColumns = ref([ }, { name: 'vat', - label: 'invoiceIn.summary.sageVat', + label: 'InvoiceIn.summary.sageVat', field: (row) => { if (row.taxTypeSage) return `#${row.taxTypeSage.id} : ${row.taxTypeSage.vat}`; }, @@ -51,7 +51,7 @@ const vatColumns = ref([ }, { name: 'transaction', - label: 'invoiceIn.summary.sageTransaction', + label: 'InvoiceIn.summary.sageTransaction', field: (row) => { if (row.transactionTypeSage) return `#${row.transactionTypeSage.id} : ${row.transactionTypeSage?.transaction}`; @@ -62,7 +62,7 @@ const vatColumns = ref([ }, { name: 'rate', - label: 'invoiceIn.summary.rate', + label: 'InvoiceIn.summary.rate', field: (row) => taxRate(row.taxableBase, row.taxTypeSage?.rate), format: (value) => toCurrency(value), sortable: true, @@ -70,7 +70,7 @@ const vatColumns = ref([ }, { name: 'currency', - label: 'invoiceIn.summary.currency', + label: 'InvoiceIn.summary.currency', field: (row) => row.foreignValue, format: (val) => val && toCurrency(val, currency.value), sortable: true, @@ -81,21 +81,21 @@ const vatColumns = ref([ const dueDayColumns = ref([ { name: 'date', - label: 'invoiceIn.summary.dueDay', + label: 'InvoiceIn.summary.dueDay', field: (row) => toDate(row.dueDated), sortable: true, align: 'left', }, { name: 'bank', - label: 'invoiceIn.summary.bank', + label: 'InvoiceIn.summary.bank', field: (row) => row.bank.bank, sortable: true, align: 'left', }, { name: 'amount', - label: 'invoiceIn.list.amount', + label: 'InvoiceIn.list.amount', field: (row) => row.amount, format: (value) => toCurrency(value), sortable: true, @@ -103,7 +103,7 @@ const dueDayColumns = ref([ }, { name: 'landed', - label: 'invoiceIn.summary.foreignValue', + label: 'InvoiceIn.summary.foreignValue', field: (row) => row.foreignValue, format: (val) => val && toCurrency(val, currency.value), sortable: true, @@ -114,7 +114,7 @@ const dueDayColumns = ref([ const intrastatColumns = ref([ { name: 'code', - label: 'invoiceIn.summary.code', + label: 'InvoiceIn.summary.code', field: (row) => { return `${row.intrastat.id}: ${row.intrastat?.description}`; }, @@ -123,21 +123,21 @@ const intrastatColumns = ref([ }, { name: 'amount', - label: 'invoiceIn.list.amount', + label: 'InvoiceIn.list.amount', field: (row) => toCurrency(row.amount), sortable: true, align: 'left', }, { name: 'net', - label: 'invoiceIn.summary.net', + label: 'InvoiceIn.summary.net', field: (row) => row.net, sortable: true, align: 'left', }, { name: 'stems', - label: 'invoiceIn.summary.stems', + label: 'InvoiceIn.summary.stems', field: (row) => row.stems, format: (value) => value, sortable: true, @@ -145,7 +145,7 @@ const intrastatColumns = ref([ }, { name: 'landed', - label: 'invoiceIn.summary.country', + label: 'InvoiceIn.summary.country', field: (row) => row.country?.code, format: (value) => value, sortable: true, @@ -210,7 +210,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; /> @@ -221,14 +221,18 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; - + + @@ -239,21 +243,22 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; + @@ -263,18 +268,18 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; /> - + @@ -285,11 +290,11 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; - + `#/invoice-in/${entityId.value}/${param}`; :color="amountsNotMatch ? 'negative' : 'transparent'" :title=" amountsNotMatch - ? t('invoiceIn.summary.noMatch') - : t('invoiceIn.summary.dueTotal') + ? t('InvoiceIn.summary.noMatch') + : t('InvoiceIn.summary.dueTotal') " > {{ toCurrency(entity.totals.totalDueDay) }} @@ -309,7 +314,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; - + `#/invoice-in/${entityId.value}/${param}`; - + @@ -395,7 +400,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`; arrayData.store.data); -const invoiceId = +useRoute().params.id; const currency = computed(() => invoiceIn.value?.currency?.code); const expenses = ref([]); const sageTaxTypes = ref([]); @@ -39,9 +41,8 @@ const columns = computed(() => [ options: expenses.value, model: 'expenseFk', optionValue: 'id', - optionLabel: 'id', + optionLabel: (row) => `${row.id}: ${row.name}`, sortable: true, - tabIndex: 1, align: 'left', }, { @@ -50,7 +51,6 @@ const columns = computed(() => [ field: (row) => row.taxableBase, model: 'taxableBase', sortable: true, - tabIndex: 2, align: 'left', }, { @@ -60,9 +60,8 @@ const columns = computed(() => [ options: sageTaxTypes.value, model: 'taxTypeSageFk', optionValue: 'id', - optionLabel: 'id', + optionLabel: (row) => `${row.id}: ${row.vat}`, sortable: true, - tabindex: 3, align: 'left', }, { @@ -72,16 +71,14 @@ const columns = computed(() => [ options: sageTransactionTypes.value, model: 'transactionTypeSageFk', optionValue: 'id', - optionLabel: 'id', + optionLabel: (row) => `${row.id}: ${row.transaction}`, sortable: true, - tabIndex: 4, align: 'left', }, { name: 'rate', label: t('Rate'), sortable: true, - tabIndex: 5, field: (row) => taxRate(row, row.taxTypeSageFk), align: 'left', }, @@ -89,7 +86,6 @@ const columns = computed(() => [ name: 'foreignvalue', label: t('Foreign value'), sortable: true, - tabIndex: 6, field: (row) => row.foreignValue, align: 'left', }, @@ -106,7 +102,7 @@ const filter = { 'transactionTypeSageFk', ], where: { - invoiceInFk: invoiceId, + invoiceInFk: route.params.id, }, }; @@ -120,14 +116,20 @@ function taxRate(invoiceInTax) { const taxTypeSage = taxRateSelection?.rate ?? 0; const taxableBase = invoiceInTax?.taxableBase ?? 0; - return (taxTypeSage / 100) * taxableBase; + return ((taxTypeSage / 100) * taxableBase).toFixed(2); } -const formatOpt = (row, { model, options }, prop) => { - const obj = row[model]; - const option = options.find(({ id }) => id == obj); - return option ? `${obj}:${option[prop]}` : ''; -}; +function autocompleteExpense(evt, row, col) { + const val = evt.target.value; + if (!val) return; + + const param = isNaN(val) ? row[col.model] : val; + const lookup = expenses.value.find( + ({ id }) => id == useAccountShortToStandard(param) + ); + + if (lookup) row[col.model] = lookup; +} { data-key="InvoiceInTaxes" url="InvoiceInTaxes" :filter="filter" - :data-required="{ invoiceInFk: invoiceId }" + :data-required="{ invoiceInFk: $route.params.id }" auto-load v-model:selected="rowsSelected" - :go-to="`/invoice-in/${invoiceId}/due-day`" + :go-to="`/invoice-in/${$route.params.id}/due-day`" > { :option-label="col.optionLabel" :filter-options="['id', 'name']" :tooltip="t('Create a new expense')" + @keydown.tab="autocompleteExpense($event, row, col)" > @@ -187,13 +190,7 @@ const formatOpt = (row, { model, options }, prop) => { - {{ currency }} { :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'vat']" - :hide-selected="false" - :fill-input="false" - :display-value="formatOpt(row, col, 'vat')" + data-cy="vat-sageiva" > @@ -233,11 +228,6 @@ const formatOpt = (row, { model, options }, prop) => { :option-value="col.optionValue" :option-label="col.optionLabel" :filter-options="['id', 'transaction']" - :autofocus="col.tabIndex == 1" - input-debounce="0" - :hide-selected="false" - :fill-input="false" - :display-value="formatOpt(row, col, 'transaction')" > @@ -262,6 +252,16 @@ const formatOpt = (row, { model, options }, prop) => { }" :disable="!isNotEuro(currency)" v-model="row.foreignValue" + @update:model-value=" + async (val) => { + if (!isNotEuro(currency)) return; + row.taxableBase = await getExchange( + val, + row.currencyFk, + invoiceIn.issued + ); + } + " /> @@ -305,7 +305,7 @@ const formatOpt = (row, { model, options }, prop) => { v-model="props.row['expenseFk']" :options="expenses" option-value="id" - option-label="name" + :option-label="(row) => `${row.id}:${row.name}`" :filter-options="['id', 'name']" :tooltip="t('Create a new expense')" > @@ -339,7 +339,7 @@ const formatOpt = (row, { model, options }, prop) => { v-model="props.row['taxTypeSageFk']" :options="sageTaxTypes" option-value="id" - option-label="vat" + :option-label="(row) => `${row.id}:${row.vat}`" :filter-options="['id', 'vat']" > @@ -362,7 +362,9 @@ const formatOpt = (row, { model, options }, prop) => { v-model="props.row['transactionTypeSageFk']" :options="sageTransactionTypes" option-value="id" - option-label="transaction" + :option-label=" + (row) => `${row.id}:${row.transaction}` + " :filter-options="['id', 'transaction']" > @@ -418,11 +420,6 @@ const formatOpt = (row, { model, options }, prop) => { .bg { background-color: var(--vn-light-gray); } - -:deep(.q-table tr td:nth-child(n + 4):nth-child(-n + 5) input) { - display: none; -} - @media (max-width: $breakpoint-xs) { .q-dialog { .q-card { diff --git a/src/pages/InvoiceIn/InvoiceInCreate.vue b/src/pages/InvoiceIn/InvoiceInCreate.vue index c809e032b..200997f65 100644 --- a/src/pages/InvoiceIn/InvoiceInCreate.vue +++ b/src/pages/InvoiceIn/InvoiceInCreate.vue @@ -83,7 +83,7 @@ const redirectToInvoiceInBasicData = (__, { id }) => { @@ -97,10 +97,10 @@ const redirectToInvoiceInBasicData = (__, { id }) => { map-options hide-selected :required="true" - :rules="validate('invoiceIn.companyFk')" + :rules="validate('InvoiceIn.companyFk')" /> diff --git a/src/pages/InvoiceIn/InvoiceInFilter.vue b/src/pages/InvoiceIn/InvoiceInFilter.vue index d1c0856b5..653692026 100644 --- a/src/pages/InvoiceIn/InvoiceInFilter.vue +++ b/src/pages/InvoiceIn/InvoiceInFilter.vue @@ -1,41 +1,66 @@ - (activities = data)" - /> - - + + - {{ t(`params.${tag.label}`) }}: + {{ getLocale(tag.label) }}: {{ formatFn(tag.value) }} - + - + - + + + + + + handleDaysAgo(params, val)" + @remove="(val) => handleDaysAgo(params, val)" + /> @@ -44,20 +69,18 @@ const activities = ref([]); v-model="params.supplierFk" url="Suppliers" :fields="['id', 'nickname']" - :label="t('params.supplierFk')" - option-value="id" + :label="getLocale('supplierFk')" option-label="nickname" dense outlined rounded - :filter-options="['id', 'name']" /> + + + + + - - - - -en: - params: - search: Id or supplier name - supplierRef: Supplier ref. - supplierFk: Supplier - fi: Supplier fiscal id - clientFk: Customer - amount: Amount - created: Created - awb: AWB - dued: Dued - serialNumber: Serial Number - serial: Serial - account: Ledger account - isBooked: is booked - correctedFk: Rectified - issued: Issued - to: To - from: From - awbCode: AWB - correctingFk: Rectificative - supplierActivityFk: Supplier activity -es: - params: - search: Id o nombre proveedor - supplierRef: Ref. proveedor - supplierFk: Proveedor - clientFk: Cliente - fi: CIF proveedor - serialNumber: Num. serie - serial: Serie - awb: AWB - amount: Importe - issued: Emitida - isBooked: Contabilizada - account: Cuenta contable - created: Creada - dued: Vencida - correctedFk: Rectificada - correctingFk: Rectificativa - supplierActivityFk: Actividad proveedor - from: Desde - to: Hasta - From: Desde - To: Hasta - Amount: Importe - Issued: Fecha factura - Id or supplier: Id o proveedor - More options: Más opciones - diff --git a/src/pages/InvoiceIn/InvoiceInList.vue b/src/pages/InvoiceIn/InvoiceInList.vue index 8d658bee3..6469f446d 100644 --- a/src/pages/InvoiceIn/InvoiceInList.vue +++ b/src/pages/InvoiceIn/InvoiceInList.vue @@ -2,6 +2,7 @@ import { ref, computed, onMounted, onUnmounted } from 'vue'; import { useI18n } from 'vue-i18n'; import { useStateStore } from 'stores/useStateStore'; +import { useState } from 'src/composables/useState'; import { downloadFile } from 'src/composables/downloadFile'; import { toDate, toCurrency } from 'src/filters/index'; import InvoiceInFilter from './InvoiceInFilter.vue'; @@ -14,8 +15,10 @@ import VnTable from 'src/components/VnTable/VnTable.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue'; +import FetchData from 'src/components/FetchData.vue'; const stateStore = useStateStore(); +const user = useState().getUser(); const { viewSummary } = useSummaryDialog(); const { t } = useI18n(); @@ -23,7 +26,14 @@ onMounted(async () => (stateStore.rightDrawer = true)); onUnmounted(() => (stateStore.rightDrawer = false)); const tableRef = ref(); +const companies = ref([]); const cols = computed(() => [ + { + align: 'left', + name: 'isBooked', + label: t('InvoiceIn.isBooked'), + columnFilter: false, + }, { align: 'left', name: 'id', @@ -32,7 +42,7 @@ const cols = computed(() => [ { align: 'left', name: 'supplierFk', - label: t('invoiceIn.list.supplier'), + label: t('InvoiceIn.list.supplier'), columnFilter: { component: 'select', attrs: { @@ -45,16 +55,16 @@ const cols = computed(() => [ { align: 'left', name: 'supplierRef', - label: t('invoiceIn.list.supplierRef'), + label: t('InvoiceIn.list.supplierRef'), }, { align: 'left', name: 'serial', - label: t('invoiceIn.serial'), + label: t('InvoiceIn.serial'), }, { align: 'left', - label: t('invoiceIn.list.issued'), + label: t('InvoiceIn.list.issued'), name: 'issued', component: null, columnFilter: { @@ -64,21 +74,38 @@ const cols = computed(() => [ }, { align: 'left', - name: 'isBooked', - label: t('invoiceIn.isBooked'), - columnFilter: false, + label: t('InvoiceIn.list.dueDated'), + name: 'dueDated', + component: null, + columnFilter: { + component: 'date', + }, + format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.dueDated)), }, { align: 'left', name: 'awbCode', - label: t('invoiceIn.list.awb'), + label: t('InvoiceIn.list.awb'), }, { align: 'left', name: 'amount', - label: t('invoiceIn.list.amount'), + label: t('InvoiceIn.list.amount'), format: ({ amount }) => toCurrency(amount), }, + { + name: 'companyFk', + label: t('globals.company'), + columnFilter: { + component: 'select', + attrs: { + url: 'Companies', + fields: ['id', 'code'], + optionLabel: 'code', + }, + }, + format: (row) => row.code, + }, { align: 'right', name: 'tableActions', @@ -101,6 +128,7 @@ const cols = computed(() => [ ]); + (companies = data)" auto-load /> @@ -116,7 +144,7 @@ const cols = computed(() => [ urlCreate: 'InvoiceIns', title: t('globals.createInvoiceIn'), onDataSaved: ({ id }) => tableRef.redirect(id), - formInitialData: {}, + formInitialData: { companyFk: user.companyFk, issued: Date.vnNew() }, }" redirect="invoice-in" :columns="cols" @@ -151,7 +179,7 @@ const cols = computed(() => [ [ option-label="code" :required="true" /> - + diff --git a/src/pages/InvoiceIn/Serial/InvoiceInSerial.vue b/src/pages/InvoiceIn/Serial/InvoiceInSerial.vue index 4eb9fa69d..a8fb3b0c8 100644 --- a/src/pages/InvoiceIn/Serial/InvoiceInSerial.vue +++ b/src/pages/InvoiceIn/Serial/InvoiceInSerial.vue @@ -58,6 +58,14 @@ onBeforeMount(async () => { :right-search="false" :user-params="{ daysAgo }" :disable-option="{ card: true }" + :row-click=" + (row) => { + $router.push({ + name: 'InvoiceInList', + query: { table: JSON.stringify({ serial: row.serial }) }, + }); + } + " auto-load /> diff --git a/src/pages/InvoiceIn/Serial/InvoiceInSerialFilter.vue b/src/pages/InvoiceIn/Serial/InvoiceInSerialFilter.vue index 4f8c9d70b..19ed73e50 100644 --- a/src/pages/InvoiceIn/Serial/InvoiceInSerialFilter.vue +++ b/src/pages/InvoiceIn/Serial/InvoiceInSerialFilter.vue @@ -8,7 +8,11 @@ defineProps({ dataKey: { type: String, required: true } }); const { t } = useI18n(); - + {{ t(`params.${tag.label}`) }}: diff --git a/src/pages/InvoiceIn/locale/en.yml b/src/pages/InvoiceIn/locale/en.yml index b39511f29..ef7e31ac3 100644 --- a/src/pages/InvoiceIn/locale/en.yml +++ b/src/pages/InvoiceIn/locale/en.yml @@ -1,4 +1,4 @@ -invoiceIn: +InvoiceIn: serial: Serial isBooked: Is booked list: @@ -7,8 +7,11 @@ invoiceIn: supplierRef: Supplier ref. file: File issued: Issued + dueDated: Due dated awb: AWB amount: Amount + descriptor: + ticketList: Ticket list card: client: Client company: Company @@ -39,3 +42,9 @@ invoiceIn: net: Net stems: Stems country: Country + params: + search: Id or supplier name + account: Ledger account + correctingFk: Rectificative + correctedFk: Corrected + isBooked: Is booked diff --git a/src/pages/InvoiceIn/locale/es.yml b/src/pages/InvoiceIn/locale/es.yml index 5f483dd08..ed5943489 100644 --- a/src/pages/InvoiceIn/locale/es.yml +++ b/src/pages/InvoiceIn/locale/es.yml @@ -1,15 +1,17 @@ -invoiceIn: +InvoiceIn: serial: Serie isBooked: Contabilizada list: ref: Referencia supplier: Proveedor supplierRef: Ref. proveedor - shortIssued: F. emisión + issued: F. emisión + dueDated: F. vencimiento file: Fichero - issued: Fecha emisión awb: AWB amount: Importe + descriptor: + ticketList: Listado de tickets card: client: Cliente company: Empresa @@ -38,3 +40,8 @@ invoiceIn: net: Neto stems: Tallos country: País + params: + search: Id o nombre proveedor + account: Cuenta contable + correctingFk: Rectificativa + correctedFk: Rectificada diff --git a/src/pages/InvoiceOut/InvoiceOutFilter.vue b/src/pages/InvoiceOut/InvoiceOutFilter.vue index 9ce8cc254..dc1d833a2 100644 --- a/src/pages/InvoiceOut/InvoiceOutFilter.vue +++ b/src/pages/InvoiceOut/InvoiceOutFilter.vue @@ -83,36 +83,29 @@ const states = ref(); /> - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + @@ -149,5 +142,4 @@ es: Issued: Fecha emisión Created: Fecha creación Dued: Fecha vencimiento - More options: Más opciones diff --git a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue index 3fd3104bf..e6c689523 100644 --- a/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue +++ b/src/pages/InvoiceOut/InvoiceOutGlobalForm.vue @@ -115,6 +115,9 @@ onMounted(async () => { [ label: t('invoiceOutList.tableVisibleColumns.id'), chip: { condition: () => true }, isId: true, - columnFilter: { name: 'search' }, + columnFilter: { + name: 'id', + }, }, { align: 'left', @@ -60,8 +62,15 @@ const columns = computed(() => [ label: t('globals.reference'), isTitle: true, component: 'select', - attrs: { url: MODEL, optionLabel: 'ref', optionValue: 'id' }, + attrs: { + url: MODEL, + optionLabel: 'ref', + optionValue: 'ref', + }, columnField: { component: null }, + columnFilter: { + inWhere: true, + }, }, { align: 'left', @@ -77,8 +86,15 @@ const columns = computed(() => [ label: t('globals.client'), cardVisible: true, component: 'select', - attrs: { url: 'Clients', fields: ['id', 'name'] }, - columnField: { component: null }, + attrs: { + url: 'Clients', + fields: ['id', 'socialName'], + optionLabel: 'socialName', + optionValue: 'id', + }, + columnField: { + component: null, + }, }, { align: 'left', @@ -139,25 +155,22 @@ function openPdf(id) { } function downloadPdf() { - if (selectedRows.value.size === 0) return; - const selectedCardsArray = Array.from(selectedRows.value.values()); + if (selectedRows.value.size === 0) return; + const selectedCardsArray = Array.from(selectedRows.value.values()); - if (selectedRows.value.size === 1) { - const [invoiceOut] = selectedCardsArray; - openPdf(invoiceOut.id); - } else { - const invoiceOutIdsArray = selectedCardsArray.map( - (invoiceOut) => invoiceOut.id - ); - const invoiceOutIds = invoiceOutIdsArray.join(','); + if (selectedRows.value.size === 1) { + const [invoiceOut] = selectedCardsArray; + openPdf(invoiceOut.id); + } else { + const invoiceOutIdsArray = selectedCardsArray.map((invoiceOut) => invoiceOut.id); + const invoiceOutIds = invoiceOutIdsArray.join(','); - const params = { - ids: invoiceOutIds, - }; - - openReport(`${MODEL}/downloadZip`, params); - } + const params = { + ids: invoiceOutIds, + }; + openReport(`${MODEL}/downloadZip`, params); + } } watchEffect(selectedRows); diff --git a/src/pages/InvoiceOut/locale/es.yml b/src/pages/InvoiceOut/locale/es.yml index bf5126641..106168a5d 100644 --- a/src/pages/InvoiceOut/locale/es.yml +++ b/src/pages/InvoiceOut/locale/es.yml @@ -11,7 +11,7 @@ invoiceOutList: ref: Referencia issued: Fecha emisión created: F. creación - dueDate: F. máxima + dueDate: Fecha vencimiento invoiceOutSerial: Serial ticket: Ticket taxArea: Area diff --git a/src/pages/Item/Card/ItemBarcode.vue b/src/pages/Item/Card/ItemBarcode.vue index 197e9142f..6db5943c7 100644 --- a/src/pages/Item/Card/ItemBarcode.vue +++ b/src/pages/Item/Card/ItemBarcode.vue @@ -2,12 +2,15 @@ import { ref, nextTick } from 'vue'; import { useRoute } from 'vue-router'; import { useI18n } from 'vue-i18n'; +import axios from 'axios'; import CrudModel from 'src/components/CrudModel.vue'; import VnInput from 'src/components/common/VnInput.vue'; +import useNotify from 'src/composables/useNotify.js'; const route = useRoute(); const { t } = useI18n(); +const { notify } = useNotify(); const itemBarcodeRef = ref(null); @@ -23,6 +26,24 @@ const focusLastInput = () => { if (lastInput) lastInput.focus(); }); }; + +const removeRow = (row) => { + itemBarcodeRef.value.remove([row]); +}; + +const submit = async (rows) => { + const params = rows[rows.length - 1]; + let { data } = await axios.get('ItemBarcodes'); + const code = params.code; + + if (data.some((codes) => codes.code === code)) { + notify(t('Codes can not be repeated'), 'negative'); + itemBarcodeRef.value.reset(); + return; + } + await axios.patch(`ItemBarcodes`, params); + notify(t('globals.dataSaved'), 'positive'); +}; @@ -39,6 +60,7 @@ const focusLastInput = () => { ref="itemBarcodeRef" url="ItemBarcodes" auto-load + :save-fn="submit" > @@ -54,7 +76,7 @@ const focusLastInput = () => { focusable-input /> diff --git a/src/pages/Item/Card/ItemBasicData.vue b/src/pages/Item/Card/ItemBasicData.vue index 1b0342668..a1788617f 100644 --- a/src/pages/Item/Card/ItemBasicData.vue +++ b/src/pages/Item/Card/ItemBasicData.vue @@ -70,6 +70,7 @@ const onIntrastatCreated = (response, formData) => { option-label="name" hide-selected map-options + required > diff --git a/src/pages/Item/Card/ItemBotanical.vue b/src/pages/Item/Card/ItemBotanical.vue index c4b561772..57774f75e 100644 --- a/src/pages/Item/Card/ItemBotanical.vue +++ b/src/pages/Item/Card/ItemBotanical.vue @@ -1,5 +1,5 @@ - @@ -192,27 +215,45 @@ onUnmounted(() => (stateStore.rightDrawer = false)); dense v-model="from" class="q-mr-lg" + data-cy="from" + /> + + - - - + + + + + + @@ -234,8 +275,8 @@ onUnmounted(() => (stateStore.rightDrawer = false)); - {{ value }} + + {{ value }} {{ t('lastEntries.grouping') }}/{{ t('lastEntries.packing') }} (stateStore.rightDrawer = false)); - + {{ toCurrency(row.cost, 'EUR', 3) }} @@ -272,10 +313,25 @@ onUnmounted(() => (stateStore.rightDrawer = false)); + + + + + {{ row.supplier }} + + + - + + es: + Hide inventory supplier: Ocultar proveedor inventario + diff --git a/src/pages/Item/Card/ItemShelving.vue b/src/pages/Item/Card/ItemShelving.vue index 27e265e6b..7ad60c9e0 100644 --- a/src/pages/Item/Card/ItemShelving.vue +++ b/src/pages/Item/Card/ItemShelving.vue @@ -1,19 +1,15 @@ @@ -203,7 +152,7 @@ onMounted(async () => { { - - - - - - - - - - - - {{ row.longName }} + + + {{ row.longName }} - + - + diff --git a/src/pages/Item/Card/ItemSummary.vue b/src/pages/Item/Card/ItemSummary.vue index db90ba06f..e1b97d7c9 100644 --- a/src/pages/Item/Card/ItemSummary.vue +++ b/src/pages/Item/Card/ItemSummary.vue @@ -46,7 +46,7 @@ const getUrl = (id, param) => `#/Item/${id}/${param}`; `#/Item/${id}/${param}`; { itemTagsRef.value.formData[itemTagsRef.value.formData.length - 1].priority = getHighestPriority(rows); }; + +const submitTags = async (data) => { + itemTagsRef.value.onSubmit(data); +}; (tagOptions = data)" auto-load /> @@ -76,7 +80,6 @@ const insertTag = (rows) => { data-key="ItemTags" model="ItemTags" url="ItemTags" - update-url="Tags/onSubmit" :data-required="{ $index: undefined, itemFk: route.params.id, @@ -137,6 +140,7 @@ const insertTag = (rows) => { :required="false" :rules="validate('itemTag.tagFk')" :use-like="false" + sort-by="value" /> { v-model="row.priority" :required="true" :rules="validate('itemTag.priority')" + @keyup.enter.stop="submitTags(row)" /> { + + +es: + Tags can not be repeated: Las etiquetas no pueden repetirse + diff --git a/src/pages/Item/Card/ItemTax.vue b/src/pages/Item/Card/ItemTax.vue index 84b5f63f4..8060481f0 100644 --- a/src/pages/Item/Card/ItemTax.vue +++ b/src/pages/Item/Card/ItemTax.vue @@ -28,7 +28,7 @@ const taxesFilter = { ], }; -const ItemTaxRef = ref(null); +const ItemTaxRef = ref(); const taxesOptions = ref([]); const submitTaxes = async (data) => { @@ -36,7 +36,10 @@ const submitTaxes = async (data) => { id: tax.id, taxClassFk: tax.taxClassFk, })); - + if (payload.some((item) => item.taxClassFk === null)) { + notify(t('Tax class cannot be blank'), 'negative'); + return; + } await axios.post(`Items/updateTaxes`, payload); notify(t('globals.dataSaved'), 'positive'); }; diff --git a/src/pages/Item/ItemFixedPrice.vue b/src/pages/Item/ItemFixedPrice.vue index 8bf5d33bd..5ebf8a477 100644 --- a/src/pages/Item/ItemFixedPrice.vue +++ b/src/pages/Item/ItemFixedPrice.vue @@ -1,5 +1,5 @@ + + + + + { :columns="columns" :user-params="userParams" :is-editable="true" + :right-search="false" auto-load :disable-option="{ card: true }" chip-locale="item.params" @@ -297,39 +291,40 @@ onMounted(async () => { handleScopeDays(evt.target.value)" @remove="handleScopeDays()" class="q-px-xs q-pr-lg" filled dense + lazy-rules + is-outlined /> + - - - - {{ row.response }} - - - - - {{ t('Discard') }} - - - + + + {{ row.response }} + + + + + {{ t('Discard') }} + + diff --git a/src/pages/Item/ItemRequestFilter.vue b/src/pages/Item/ItemRequestFilter.vue index 64bc0e575..4e8ae0d42 100644 --- a/src/pages/Item/ItemRequestFilter.vue +++ b/src/pages/Item/ItemRequestFilter.vue @@ -1,12 +1,13 @@ @@ -79,8 +80,7 @@ const decrement = (paramsObj, key) => { {{ t(`params.${tag.label}`) }}: - {{ formatFn(tag.value) }} - {{ t(`${tag.value}`) }} + {{ formatFn(tag.value) }} @@ -145,109 +145,16 @@ const decrement = (paramsObj, key) => { - - - - - {{ scope.opt?.name }} - {{ scope.opt?.nickname }}, - {{ scope.opt?.code }} - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - {{ t('dateFiltersTooltip') }} - - - - - - @@ -267,6 +174,16 @@ const decrement = (paramsObj, key) => { /> + + + + + diff --git a/src/pages/Item/ItemType/Card/ItemTypeSummary.vue b/src/pages/Item/ItemType/Card/ItemTypeSummary.vue index c51d59e13..9ba774ca4 100644 --- a/src/pages/Item/ItemType/Card/ItemTypeSummary.vue +++ b/src/pages/Item/ItemType/Card/ItemTypeSummary.vue @@ -78,29 +78,32 @@ async function setItemTypeData(data) { {{ t('globals.summary.basicData') }} - - - - + + + + {{ itemType.worker?.firstName }} - + - - + + diff --git a/src/pages/Item/ItemType/ItemTypeSearchbar.vue b/src/pages/Item/ItemType/ItemTypeSearchbar.vue index 87903a517..749033d43 100644 --- a/src/pages/Item/ItemType/ItemTypeSearchbar.vue +++ b/src/pages/Item/ItemType/ItemTypeSearchbar.vue @@ -10,7 +10,6 @@ const { t } = useI18n(); url="ItemTypes" :label="t('Search item type')" :info="t('Search itemType by id, name or code')" - search-url="table" /> diff --git a/src/pages/Item/ItemType/locale/en.yml b/src/pages/Item/ItemType/locale/en.yml index 575d5e402..99c6791f2 100644 --- a/src/pages/Item/ItemType/locale/en.yml +++ b/src/pages/Item/ItemType/locale/en.yml @@ -1,16 +1,17 @@ -shared: - code: Code - name: Name - worker: Worker - category: Category - temperature: Temperature - life: Life - itemPackingType: Item packing type - maxRefs: Maximum references - fragile: Fragile -summary: - id: id - life: Life - promo: Promo - itemPackingType: Item packing type - isUnconventionalSize: Is unconventional size +itemType: + shared: + code: Code + name: Name + worker: Worker + category: Category + temperature: Temperature + life: Life + itemPackingType: Item packing type + maxRefs: Maximum references + fragile: Fragile + summary: + id: id + life: Life + promo: Promo + itemPackingType: Item packing type + isUnconventionalSize: Is unconventional size diff --git a/src/pages/Item/ItemType/locale/es.yml b/src/pages/Item/ItemType/locale/es.yml index 93f8b0d0e..c91fb4058 100644 --- a/src/pages/Item/ItemType/locale/es.yml +++ b/src/pages/Item/ItemType/locale/es.yml @@ -1,16 +1,17 @@ -shared: - code: Código - name: Nombre - worker: Trabajador - category: Reino - temperature: Temperatura - life: Vida - itemPackingType: Tipo de embalaje - maxRefs: Referencias máximas - fragile: Frágil -summary: - id: id - life: Vida - promo: Promoción - itemPackingType: Tipo de embalaje - isUnconventionalSize: Es de tamaño poco convencional +itemType: + shared: + code: Código + name: Nombre + worker: Trabajador + category: Reino + temperature: Temperatura + life: Vida + itemPackingType: Tipo de embalaje + maxRefs: Referencias máximas + fragile: Frágil + summary: + id: id + life: Vida + promo: Promoción + itemPackingType: Tipo de embalaje + isUnconventionalSize: Es de tamaño poco convencional diff --git a/src/pages/Item/ItemTypeList.vue b/src/pages/Item/ItemTypeList.vue index 149de482d..4cea931e2 100644 --- a/src/pages/Item/ItemTypeList.vue +++ b/src/pages/Item/ItemTypeList.vue @@ -6,6 +6,7 @@ import VnTable from 'components/VnTable/VnTable.vue'; import FetchData from 'components/FetchData.vue'; import RightMenu from 'src/components/common/RightMenu.vue'; import ItemTypeFilter from './ItemType/ItemTypeFilter.vue'; +import WorkerDescriptorProxy from '../Worker/Card/WorkerDescriptorProxy.vue'; const { t } = useI18n(); const tableRef = ref(); @@ -31,13 +32,14 @@ const columns = computed(() => [ { align: 'left', name: 'name', - label: t('name'), + label: t('globals.name'), cardVisible: true, create: true, }, { align: 'left', label: t('worker'), + name: 'workerFk', create: true, component: 'select', attrs: { @@ -45,20 +47,20 @@ const columns = computed(() => [ optionLabel: 'nickname', optionValue: 'id', }, + format: (row) => row.worker?.user?.name, cardVisible: true, - visible: true, - columnField: { - component: 'userLink', - attrs: ({ row }) => { - return { - workerId: row?.worker?.id, - name: row.worker?.user?.name, - defaultName: true, - }; - }, - }, + columnField: { component: null }, columnFilter: { - name: 'workerFk', + attrs: { + url: 'Workers/activeWithInheritedRole', + fields: ['id', 'name'], + where: { role: 'buyer' }, + optionFilter: 'firstName', + optionLabel: 'name', + optionValue: 'id', + useLike: false, + }, + inWhere: true, }, }, { @@ -135,24 +137,27 @@ const columns = computed(() => [ :columns="columns" auto-load :right-search="false" - :is-editable="false" - :use-model="true" redirect="item/item-type" - /> + > + + + {{ row.worker?.user?.name }} + + + + es: id: Id code: Código - name: Nombre worker: Trabajador ItemCategory: Reino Temperature: Temperatura Create ItemTypes: Crear familia en: code: Code - name: Name worker: Worker ItemCategory: ItemCategory Temperature: Temperature diff --git a/src/pages/Item/locale/en.yml b/src/pages/Item/locale/en.yml index 78a1c3ff0..bd91ef745 100644 --- a/src/pages/Item/locale/en.yml +++ b/src/pages/Item/locale/en.yml @@ -66,6 +66,7 @@ lastEntries: package: Package freight: Freight comission: Comission + printedStickers: Pri. itemTags: removeTag: Remove tag addTag: Add tag @@ -95,6 +96,15 @@ item: mine: For me state: State myTeam: My team + shipped: Shipped + description: Description + quantity: Quantity + price: Price + item: Item + achieved: Achieved + concept: Concept + denyOptions: Deny + scopeDays: Scope days searchbar: label: Search item descriptor: @@ -112,7 +122,7 @@ item: title: All its properties will be copied subTitle: Do you want to clone this item? list: - id: Identifier + id: Id grouping: Grouping packing: Packing description: Description @@ -122,8 +132,9 @@ item: intrastat: Intrastat isActive: Active size: Size - origin: Origin + origin: Orig. userName: Buyer + weight: Weight weightByPiece: Weight/Piece stemMultiplier: Multiplier producer: Producer diff --git a/src/pages/Item/locale/es.yml b/src/pages/Item/locale/es.yml index 5498f4458..b821d276a 100644 --- a/src/pages/Item/locale/es.yml +++ b/src/pages/Item/locale/es.yml @@ -56,7 +56,7 @@ lastEntries: landed: F. Entrega entry: Entrada pvp: PVP - label: Etiquetas + label: Eti. grouping: Grouping quantity: Cantidad cost: Coste @@ -66,6 +66,7 @@ lastEntries: package: Embalaje freight: Porte comission: Comisión + printedStickers: Imp. itemTags: removeTag: Quitar etiqueta addTag: Añadir etiqueta @@ -97,6 +98,15 @@ item: mine: Para mi state: Estado myTeam: Mi equipo + shipped: Enviado + description: Descripción + quantity: Cantidad + price: Precio + item: Artículo + achieved: Conseguido + concept: Concepto + denyOptions: Denegado + scopeDays: Días en adelante searchbar: label: Buscar artículo descriptor: @@ -114,7 +124,7 @@ item: title: Todas sus propiedades serán copiadas subTitle: ¿Desea clonar este artículo? list: - id: Identificador + id: Id grouping: Grouping packing: Packing description: Descripción @@ -124,7 +134,8 @@ item: intrastat: Intrastat isActive: Activo size: Medida - origin: Origen + origin: Orig. + weight: Peso weightByPiece: Peso (gramos)/tallo userName: Comprador stemMultiplier: Multiplicador diff --git a/src/pages/Monitor/Ticket/MonitorTicketFilter.vue b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue index 3247da014..82578a61f 100644 --- a/src/pages/Monitor/Ticket/MonitorTicketFilter.vue +++ b/src/pages/Monitor/Ticket/MonitorTicketFilter.vue @@ -9,6 +9,7 @@ import VnInput from 'src/components/common/VnInput.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import FetchData from 'src/components/FetchData.vue'; import { dateRange } from 'src/filters'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; defineProps({ dataKey: { type: String, required: true } }); const { t, te } = useI18n(); @@ -59,7 +60,11 @@ const getLocale = (label) => { @@ -108,33 +113,16 @@ const getLocale = (label) => { - - - - - {{ opt.name }} - - {{ `${opt.nickname}, ${opt.code}` }} - - - - - + @@ -146,6 +134,7 @@ const getLocale = (label) => { /> + { /> + + + + + { /> + + + + + + + + + + -import { ref, computed } from 'vue'; +import { ref, computed, onMounted } from 'vue'; import { useI18n } from 'vue-i18n'; import FetchData from 'components/FetchData.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; @@ -10,20 +10,25 @@ import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue'; import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue'; import VnTable from 'components/VnTable/VnTable.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; -import { toDateFormat } from 'src/filters/date.js'; import { toCurrency, dateRange, dashIfEmpty } from 'src/filters'; import RightMenu from 'src/components/common/RightMenu.vue'; import MonitorTicketSearchbar from './MonitorTicketSearchbar.vue'; import MonitorTicketFilter from './MonitorTicketFilter.vue'; import TicketProblems from 'src/components/TicketProblems.vue'; +import VnDateBadge from 'src/components/common/VnDateBadge.vue'; +import { useStateStore } from 'src/stores/useStateStore'; -const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; // 2min in ms +const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; const { t } = useI18n(); const autoRefresh = ref(false); const tableRef = ref(null); const provinceOpts = ref([]); const stateOpts = ref([]); const zoneOpts = ref([]); +const DepartmentOpts = ref([]); +const PayMethodOpts = ref([]); +const ItemPackingTypeOpts = ref([]); +const stateStore = useStateStore(); const { viewSummary } = useSummaryDialog(); const [from, to] = dateRange(Date.vnNew()); @@ -34,6 +39,11 @@ const stateColors = { alert: 'negative', }; +onMounted(() => { + stateStore.leftDrawer = false; + stateStore.rightDrawer = false; +}); + function exprBuilder(param, value) { switch (param) { case 'stateFk': @@ -51,6 +61,8 @@ function exprBuilder(param, value) { case 'nickname': return { [`t.nickname`]: { like: `%${value}%` } }; case 'zoneFk': + case 'department': + return { 'd.name': value }; case 'totalWithVat': return { [`t.${param}`]: value }; } @@ -137,6 +149,7 @@ const columns = computed(() => [ align: 'left', format: (row) => row.practicalHour, columnFilter: false, + dense: true, }, { label: t('salesTicketsTable.preparation'), @@ -190,6 +203,7 @@ const columns = computed(() => [ 'false-value': 0, 'true-value': 1, }, + component: false, }, { label: t('salesTicketsTable.zone'), @@ -206,6 +220,21 @@ const columns = computed(() => [ }, }, }, + { + label: t('salesTicketsTable.payMethod'), + name: 'payMethod', + align: 'left', + columnFilter: { + component: 'select', + url: 'PayMethods', + attrs: { + options: PayMethodOpts.value, + optionValue: 'id', + optionLabel: 'name', + dense: true, + }, + }, + }, { label: t('salesTicketsTable.total'), name: 'totalWithVat', @@ -219,6 +248,33 @@ const columns = computed(() => [ }, }, }, + { + label: t('salesTicketsTable.department'), + name: 'department', + align: 'left', + columnFilter: { + component: 'select', + attrs: { + options: DepartmentOpts.value, + dense: true, + }, + }, + }, + { + label: t('salesTicketsTable.packing'), + name: 'packing', + align: 'left', + columnFilter: { + component: 'select', + url: 'ItemPackingTypes', + attrs: { + options: ItemPackingTypeOpts.value, + 'option-value': 'code', + 'option-label': 'code', + dense: true, + }, + }, + }, { align: 'right', name: 'tableActions', @@ -250,19 +306,6 @@ const columns = computed(() => [ }, ]); -const getBadgeAttrs = (date) => { - let today = Date.vnNew(); - today.setHours(0, 0, 0, 0); - let timeTicket = new Date(date); - timeTicket.setHours(0, 0, 0, 0); - - let timeDiff = today - timeTicket; - - if (timeDiff == 0) return { color: 'warning', 'text-color': 'black' }; - if (timeDiff < 0) return { color: 'success', 'text-color': 'black' }; - return { color: 'transparent', 'text-color': 'white' }; -}; - let refreshTimer = null; const autoRefreshHandler = (value) => { @@ -279,14 +322,6 @@ const totalPriceColor = (ticket) => { if (total > 0 && total < 50) return 'warning'; }; -const formatShippedDate = (date) => { - if (!date) return '-'; - const dateSplit = date.split('T'); - const [year, month, day] = dateSplit[0].split('-'); - const newDate = new Date(year, month - 1, day); - return toDateFormat(newDate); -}; - const openTab = (id) => window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer'); @@ -318,6 +353,33 @@ const openTab = (id) => auto-load @on-fetch="(data) => (zoneOpts = data)" /> + (ItemPackingTypeOpts = data)" + /> + (DepartmentOpts = data)" + /> + (PayMethodOpts = data)" + /> @@ -382,13 +444,7 @@ const openTab = (id) => - - {{ formatShippedDate(row.shippedDate) }} - + diff --git a/src/pages/Monitor/locale/en.yml b/src/pages/Monitor/locale/en.yml index ff4031654..e61a24979 100644 --- a/src/pages/Monitor/locale/en.yml +++ b/src/pages/Monitor/locale/en.yml @@ -26,8 +26,8 @@ salesTicketsTable: componentLack: Component lack tooLittle: Ticket too little identifier: Identifier - theoretical: Theoretical - practical: Practical + theoretical: H.Theor + practical: H.Prac province: Province state: State isFragile: Is fragile @@ -35,7 +35,10 @@ salesTicketsTable: goToLines: Go to lines preview: Preview total: Total - preparation: Preparation + preparation: H.Prep + payMethod: Pay method + department: Department + packing: ITP searchBar: label: Search tickets info: Search tickets by id or alias diff --git a/src/pages/Monitor/locale/es.yml b/src/pages/Monitor/locale/es.yml index a2ed3bb1a..30afb1904 100644 --- a/src/pages/Monitor/locale/es.yml +++ b/src/pages/Monitor/locale/es.yml @@ -26,8 +26,8 @@ salesTicketsTable: componentLack: Faltan componentes tooLittle: Ticket demasiado pequeño identifier: Identificador - theoretical: Teórica - practical: Práctica + theoretical: H.Teór + practical: H.Prác province: Provincia state: Estado isFragile: Es frágil @@ -35,7 +35,10 @@ salesTicketsTable: goToLines: Ir a líneas preview: Vista previa total: Total - preparation: Preparación + preparation: H.Prep + payMethod: Método de pago + department: Departamento + packing: ITP searchBar: label: Buscar tickets info: Buscar tickets por identificador o alias diff --git a/src/pages/Order/Card/CatalogFilterValueDialog.vue b/src/pages/Order/Card/CatalogFilterValueDialog.vue index 53bb87f8d..b91e7d229 100644 --- a/src/pages/Order/Card/CatalogFilterValueDialog.vue +++ b/src/pages/Order/Card/CatalogFilterValueDialog.vue @@ -49,7 +49,7 @@ const getSelectedTagValues = async (tag) => { - + { :emit-value="false" use-input @update:model-value="getSelectedTagValues" + data-cy="catalogFilterValueDialogTagSelect" /> { :disable="!value" is-outlined class="col" + data-cy="catalogFilterValueDialogValueInput" /> import { useStateStore } from 'stores/useStateStore'; import { useRoute, useRouter } from 'vue-router'; -import { onMounted, onUnmounted, ref, computed, watch } from 'vue'; +import { onMounted, ref, computed, watch } from 'vue'; import axios from 'axios'; import { useI18n } from 'vue-i18n'; import VnPaginate from 'src/components/ui/VnPaginate.vue'; @@ -18,6 +18,7 @@ const dataKey = 'OrderCatalogList'; const arrayData = useArrayData(dataKey); const store = arrayData.store; const tags = ref([]); +const itemRefs = ref({}); let catalogParams = { orderFk: route.params.id, @@ -29,8 +30,6 @@ onMounted(() => { checkOrderConfirmation(); }); -onUnmounted(() => (stateStore.rightDrawer = false)); - async function checkOrderConfirmation() { const response = await axios.get(`Orders/${route.params.id}`); if (response.data.isConfirmed === 1) { @@ -88,17 +87,15 @@ watch( :label="t('Search items')" :info="t('You can search items by name or id')" /> - - - - - - + + + + diff --git a/src/pages/Order/Card/OrderCatalogFilter.vue b/src/pages/Order/Card/OrderCatalogFilter.vue index 6202a6f90..39627595d 100644 --- a/src/pages/Order/Card/OrderCatalogFilter.vue +++ b/src/pages/Order/Card/OrderCatalogFilter.vue @@ -65,7 +65,6 @@ const selectCategory = async (params, category, search) => { params.typeFk = null; params.categoryFk = category.id; await loadTypes(category?.id); - await search(); }; const loadTypes = async (id) => { @@ -178,6 +177,7 @@ function addOrder(value, field, params) { ? resetCategory(params, searchFn) : removeTagGroupParam(params, searchFn, valIndex) " + data-cy="catalogFilterCustomTag" > {{ @@ -211,6 +211,7 @@ function addOrder(value, field, params) { :name="category.icon" class="category-icon" @click="selectCategory(params, category, searchFn)" + data-cy="catalogFilterCategory" > {{ t(category.name) }} @@ -234,6 +235,7 @@ function addOrder(value, field, params) { sort-by="name ASC" :disable="!params.categoryFk" @update:model-value="searchFn()" + data-cy="catalogFilterType" > @@ -285,6 +287,7 @@ function addOrder(value, field, params) { :is-clearable="false" v-model="searchByTag" @keyup.enter="(val) => onSearchByTag(val, params)" + data-cy="catalogFilterValueInput" > @@ -297,6 +300,7 @@ function addOrder(value, field, params) { color="primary" size="md" dense + data-cy="catalogFilterValueDialogBtn" /> -import toCurrency from '../../../filters/toCurrency'; -import { ref } from 'vue'; +import toCurrency from 'src/filters/toCurrency'; +import { computed, inject, ref } from 'vue'; import { useI18n } from 'vue-i18n'; import axios from 'axios'; import { useRoute } from 'vue-router'; import useNotify from 'composables/useNotify'; -import { useArrayData } from 'composables/useArrayData'; +import VnInputNumber from 'src/components/common/VnInputNumber.vue'; +import { useState } from 'src/composables/useState'; const { t } = useI18n(); const { notify } = useNotify(); const emit = defineEmits(['added']); const route = useRoute(); const props = defineProps({ - prices: { + item: { type: Array, required: true, }, }); +const state = useState(); -const fields = ref((props.prices || []).map((item) => ({ ...item, quantity: 0 }))); -const descriptorData = useArrayData('orderData'); +const orderData = computed(() => state.get('orderData')); + +const prices = ref((props.item.prices || []).map((item) => ({ ...item, quantity: 0 }))); const isLoading = ref(false); + +const totalQuantity = (items) => + items.reduce((acc, item) => { + return acc + item.quantity; + }, 0); const addToOrder = async () => { if (isLoading.value) return; isLoading.value = true; - const items = (fields.value || []).filter((item) => Number(item.quantity) > 0); + const items = (prices.value || []).filter((item) => Number(item.quantity) > 0); await axios.post('/OrderRows/addToOrder', { items, orderFk: Number(route.params.id), }); + + const { data: orderTotal } = await axios.get( + `Orders/${Number(route.params.id)}/getTotal` + ); + + state.set('orderTotal', orderTotal); + const rows = orderData.value.rows.push(...items) || []; + state.set('orderData', { + ...orderData.value, + rows, + }); notify(t('globals.dataSaved'), 'positive'); - emit('added'); - descriptorData.fetch({}); + emit('added', -totalQuantity(items)); isLoading.value = false; }; const canAddToOrder = () => { - return (fields.value || []).some((item) => Number(item.quantity) > 0); + let canAddToOrder = (prices.value || []).some((price) => Number(price.quantity) > 0); + if (canAddToOrder) { + const excedQuantity = prices.value.reduce( + (acc, { quantity }) => acc + quantity, + 0 + ); + if (excedQuantity > props.item.available) { + canAddToOrder = false; + } + } + return canAddToOrder; }; @@ -44,30 +72,33 @@ const canAddToOrder = () => { - + - {{ item.warehouse }} + {{ price.warehouse }} { - item.quantity += item.grouping; + price.quantity -= price.grouping; + } + " + @click.exact=" + () => { + price.quantity += price.grouping; } " > - {{ item.grouping }} + {{ price.grouping }} - x {{ toCurrency(item.price) }} + x {{ toCurrency(price.price) }} - diff --git a/src/pages/Order/Card/OrderCreateDialog.vue b/src/pages/Order/Card/OrderCreateDialog.vue index c78b04d7f..9a53b9e30 100644 --- a/src/pages/Order/Card/OrderCreateDialog.vue +++ b/src/pages/Order/Card/OrderCreateDialog.vue @@ -1,9 +1,8 @@ @@ -90,10 +64,9 @@ onMounted(async () => { option-value="id" option-label="name" :filter="{ - fields: ['id', 'name', 'defaultAddressFk'], + fields: ['id', 'name'], }" hide-selected - @update:model-value="onClientChange" > @@ -110,7 +83,7 @@ onMounted(async () => { :label="t('order.form.addressFk')" v-model="data.addressId" url="addresses" - :fields="['id', 'nickname', 'defaultAddressFk', 'street', 'city']" + :fields="['id', 'nickname', 'street', 'city']" sort-by="id" option-value="id" option-label="street" diff --git a/src/pages/Order/Card/OrderDescriptor.vue b/src/pages/Order/Card/OrderDescriptor.vue index 138fcc40f..e0c613aed 100644 --- a/src/pages/Order/Card/OrderDescriptor.vue +++ b/src/pages/Order/Card/OrderDescriptor.vue @@ -63,21 +63,26 @@ const setData = (entity) => { if (!entity) return; getTotalRef.value && getTotalRef.value.fetch(); data.value = useCardDescription(entity?.client?.name, entity?.id); - state.set('orderData', entity); + state.set('orderTotal', total); }; const getConfirmationValue = (isConfirmed) => { return t(isConfirmed ? 'globals.confirmed' : 'order.summary.notConfirmed'); }; -const total = ref(null); +const orderTotal = computed(() => state.get('orderTotal') ?? 0); +const total = ref(0); (total = response)" + @on-fetch=" + (response) => { + total = response; + } + " /> - + diff --git a/src/pages/Order/Card/OrderFilter.vue b/src/pages/Order/Card/OrderFilter.vue index 917369919..dc86600ac 100644 --- a/src/pages/Order/Card/OrderFilter.vue +++ b/src/pages/Order/Card/OrderFilter.vue @@ -6,6 +6,7 @@ import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue'; import VnSelect from 'components/common/VnSelect.vue'; import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'components/common/VnInput.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const props = defineProps({ @@ -61,28 +62,16 @@ const sourceList = ref([]); outlined rounded /> - - - - - {{ opt.name }} - - {{ opt.nickname }},{{ opt.code }} - - - - - + /> (orderSummary.vat = data)" auto-load /> - + - + import axios from 'axios'; import { useI18n } from 'vue-i18n'; -import { computed, onMounted, ref } from 'vue'; +import { computed, ref, onMounted } from 'vue'; import { dashIfEmpty, toCurrency, toDate } from 'src/filters'; import OrderSummary from 'pages/Order/Card/OrderSummary.vue'; import { useSummaryDialog } from 'src/composables/useSummaryDialog'; @@ -20,8 +20,8 @@ const { t } = useI18n(); const { viewSummary } = useSummaryDialog(); const tableRef = ref(); const agencyList = ref([]); -const addressesList = ref([]); const route = useRoute(); +const addressOptions = ref([]); const columns = computed(() => [ { align: 'left', @@ -147,11 +147,12 @@ onMounted(() => { const id = JSON.parse(clientId); fetchClientAddress(id.clientFk); }); + async function fetchClientAddress(id, formData = {}) { - const { data } = await axios.get(`Clients/${id}`, { - params: { filter: { include: { relation: 'addresses' } } }, - }); - addressesList.value = data.addresses; + const { data } = await axios.get( + `Clients/${id}/addresses?filter[order]=isActive DESC` + ); + addressOptions.value = data; formData.addressId = data.defaultAddressFk; fetchAgencies(formData); } @@ -191,7 +192,7 @@ const getDateColor = (date) => { urlCreate: 'Orders/new', title: t('module.cerateOrder'), onDataSaved: (url) => { - tableRef.redirect(`${url}/catalog`); + tableRef.redirect(`${url}/catalog`); }, formInitialData: { active: true, @@ -246,7 +247,7 @@ const getDateColor = (date) => { { return { 'a.supplierName': value }; case 'routeFk': return { 'a.routeFk': value }; - case 'created': + case 'dated': case 'agencyFk': case 'packages': case 'm3': @@ -145,7 +145,7 @@ const exprBuilder = (param, value) => { diff --git a/src/pages/Route/Card/RouteDescriptor.vue b/src/pages/Route/Card/RouteDescriptor.vue index cbabaf648..fa621843e 100644 --- a/src/pages/Route/Card/RouteDescriptor.vue +++ b/src/pages/Route/Card/RouteDescriptor.vue @@ -28,7 +28,7 @@ const filter = { 'id', 'workerFk', 'agencyModeFk', - 'created', + 'dated', 'm3', 'warehouseFk', 'description', @@ -78,7 +78,7 @@ const setData = (entity) => (data.value = useCardDescription(entity.code, entity @on-fetch="setData" > - + - - - - - {{ opt.name }} - - {{ opt.nickname }},{{ opt.code }} - - - - - + /> diff --git a/src/pages/Route/Card/RouteForm.vue b/src/pages/Route/Card/RouteForm.vue index 8c89718fa..9bf0a2f4e 100644 --- a/src/pages/Route/Card/RouteForm.vue +++ b/src/pages/Route/Card/RouteForm.vue @@ -11,6 +11,7 @@ import VnInputDate from 'components/common/VnInputDate.vue'; import VnInput from 'components/common/VnInput.vue'; import axios from 'axios'; import VnInputTime from 'components/common/VnInputTime.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const route = useRoute(); @@ -19,7 +20,7 @@ const shelvingId = ref(route.params?.id || null); const isNew = Boolean(!shelvingId.value); const defaultInitialData = { agencyModeFk: null, - created: null, + dated: null, description: '', vehicleFk: null, workerFk: null, @@ -32,7 +33,7 @@ const routeFilter = { 'id', 'workerFk', 'agencyModeFk', - 'created', + 'dated', 'm3', 'warehouseFk', 'description', @@ -94,26 +95,7 @@ const onSave = (data, response) => { > - - - - - {{ opt.name }} - - {{ opt.nickname }}, {{ opt.code }} - - - - - + { option-label="name" :input-debounce="0" /> - + @@ -188,7 +170,7 @@ es: Hour finished: Hora fin Description: Descripción Is served: Se ha servido - Created: Creado + Dated: Fecha The km can not exceed: La distancia debe ser inferior a {maxDistance} en: The km can not exceed: Distance must be lesser than {maxDistance} diff --git a/src/pages/Route/Card/RouteSummary.vue b/src/pages/Route/Card/RouteSummary.vue index 3f9b1a2a9..a0b971195 100644 --- a/src/pages/Route/Card/RouteSummary.vue +++ b/src/pages/Route/Card/RouteSummary.vue @@ -139,7 +139,7 @@ const ticketColumns = ref([ [ }, { align: 'left', - name: 'created', + name: 'dated', label: t('Date'), columnFilter: false, - format: ({ created }) => toDate(created), + format: ({ dated }) => toDate(dated), }, { align: 'left', diff --git a/src/pages/Route/RouteExtendedList.vue b/src/pages/Route/RouteExtendedList.vue index dbf646935..221fc4754 100644 --- a/src/pages/Route/RouteExtendedList.vue +++ b/src/pages/Route/RouteExtendedList.vue @@ -111,7 +111,7 @@ const columns = computed(() => [ }, { align: 'left', - name: 'created', + name: 'dated', label: t('route.Date'), columnFilter: false, cardVisible: true, @@ -223,10 +223,10 @@ function navigate(id) { router.push({ path: `/route/${id}` }); } -const cloneRoutes = () => { +const cloneRoutes = async () => { if (!selectedRows.value.length || !startingDate.value) return; - axios.post('Routes/clone', { - created: startingDate.value, + await axios.post('Routes/clone', { + dated: startingDate.value, ids: selectedRows.value.map((row) => row?.id), }); startingDate.value = null; @@ -274,7 +274,6 @@ const openTicketsDialog = (id) => { {{ t('route.Select the starting date') }} - [ columnFilter: { inWhere: true, }, - format: ({ created }) => toDate(created), + format: ({ dated }) => toDate(dated), cardVisible: true, }, { diff --git a/src/pages/Route/RouteTickets.vue b/src/pages/Route/RouteTickets.vue index 3bdad4fec..56e3143b4 100644 --- a/src/pages/Route/RouteTickets.vue +++ b/src/pages/Route/RouteTickets.vue @@ -109,7 +109,7 @@ const ticketList = ref([]); const cloneRoutes = () => { axios.post('Routes/clone', { - created: startingDate.value, + dated: startingDate.value, ids: selectedRows.value.map((row) => row?.id), }); refreshKey.value++; diff --git a/src/pages/Route/locale/en.yml b/src/pages/Route/locale/en.yml index d113fda67..420d18dfe 100644 --- a/src/pages/Route/locale/en.yml +++ b/src/pages/Route/locale/en.yml @@ -5,7 +5,7 @@ route: Description: Description hourStarted: H.Start hourFinished: H.End - createRoute: Create route + dated: Dated From: From To: To Date: Date diff --git a/src/pages/Shelving/ShelvingList.vue b/src/pages/Shelving/ShelvingList.vue index d29f6ff15..a6ea50cf4 100644 --- a/src/pages/Shelving/ShelvingList.vue +++ b/src/pages/Shelving/ShelvingList.vue @@ -2,7 +2,7 @@ import VnPaginate from 'components/ui/VnPaginate.vue'; import { useStateStore } from 'stores/useStateStore'; import { useI18n } from 'vue-i18n'; -import { onMounted, onUnmounted } from 'vue'; +import { onMounted } from 'vue'; import CardList from 'components/ui/CardList.vue'; import VnLv from 'components/ui/VnLv.vue'; import { useRouter } from 'vue-router'; @@ -21,7 +21,6 @@ const filter = { }; onMounted(() => (stateStore.rightDrawer = true)); -onUnmounted(() => (stateStore.rightDrawer = false)); function navigate(id) { router.push({ path: `/shelving/${id}` }); diff --git a/src/pages/Supplier/Card/SupplierAccounts.vue b/src/pages/Supplier/Card/SupplierAccounts.vue index 428ab05c2..c2934830a 100644 --- a/src/pages/Supplier/Card/SupplierAccounts.vue +++ b/src/pages/Supplier/Card/SupplierAccounts.vue @@ -24,13 +24,14 @@ const supplier = ref(null); const supplierAccountRef = ref(null); const wireTransferFk = ref(null); const bankEntitiesOptions = ref([]); +const filteredBankEntitiesOptions = ref([]); const onBankEntityCreated = async (dataSaved, rowData) => { await bankEntitiesRef.value.fetch(); rowData.bankEntityFk = dataSaved.id; }; -const onChangesSaved = () => { +const onChangesSaved = async () => { if (supplier.value.payMethodFk !== wireTransferFk.value) quasar .dialog({ @@ -55,12 +56,35 @@ const setWireTransfer = async () => { await axios.patch(`Suppliers/${route.params.id}`, params); notify('globals.dataSaved', 'positive'); }; + +function findBankFk(value, row) { + row.bankEntityFk = null; + if (!value) return; + + const bankEntityFk = bankEntitiesOptions.value.find((b) => b.id == value.slice(4, 8)); + if (bankEntityFk) row.bankEntityFk = bankEntityFk.id; +} + +function bankEntityFilter(val, update) { + update(() => { + const needle = val.toLowerCase(); + filteredBankEntitiesOptions.value = bankEntitiesOptions.value.filter( + (bank) => + bank.bic.toLowerCase().startsWith(needle) || + bank.name.toLowerCase().includes(needle) + ); + }); +} (bankEntitiesOptions = data)" + @on-fetch=" + (data) => { + (bankEntitiesOptions = data), (filteredBankEntitiesOptions = data); + } + " auto-load /> { findBankFk(value, row)" :required="true" > @@ -109,7 +134,9 @@ const setWireTransfer = async () => { bankEntityFilter(val, update)" option-label="bic" option-value="id" hide-selected diff --git a/src/pages/Supplier/Card/SupplierBasicData.vue b/src/pages/Supplier/Card/SupplierBasicData.vue index 70f6432dd..842109656 100644 --- a/src/pages/Supplier/Card/SupplierBasicData.vue +++ b/src/pages/Supplier/Card/SupplierBasicData.vue @@ -5,6 +5,7 @@ import FormModel from 'components/FormModel.vue'; import VnRow from 'components/ui/VnRow.vue'; import VnInput from 'src/components/common/VnInput.vue'; import VnSelect from 'src/components/common/VnSelect.vue'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const route = useRoute(); const { t } = useI18n(); @@ -30,31 +31,11 @@ const companySizes = [ :rules="validate('supplier.nickname')" clearable /> - - - - {{ - t('Responsible for approving invoices') - }} - - - - - - {{ scope.opt?.name }} - - {{ scope.opt?.nickname }}, {{ scope.opt?.id }} - - - - - + /> es: - Responsible for approving invoices: Responsable de aprobar las facturas Small(1-5), Medium(6-50), Big(> 50): Pequeño(1-5), Mediano(6-50), Grande(> 50) diff --git a/src/pages/Supplier/Card/SupplierConsumption.vue b/src/pages/Supplier/Card/SupplierConsumption.vue index fe1cec260..8a7021fb3 100644 --- a/src/pages/Supplier/Card/SupplierConsumption.vue +++ b/src/pages/Supplier/Card/SupplierConsumption.vue @@ -8,7 +8,7 @@ import SendEmailDialog from 'components/common/SendEmailDialog.vue'; import SupplierConsumptionFilter from './SupplierConsumptionFilter.vue'; import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue'; -import { toDate } from 'src/filters'; +import { dateRange, toDate } from 'src/filters'; import { dashIfEmpty } from 'src/filters'; import { usePrintService } from 'composables/usePrintService'; import useNotify from 'src/composables/useNotify.js'; @@ -35,14 +35,17 @@ const store = arrayData.store; onUnmounted(() => state.unset('SupplierConsumption')); const dateRanges = computed(() => { - const { from, to } = arrayData.store?.userParams || {}; + let { from, to } = arrayData.store?.userParams || {}; return { from, to }; }); -const reportParams = computed(() => ({ - recipientId: Number(route.params.id), - ...dateRanges.value, -})); +const reportParams = computed(() => { + return { + recipientId: Number(route.params.id), + to: dateRange(dateRanges.value.to)[1], + from: dateRange(dateRanges.value.from)[1], + }; +}); async function getSupplierConsumptionData() { await arrayData.fetch({ append: false }); @@ -171,11 +174,9 @@ onMounted(async () => { - - - - - + + + - + {{ diff --git a/src/pages/Supplier/SupplierList.vue b/src/pages/Supplier/SupplierList.vue index 69f826201..a54012b34 100644 --- a/src/pages/Supplier/SupplierList.vue +++ b/src/pages/Supplier/SupplierList.vue @@ -124,8 +124,7 @@ const columns = computed(() => [ -en: - Search suppliers: Search suppliers -es: + es: Search suppliers: Buscar proveedores + Create Supplier: Crear proveedor diff --git a/src/pages/Supplier/SupplierListFilter.vue b/src/pages/Supplier/SupplierListFilter.vue index 7f838d3f5..b170a35cc 100644 --- a/src/pages/Supplier/SupplierListFilter.vue +++ b/src/pages/Supplier/SupplierListFilter.vue @@ -23,13 +23,13 @@ const countriesOptions = ref([]); (provincesOptions = data)" auto-load /> (countriesOptions = data)" auto-load /> diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue index ab96a6e75..c6a85c287 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicData.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicData.vue @@ -1,5 +1,5 @@ @@ -191,7 +173,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); horizontal > (stateStore.rightDrawer = false)); @@ -225,7 +207,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); :columns="columns" row-key="id" :pagination="{ rowsPerPage: 0 }" - class="full-width q-mt-md" + class="full-width" :no-data-label="t('globals.noResults')" flat > @@ -238,21 +220,27 @@ onUnmounted(() => (stateStore.rightDrawer = false)); - - + + {{ row.item.name }} - {{ row.item.subName }} - + - + - {{ row.item.subName }} + + + + + diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue index 0c53552fe..df84add93 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataForm.vue @@ -21,7 +21,6 @@ const formData = defineModel({ required: true, }); -const emit = defineEmits(['updateForm']); const { validate } = useValidator(); const { notify } = useNotify(); const router = useRouter(); @@ -33,18 +32,11 @@ const canEditZone = useAcl().hasAny([ const agencyFetchRef = ref(); const warehousesOptions = ref([]); const companiesOptions = ref([]); -const currenciesOptions = ref([]); const agenciesOptions = ref([]); const zonesOptions = ref([]); const addresses = ref([]); const zoneSelectRef = ref(); -watch( - () => formData.value, - (val) => emit('updateForm', val), - { deep: true } -); - onMounted(() => onFormModelInit()); const agencyByWarehouseFilter = computed(() => ({ diff --git a/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue b/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue index 851593bff..89249b899 100644 --- a/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue +++ b/src/pages/Ticket/Card/BasicData/TicketBasicDataView.vue @@ -155,18 +155,10 @@ onBeforeMount(async () => await getTicketData()); }" > - (formData = $event)" - v-model="formData" - /> + - (formData = $event)" - /> + diff --git a/src/pages/Ticket/Card/TicketBoxing.vue b/src/pages/Ticket/Card/TicketBoxing.vue index 1a7287396..bd8cad03f 100644 --- a/src/pages/Ticket/Card/TicketBoxing.vue +++ b/src/pages/Ticket/Card/TicketBoxing.vue @@ -1,6 +1,7 @@ - - - - - - - {{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ - time.max - }}) - - - - - - - - - - - - - - - - #{{ expedition.id }} - - - {{ t('globals.created') }} - - {{ - date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') - }} - - {{ t('globals.item') }} - {{ expedition.packagingItemFk }} - {{ t('ticket.boxing.worker') }} - {{ expedition.userName }} - - - - - + + + + + + {{ t('ticket.boxing.selectTime') }} ({{ time.min }}-{{ + time.max + }}) + + + + + + + + + + + + + + + + #{{ expedition.id }} + + + {{ t('globals.created') }} + + {{ date.formatDate(expedition.created, 'YYYY-MM-DD HH:mm:ss') }} + + {{ t('globals.item') }} + {{ expedition.packagingItemFk }} + {{ t('ticket.boxing.worker') }} + {{ expedition.userName }} + + + + diff --git a/src/pages/Ticket/Card/TicketComponents.vue b/src/pages/Ticket/Card/TicketComponents.vue index f45bde2d1..64815752a 100644 --- a/src/pages/Ticket/Card/TicketComponents.vue +++ b/src/pages/Ticket/Card/TicketComponents.vue @@ -1,5 +1,5 @@ @@ -180,7 +178,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); @on-fetch="(data) => (components = data)" auto-load /> - + @@ -266,7 +264,7 @@ onUnmounted(() => (stateStore.rightDrawer = false)); {{ toCurrency(theoricalCost, 'EUR', 2) }} - + (stateStore.rightDrawer = false)); {{ toCurrency(saleComponent.value * row.quantity, 'EUR', 3) }} - diff --git a/src/pages/Ticket/Card/TicketCreateTracking.vue b/src/pages/Ticket/Card/TicketCreateTracking.vue index 3ea762c6c..5c1e916f2 100644 --- a/src/pages/Ticket/Card/TicketCreateTracking.vue +++ b/src/pages/Ticket/Card/TicketCreateTracking.vue @@ -9,6 +9,7 @@ import VnSelect from 'src/components/common/VnSelect.vue'; import FetchData from 'components/FetchData.vue'; import { useState } from 'src/composables/useState'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const emit = defineEmits(['onRequestCreated']); @@ -46,29 +47,7 @@ const onStateFkChange = (formData) => (formData.userFk = user.value.id); option-label="name" option-value="id" /> - - - - - - {{ opt.name }} - - - {{ opt.nickname }}, {{ opt.code }} - - - - + diff --git a/src/pages/Ticket/Card/TicketDescriptorMenu.vue b/src/pages/Ticket/Card/TicketDescriptorMenu.vue index b8e64cb79..38e2af612 100644 --- a/src/pages/Ticket/Card/TicketDescriptorMenu.vue +++ b/src/pages/Ticket/Card/TicketDescriptorMenu.vue @@ -1,6 +1,6 @@ {{ t('Show Proforma') }} + + + + + {{ t('Restore ticket') }} + @@ -741,10 +805,13 @@ es: Ticket invoiced: Ticket facturado Set weight: Establecer peso Weight set: Peso establecido - This ticket may be invoiced, do you want to continue?: Es posible que se facture este ticket, desea continuar? invoiceIds: "Se han generado las facturas con los siguientes ids: {invoiceIds}" This ticket will be removed from current route! Continue anyway?: ¡Se eliminará el ticket de la ruta actual! ¿Continuar de todas formas? You are going to delete this ticket: Vas a eliminar este ticket as PDF signed: como PDF firmado Are you sure you want to replace this delivery note?: ¿Seguro que quieres reemplazar este albarán? + Restore ticket: Restaurar ticket + Are you sure you want to restore the ticket?: ¿Seguro que quieres restaurar el ticket? + You are going to restore this ticket: Vas a restaurar este ticket + Ticket restored: Ticket restaurado diff --git a/src/pages/Ticket/Card/TicketExpedition.vue b/src/pages/Ticket/Card/TicketExpedition.vue index b7f1f4dd0..7da049124 100644 --- a/src/pages/Ticket/Card/TicketExpedition.vue +++ b/src/pages/Ticket/Card/TicketExpedition.vue @@ -1,5 +1,5 @@ diff --git a/src/pages/Ticket/Card/TicketSale.vue b/src/pages/Ticket/Card/TicketSale.vue index deeaaa28d..703a00a0e 100644 --- a/src/pages/Ticket/Card/TicketSale.vue +++ b/src/pages/Ticket/Card/TicketSale.vue @@ -1,5 +1,5 @@ @@ -102,11 +100,9 @@ onUnmounted(() => (stateStore.rightDrawer = false)); @on-fetch="(data) => applyVolumes(data)" auto-load /> - (stateStore.rightDrawer = false)); {{ t('volume.volume') }}: {{ packingType.volume }} - - + [ @@ -441,6 +442,7 @@ watch( @@ -482,6 +485,14 @@ watch( + + {{ t('advanceTickets.clonedSales') }} + diff --git a/src/pages/Ticket/TicketAdvanceFilter.vue b/src/pages/Ticket/TicketAdvanceFilter.vue index a1d301f35..6d5c7726e 100644 --- a/src/pages/Ticket/TicketAdvanceFilter.vue +++ b/src/pages/Ticket/TicketAdvanceFilter.vue @@ -57,7 +57,6 @@ onMounted(async () => await getItemPackingTypes()); search-url="advanceTickets" :data-key="props.dataKey" :search-button="true" - :hidden-tags="['search']" :unremovable-params="['warehouseFk', 'dateFuture', 'dateToAdvance']" > @@ -169,6 +168,16 @@ onMounted(async () => await getItemPackingTypes()); + + + + + @@ -183,6 +192,7 @@ en: ipt: Destination IPT isFullMovable: 100% movable warehouseFk: Warehouse + onlyWithDestination: Only with destination es: Horizontal: Horizontal Vertical: Vertical @@ -194,4 +204,5 @@ es: ipt: IPT destino isFullMovable: 100% movible warehouseFk: Almacén + onlyWithDestination: Solo con destino diff --git a/src/pages/Ticket/TicketFilter.vue b/src/pages/Ticket/TicketFilter.vue index 6f1cac83b..bde27f30e 100644 --- a/src/pages/Ticket/TicketFilter.vue +++ b/src/pages/Ticket/TicketFilter.vue @@ -212,81 +212,78 @@ const getGroupedStates = (data) => { /> - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + @@ -340,7 +337,6 @@ es: With problems: Con problemas Invoiced: Facturado Routed: Enrutado - More options: Más opciones Province: Provincia Agency: Agencia Warehouse: Almacén diff --git a/src/pages/Ticket/TicketFutureFilter.vue b/src/pages/Ticket/TicketFutureFilter.vue index ffe967272..d28b0af71 100644 --- a/src/pages/Ticket/TicketFutureFilter.vue +++ b/src/pages/Ticket/TicketFutureFilter.vue @@ -59,7 +59,6 @@ onMounted(async () => { /> diff --git a/src/pages/Ticket/TicketList.vue b/src/pages/Ticket/TicketList.vue index 149a794a9..eb03a4927 100644 --- a/src/pages/Ticket/TicketList.vue +++ b/src/pages/Ticket/TicketList.vue @@ -1,6 +1,6 @@ diff --git a/src/pages/Ticket/locale/en.yml b/src/pages/Ticket/locale/en.yml index 56cc798ba..f11b32c3a 100644 --- a/src/pages/Ticket/locale/en.yml +++ b/src/pages/Ticket/locale/en.yml @@ -53,6 +53,7 @@ advanceTickets: errorsList: Errors list search: Search advance tickets searchInfo: Search advance tickets by ID or client ID + clonedSales: Has turn lines futureTickets: problems: Problems shipped: Date diff --git a/src/pages/Ticket/locale/es.yml b/src/pages/Ticket/locale/es.yml index bb068ac5b..945da8367 100644 --- a/src/pages/Ticket/locale/es.yml +++ b/src/pages/Ticket/locale/es.yml @@ -91,6 +91,7 @@ advanceTickets: errorsList: Lista de errores search: Buscar por tickets adelantados searchInfo: Buscar tickets adelantados por el identificador o el identificador del cliente + clonedSales: Tiene líneas de turno futureTickets: problems: Problemas shipped: Fecha diff --git a/src/pages/Travel/Card/TravelBasicData.vue b/src/pages/Travel/Card/TravelBasicData.vue index fd02b28cd..4b9aa28ed 100644 --- a/src/pages/Travel/Card/TravelBasicData.vue +++ b/src/pages/Travel/Card/TravelBasicData.vue @@ -104,7 +104,7 @@ const warehousesOptionsIn = ref([]); es: - raidDays: Si se marca "Redada", la fecha de entrega se moverá automáticamente los días indicados. + raidDays: El travel se desplaza automáticamente cada día para estar desde hoy al número de días indicado. Si se deja vacio no se moverá en: - raidDays: If "Raid" is checked, the landing date will automatically shift by the specified number of days. + raidDays: The travel adjusts itself daily to match the number of days set, starting from today. If left blank, it won’t move diff --git a/src/pages/Travel/Card/TravelCard.vue b/src/pages/Travel/Card/TravelCard.vue index 50cecef34..fd442d58b 100644 --- a/src/pages/Travel/Card/TravelCard.vue +++ b/src/pages/Travel/Card/TravelCard.vue @@ -7,13 +7,13 @@ import filter from './TravelFilter.js'; diff --git a/src/pages/Travel/Card/TravelSummary.vue b/src/pages/Travel/Card/TravelSummary.vue index be1a12406..cfc082ad9 100644 --- a/src/pages/Travel/Card/TravelSummary.vue +++ b/src/pages/Travel/Card/TravelSummary.vue @@ -8,7 +8,7 @@ import VnLv from 'src/components/ui/VnLv.vue'; import VnTitle from 'src/components/common/VnTitle.vue'; import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue'; import FetchData from 'src/components/FetchData.vue'; - +import VnRow from 'components/ui/VnRow.vue'; import { toDate, toCurrency } from 'src/filters'; import axios from 'axios'; @@ -256,16 +256,20 @@ const getLink = (param) => `#/travel/${entityId.value}/${param}`; :label="t('globals.warehouseOut')" :value="travel.warehouseOut?.name" /> - - + + + + + + @@ -320,7 +324,6 @@ const getLink = (param) => `#/travel/${entityId.value}/${param}`; [ align: 'left', showValue: false, sortable: true, + style: 'min-width: 170px;', }, { label: t('globals.packages'), @@ -237,7 +238,7 @@ const columns = computed(() => [ format: (value) => toDate(value), }, { - label: t('globals.wareHhuseIn'), + label: t('globals.warehouseIn'), field: 'warehouseInName', name: 'warehouseInName', align: 'left', @@ -506,7 +507,7 @@ const getColor = (percentage) => { :key="col.name" :props="props" @click="stopEventPropagation($event, col)" - auto-width + :style="col.style" > { }" > - {{ entry.id }} + {{ entry.id }} @@ -637,6 +638,18 @@ const getColor = (percentage) => { :deep(.q-table) { border-collapse: collapse; + + th { + padding: 0; + } + tbody tr td { + &:nth-child(1) { + max-width: 65px; + } + &:nth-child(4) { + padding: 0; + } + } } .q-td :deep(input) { @@ -684,7 +697,6 @@ const getColor = (percentage) => { width: max-content; } - en: searchExtraCommunity: Search for extra community shipping diff --git a/src/pages/Travel/TravelFilter.vue b/src/pages/Travel/TravelFilter.vue index 1ac83b9e0..287ac5ad2 100644 --- a/src/pages/Travel/TravelFilter.vue +++ b/src/pages/Travel/TravelFilter.vue @@ -77,7 +77,6 @@ defineExpose({ states }); :label="t('travel.shipped')" v-model="params.shipped" @update:model-value="searchFn()" - dense outlined rounded /> @@ -153,7 +152,7 @@ es: Id: Id ref: Referencia agency: Agencia - warehouseInFk: Alm.Salida + warehouseInFk: Alm.Entrada shipped: F.Envío shipmentHour: Hora de envío warehouseOut: Alm.Entrada diff --git a/src/pages/Travel/TravelList.vue b/src/pages/Travel/TravelList.vue index 852003a95..70e81aae2 100644 --- a/src/pages/Travel/TravelList.vue +++ b/src/pages/Travel/TravelList.vue @@ -54,7 +54,9 @@ const columns = computed(() => [ name: 'id', label: t('globals.id'), isId: true, - cardVisible: true, + chip: { + condition: () => true, + }, }, { align: 'left', @@ -64,7 +66,7 @@ const columns = computed(() => [ columnField: { component: null, }, - cardVisible: true, + isTitle: true, create: true, }, { @@ -103,14 +105,14 @@ const columns = computed(() => [ }, { align: 'left', - name: 'shipped', label: t('globals.shipped'), + name: 'shipped', + create: true, + cardVisible: true, component: 'date', columnField: { component: null, }, - cardVisible: true, - create: true, format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.shipped)), }, { @@ -201,13 +203,14 @@ const columns = computed(() => [ /> - + diff --git a/src/pages/Worker/Card/WorkerDescriptor.vue b/src/pages/Worker/Card/WorkerDescriptor.vue index 13f9e9795..f09aec816 100644 --- a/src/pages/Worker/Card/WorkerDescriptor.vue +++ b/src/pages/Worker/Card/WorkerDescriptor.vue @@ -10,6 +10,7 @@ import { useState } from 'src/composables/useState'; import axios from 'axios'; import VnImg from 'src/components/ui/VnImg.vue'; import EditPictureForm from 'components/EditPictureForm.vue'; +import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; const $props = defineProps({ id: { @@ -143,10 +144,14 @@ const handlePhotoUpdated = (evt = false) => { :value="entity.user?.emailUser?.email" copy /> - + + + + + + {{ t('globals.phone') }} @@ -206,6 +211,8 @@ const handlePhotoUpdated = (evt = false) => { es: + Go to client: Ir a cliente + Go to user: Ir al usuario Click to allow the user to be disabled: Marcar para deshabilitar Click to exclude the user from getting disabled: Marcar para no deshabilitar diff --git a/src/pages/Worker/Card/WorkerPda.vue b/src/pages/Worker/Card/WorkerPda.vue index 94f4e0d95..3ceee2493 100644 --- a/src/pages/Worker/Card/WorkerPda.vue +++ b/src/pages/Worker/Card/WorkerPda.vue @@ -133,6 +133,7 @@ function reloadData() { option-value="id" id="deviceProductionFk" hide-selected + data-cy="pda-dialog-select" > diff --git a/src/pages/Worker/Card/WorkerPit.vue b/src/pages/Worker/Card/WorkerPit.vue index c58196c7b..79cf1a04f 100644 --- a/src/pages/Worker/Card/WorkerPit.vue +++ b/src/pages/Worker/Card/WorkerPit.vue @@ -23,6 +23,7 @@ const insertTag = () => { workerPitCrudRef.value.insert(); }; const quasar = useQuasar(); + const deleteRelative = async (id) => { await new Promise((resolve) => { quasar @@ -62,7 +63,6 @@ const deleteRelative = async (id) => { > - { :label="t(`spousePension`)" /> - - + + { :filter="{ where: { workerFk: route.params.id }, }" - :data-required="{ workerFk: route.params.id }" + :data-required="{ + workerFk: route.params.id, + isJointCustody: false, + isDependend: false, + }" :has-sub-toolbar="false" > diff --git a/src/pages/Worker/Card/WorkerTimeControl.vue b/src/pages/Worker/Card/WorkerTimeControl.vue index 0bba2f891..491e5e180 100644 --- a/src/pages/Worker/Card/WorkerTimeControl.vue +++ b/src/pages/Worker/Card/WorkerTimeControl.vue @@ -103,9 +103,12 @@ const formattedWeekTotalHours = computed(() => const onInputChange = async (date) => { if (!date) return; - const { year, month, day } = date.scope.timestamp; + const { timestamp, outside } = date.scope; + const { year, month, day } = timestamp; const _date = new Date(year, month - 1, day); setDate(_date); + + if (outside) getMailStates(_date); }; const setDate = async (date) => { diff --git a/src/pages/Worker/WorkerCreate.vue b/src/pages/Worker/WorkerCreate.vue index 5676837dd..a4c6c2a06 100644 --- a/src/pages/Worker/WorkerCreate.vue +++ b/src/pages/Worker/WorkerCreate.vue @@ -14,6 +14,7 @@ import FormModel from 'components/FormModel.vue'; import CreateBankEntityForm from 'src/components/CreateBankEntityForm.vue'; import VnRadio from 'src/components/common/VnRadio.vue'; import { useState } from 'src/composables/useState'; +import VnSelectWorker from 'src/components/common/VnSelectWorker.vue'; const { t } = useI18n(); const user = useState().getUser(); @@ -149,27 +150,11 @@ async function autofillBic(worker) { hide-selected :rules="validate('Worker.company')" /> - - - - - {{ scope.opt.name }} - {{ scope.opt.nickname }}, - {{ scope.opt.code }} - - - - - + /> - - - - - {{ scope.opt.name }} - {{ scope.opt.nickname }}, - {{ scope.opt.code }} - - - - - + /> @@ -376,6 +361,7 @@ async function autofillBic(worker) { es: + Create worker: Crear trabajador Search worker: Buscar trabajador You can search by worker id or name: Puedes buscar por id o nombre del trabajador diff --git a/src/pages/Zone/Card/ZoneBasicData.vue b/src/pages/Zone/Card/ZoneBasicData.vue index 535f2393d..2d65476ce 100644 --- a/src/pages/Zone/Card/ZoneBasicData.vue +++ b/src/pages/Zone/Card/ZoneBasicData.vue @@ -32,7 +32,12 @@ const agencyOptions = ref([]); - + @@ -103,7 +108,20 @@ const agencyOptions = ref([]); clearable /> - + + + diff --git a/src/pages/Zone/Card/ZoneCreateWarehouse.vue b/src/pages/Zone/Card/ZoneCreateWarehouse.vue index a46ec2e6c..88f6a7701 100644 --- a/src/pages/Zone/Card/ZoneCreateWarehouse.vue +++ b/src/pages/Zone/Card/ZoneCreateWarehouse.vue @@ -30,17 +30,15 @@ const warehousesOptions = ref([]); > - - - + diff --git a/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue b/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue index 22d5bcd5e..3c45700cb 100644 --- a/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue +++ b/src/pages/Zone/Card/ZoneDescriptorMenuItems.vue @@ -16,24 +16,14 @@ const actions = { clone: async () => { const opts = { message: t('Zone cloned'), type: 'positive' }; - try { - const { data } = await axios.post(`Zones/${zoneId}/clone`, {}); - notify(opts); - push(`/zone/${data.id}/basic-data`); - } catch (e) { - opts.message = t('It was not able to clone the zone'); - opts.type = 'negative'; - } + const { data } = await axios.post(`Zones/${zoneId}/clone`, {}); + notify(opts); + push(`/zone/${data.id}/basic-data`); }, remove: async () => { - try { - await axios.post(`Zones/${zoneId}/deleteZone`); - - notify({ message: t('Zone deleted'), type: 'positive' }); - push({ name: 'ZoneList' }); - } catch (e) { - notify({ message: e.message, type: 'negative' }); - } + await axios.post(`Zones/${zoneId}/deleteZone`); + notify({ message: t('Zone deleted'), type: 'positive' }); + push({ name: 'ZoneList' }); }, }; function openConfirmDialog(callback) { diff --git a/src/pages/Zone/Card/ZoneEventExclusionForm.vue b/src/pages/Zone/Card/ZoneEventExclusionForm.vue index 4b54f6743..0882036c1 100644 --- a/src/pages/Zone/Card/ZoneEventExclusionForm.vue +++ b/src/pages/Zone/Card/ZoneEventExclusionForm.vue @@ -1,5 +1,5 @@ - - - - - + + + (stateStore.rightDrawer = false)); /> @@ -78,9 +74,12 @@ onUnmounted(() => (stateStore.rightDrawer = false)); props.formModeName, set: (value) => emit('update:formModeName', value), }); +const emit = defineEmits(['openZoneForm', 'update:formModeName']); +const { t } = useI18n(); +const route = useRoute(); +const weekdayStore = useWeekdayStore(); +const { openConfirmationModal } = useVnConfirm(); const params = computed(() => ({ zoneFk: route.params.id, @@ -94,7 +92,6 @@ const openInclusionForm = (event) => { date: event.dated, event, isNewMode: false, - eventType: 'event', }); }; diff --git a/src/pages/Zone/ZoneFilterPanel.vue b/src/pages/Zone/ZoneFilterPanel.vue index 55d21756d..3a35527ab 100644 --- a/src/pages/Zone/ZoneFilterPanel.vue +++ b/src/pages/Zone/ZoneFilterPanel.vue @@ -28,11 +28,7 @@ const agencies = ref([]); @on-fetch="(data) => (agencies = data)" auto-load /> - + {{ t(`filterPanel.${tag.label}`) }}: diff --git a/src/pages/Zone/locale/en.yml b/src/pages/Zone/locale/en.yml index bf7d8f0de..ba4982311 100644 --- a/src/pages/Zone/locale/en.yml +++ b/src/pages/Zone/locale/en.yml @@ -61,6 +61,8 @@ eventsPanel: events: Events everyday: Everyday delete: Delete + deleteTitle: This item will be deleted + deleteSubtitle: Are you sure you want to continue? eventsExclusionForm: addExclusion: Add exclusion editExclusion: Edit exclusion @@ -76,6 +78,7 @@ eventsInclusionForm: rangeOfDates: Range of dates from: From to: To + day: Day upcomingDeliveries: province: Province closing: Closing diff --git a/src/pages/Zone/locale/es.yml b/src/pages/Zone/locale/es.yml index 91541bea3..d0bab83f4 100644 --- a/src/pages/Zone/locale/es.yml +++ b/src/pages/Zone/locale/es.yml @@ -61,6 +61,8 @@ eventsPanel: events: Eventos everyday: Todos los días delete: Eliminar + deleteTitle: Eliminar evento + deleteSubtitle: ¿Seguro que quieres eliminar este evento? eventsExclusionForm: addExclusion: Añadir exclusión editExclusion: Editar exclusión @@ -76,5 +78,6 @@ eventsInclusionForm: rangeOfDates: Rango de fechas from: Desde to: Hasta + day: Día upcomingDeliveries: province: Provincia diff --git a/src/router/modules/monitor.js b/src/router/modules/monitor.js index 7342a5904..2af60c09c 100644 --- a/src/router/modules/monitor.js +++ b/src/router/modules/monitor.js @@ -20,6 +20,7 @@ export default { path: '', name: 'MonitorMain', component: () => import('src/components/common/VnSectionMain.vue'), + props: (route) => ({ leftDrawer: route.name === 'MonitorClientsActions' }), redirect: { name: 'MonitorTickets' }, children: [ { diff --git a/src/router/modules/travel.js b/src/router/modules/travel.js index 627692be8..dff693d2f 100644 --- a/src/router/modules/travel.js +++ b/src/router/modules/travel.js @@ -75,9 +75,9 @@ export default { }, { name: 'TravelHistory', - path: 'history', + path: 'log', meta: { - title: 'history', + title: 'log', icon: 'history', }, component: () => import('src/pages/Travel/Card/TravelLog.vue'), diff --git a/src/router/modules/zone.js b/src/router/modules/zone.js index 1f27cc76f..c5ebe762e 100644 --- a/src/router/modules/zone.js +++ b/src/router/modules/zone.js @@ -106,7 +106,7 @@ export default { }, { name: 'ZoneHistory', - path: 'history', + path: 'log', meta: { title: 'log', icon: 'history', diff --git a/src/stores/invoiceOutGlobal.js b/src/stores/invoiceOutGlobal.js index 332494aa8..cc8d86ea8 100644 --- a/src/stores/invoiceOutGlobal.js +++ b/src/stores/invoiceOutGlobal.js @@ -19,7 +19,7 @@ export const useInvoiceOutGlobalStore = defineStore({ maxShipped: null, clientId: null, printer: null, - serialType: null, + serialType: 'global', }, addresses: [], minInvoicingDate: null, @@ -41,7 +41,6 @@ export const useInvoiceOutGlobalStore = defineStore({ async fetchAllData() { try { - const userInfo = await useUserConfig().fetch(); const date = Date.vnNew(); this.formInitialData.maxShipped = new Date( date.getFullYear(), @@ -53,7 +52,7 @@ export const useInvoiceOutGlobalStore = defineStore({ await Promise.all([ this.fetchParallelism(), - this.fetchInvoiceOutConfig(userInfo.companyFk), + this.fetchInvoiceOutConfig(), ]); this.initialDataLoading = false; @@ -62,21 +61,23 @@ export const useInvoiceOutGlobalStore = defineStore({ } }, - async fetchInvoiceOutConfig(companyFk) { + async fetchInvoiceOutConfig(formData = this.formInitialData) { try { - this.formInitialData.companyFk = companyFk; - const params = { companyFk: companyFk }; + const userInfo = await useUserConfig().fetch(); + const params = { + companyFk: userInfo.companyFk, + serialType: formData.serialType, + }; const { data } = await axios.get('InvoiceOuts/getInvoiceDate', { params, }); - const stringDate = data.issued.substring(0, 10); - this.minInvoicingDate = stringDate; - this.formInitialData.invoiceDate = stringDate; - - this.minInvoicingDate = new Date(data.issued); + this.minInvoicingDate = data?.issued + ? new Date(data.issued) + : Date.vnNew(); this.formInitialData.invoiceDate = this.minInvoicingDate; + formData.invoiceDate = this.minInvoicingDate; } catch (err) { console.error('Error fetching invoice out global initial data'); throw new Error(); diff --git a/src/stores/useArrayDataStore.js b/src/stores/useArrayDataStore.js index 6a0e7dfa8..d0a1c3a8f 100644 --- a/src/stores/useArrayDataStore.js +++ b/src/stores/useArrayDataStore.js @@ -17,6 +17,7 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => { searchUrl: 'params', navigate: null, page: 1, + mapKey: 'id', }; function get(key) { @@ -46,6 +47,7 @@ export const useArrayDataStore = defineStore('arrayDataStore', () => { function getDefaultState() { return Object.assign(JSON.parse(JSON.stringify(defaultOpts)), { data: ref(), + map: ref(new Map()), }); } diff --git a/src/utils/dataByOrder.js b/src/utils/dataByOrder.js index 1bdedb8a1..eb4a4f586 100644 --- a/src/utils/dataByOrder.js +++ b/src/utils/dataByOrder.js @@ -1,6 +1,7 @@ function orderData(data, order) { if (typeof order === 'function') return data.sort(data); if (typeof order === 'string') order = [order]; + if (!Array.isArray(data)) return []; if (Array.isArray(order)) { let orderComp = []; diff --git a/test/cypress/integration/Order/orderCatalog.spec.js b/test/cypress/integration/Order/orderCatalog.spec.js new file mode 100644 index 000000000..45eda6f1f --- /dev/null +++ b/test/cypress/integration/Order/orderCatalog.spec.js @@ -0,0 +1,112 @@ +/// +describe('OrderCatalog', () => { + beforeEach(() => { + cy.login('developer'); + cy.viewport(1920, 720); + cy.visit('/#/order/8/catalog'); + }); + + const checkCustomFilterTag = (filterName = 'Plant') => { + cy.dataCy('catalogFilterCustomTag').should('exist'); + cy.dataCy('catalogFilterCustomTag').contains(filterName); + }; + + const checkFilterTag = (filterName = 'Plant') => { + cy.dataCy('vnFilterPanelChip').should('exist'); + cy.dataCy('vnFilterPanelChip').contains(filterName); + }; + + const selectCategory = (categoryIndex = 1, categoryName = 'Plant') => { + cy.get( + `div.q-page-container div:nth-of-type(${categoryIndex}) > [data-cy='catalogFilterCategory']` + ).should('exist'); + cy.get( + `div.q-page-container div:nth-of-type(${categoryIndex}) > [data-cy='catalogFilterCategory']` + ).click(); + checkCustomFilterTag(categoryName); + }; + + const searchByCustomTagInput = (option) => { + cy.dataCy('catalogFilterValueInput').find('input').last().focus(); + cy.dataCy('catalogFilterValueInput').find('input').last().type(option); + cy.dataCy('catalogFilterValueInput').find('input').last().type('{enter}'); + checkCustomFilterTag(option); + }; + + const selectTypeFilter = (option) => { + cy.selectOption( + 'div.q-page-container div.list > div:nth-of-type(2) div:nth-of-type(3)', + option + ); + checkFilterTag(option); + }; + + it('Shows empty state', () => { + cy.dataCy('orderCatalogPage').should('exist'); + cy.dataCy('orderCatalogPage').contains('No data to display'); + }); + + it('filter by category', () => { + selectCategory(); + cy.dataCy('orderCatalogItem').should('exist'); + }); + + it('filters by type', () => { + selectCategory(); + selectTypeFilter('Anthurium'); + }); + + it('filters by custom value select', () => { + selectCategory(); + searchByCustomTagInput('Silver'); + }); + + it('filters by custom value dialog', () => { + Cypress.on('uncaught:exception', (err) => { + if (err.message.includes('canceled')) { + return false; + } + }); + selectCategory(); + cy.dataCy('catalogFilterValueDialogBtn').should('exist'); + cy.dataCy('catalogFilterValueDialogBtn').last().click(); + cy.dataCy('catalogFilterValueDialogTagSelect').should('exist'); + cy.selectOption("[data-cy='catalogFilterValueDialogTagSelect']", 'Tallos'); + cy.dataCy('catalogFilterValueDialogValueInput').find('input').focus(); + cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('2'); + cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('{enter}'); + checkCustomFilterTag('2'); + }); + + it('removes a secondary tag', () => { + selectCategory(); + selectTypeFilter('Anthurium'); + cy.dataCy('vnFilterPanelChip').should('exist'); + cy.get( + "div.q-page-container [data-cy='vnFilterPanelChip'] > i.q-chip__icon--remove" + ) + .contains('cancel') + .should('exist'); + cy.get( + "div.q-page-container [data-cy='vnFilterPanelChip'] > i.q-chip__icon--remove" + ) + .contains('cancel') + .click(); + cy.dataCy('vnFilterPanelChip').should('not.exist'); + }); + + it('Removes category tag', () => { + selectCategory(); + cy.get( + "div.q-page-container [data-cy='catalogFilterCustomTag'] > i.q-chip__icon--remove" + ) + .contains('cancel') + .should('exist'); + cy.get( + "div.q-page-container [data-cy='catalogFilterCustomTag'] > i.q-chip__icon--remove" + ) + .contains('cancel') + .click(); + cy.dataCy('catalogFilterCustomTag').should('not.exist'); + }); +}); diff --git a/test/cypress/integration/claim/claimDevelopment.spec.js b/test/cypress/integration/claim/claimDevelopment.spec.js index eb39f340a..df9d09a49 100755 --- a/test/cypress/integration/claim/claimDevelopment.spec.js +++ b/test/cypress/integration/claim/claimDevelopment.spec.js @@ -3,6 +3,8 @@ describe('ClaimDevelopment', () => { const claimId = 1; const firstLineReason = 'tbody > :nth-child(1) > :nth-child(2)'; const thirdRow = 'tbody > :nth-child(3)'; + const lastReason = 'Incompetencia'; + const newReason = 'Calor'; beforeEach(() => { cy.viewport(1920, 1080); @@ -14,22 +16,22 @@ describe('ClaimDevelopment', () => { }); it('should reset line', () => { - cy.selectOption(firstLineReason, 'Novato'); + cy.selectOption(firstLineReason, newReason); cy.resetCard(); - cy.getValue(firstLineReason).should('equal', 'Prisas'); + cy.getValue(firstLineReason).should('equal', lastReason); }); it('should edit line', () => { - cy.selectOption(firstLineReason, 'Novato'); + cy.selectOption(firstLineReason, newReason); cy.saveCard(); cy.login('developer'); cy.visit(`/#/claim/${claimId}/development`); - cy.getValue(firstLineReason).should('equal', 'Novato'); + cy.getValue(firstLineReason).should('equal', newReason); //Restart data - cy.selectOption(firstLineReason, 'Prisas'); + cy.selectOption(firstLineReason, lastReason); cy.saveCard(); }); @@ -42,7 +44,7 @@ describe('ClaimDevelopment', () => { const rowData = [ false, - 'Novato', + newReason, 'Roces', 'Compradores', 'administrativeNick', diff --git a/test/cypress/integration/client/clientBasicData.spec.js b/test/cypress/integration/client/clientBasicData.spec.js index 7b0a19828..bed28dc22 100644 --- a/test/cypress/integration/client/clientBasicData.spec.js +++ b/test/cypress/integration/client/clientBasicData.spec.js @@ -3,11 +3,17 @@ describe('Client basic data', () => { beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); - cy.visit('#/customer/1110/basic-data', { - timeout: 5000, - }); + cy.visit('#/customer/1102/basic-data'); }); it('Should load layout', () => { cy.get('.q-card').should('be.visible'); + cy.dataCy('customerPhone').find('input').should('be.visible'); + cy.dataCy('customerPhone').find('input').type('123456789'); + cy.get('.q-btn-group > .q-btn--standard').click(); + cy.intercept('PATCH', '/api/Clients/1102', (req) => { + const { body } = req; + cy.wrap(body).should('have.property', 'phone', '123456789'); + }); + cy.get('.q-notification__message').should('have.text', 'Data saved'); }); }); diff --git a/test/cypress/integration/client/clientBillingData.spec.js b/test/cypress/integration/client/clientBillingData.spec.js index 00af82e39..16e8fc0fc 100644 --- a/test/cypress/integration/client/clientBillingData.spec.js +++ b/test/cypress/integration/client/clientBillingData.spec.js @@ -8,6 +8,6 @@ describe('Client billing data', () => { }); }); it('Should load layout', () => { - cy.get('.q-card').should('be.visible'); + cy.get('.q-page').should('be.visible'); }); }); diff --git a/test/cypress/integration/client/clientFiscalData.spec.js b/test/cypress/integration/client/clientFiscalData.spec.js index e337c26f8..05e0772e9 100644 --- a/test/cypress/integration/client/clientFiscalData.spec.js +++ b/test/cypress/integration/client/clientFiscalData.spec.js @@ -3,11 +3,16 @@ describe('Client fiscal data', () => { beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); - cy.visit('#/customer/1110/fiscal-data', { + cy.visit('#/customer/1107/fiscal-data', { timeout: 5000, }); }); - it('Should load layout', () => { + it('Should change required value when change customer', () => { cy.get('.q-card').should('be.visible'); + cy.dataCy('sageTaxTypeFk').filter('input').should('not.have.attr', 'required'); + cy.get('#searchbar input').clear(); + cy.get('#searchbar input').type('1{enter}'); + cy.get('.q-item > .q-item__label').should('have.text', ' #1'); + cy.dataCy('sageTaxTypeFk').filter('input').should('have.attr', 'required'); }); }); diff --git a/test/cypress/integration/client/clientList.spec.js b/test/cypress/integration/client/clientList.spec.js index 431ac0548..dcded63b0 100644 --- a/test/cypress/integration/client/clientList.spec.js +++ b/test/cypress/integration/client/clientList.spec.js @@ -16,23 +16,25 @@ describe('Client list', () => { }); it('Client list create new client', () => { - cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.addBtnClick(); + const randomInt = Math.floor(Math.random() * 90) + 10; + const data = { - Name: { val: 'Name 1' }, - 'Social name': { val: 'TEST 1' }, - 'Tax number': { val: '20852113Z' }, - 'Web user': { val: 'user_test_1' }, - Street: { val: 'C/ STREET 1' }, - Email: { val: 'user.test@1.com' }, - 'Sales person': { val: 'employee', type: 'select' }, - Location: { val: '46000, Valencia(Province one), España', type: 'select' }, + Name: { val: `Name ${randomInt}` }, + 'Social name': { val: `TEST ${randomInt}` }, + 'Tax number': { val: `20852${randomInt}3Z` }, + 'Web user': { val: `user_test_${randomInt}` }, + Street: { val: `C/ STREET ${randomInt}` }, + Email: { val: `user.test${randomInt}@cypress.com` }, + 'Sales person': { val: 'salesPerson', type: 'select' }, + Location: { val: '46000', type: 'select' }, 'Business type': { val: 'Otros', type: 'select' }, }; cy.fillInForm(data); cy.get('.q-mt-lg > .q-btn--standard').click(); - cy.checkNotification('created'); + cy.checkNotification('Data created'); cy.url().should('include', '/summary'); }); it('Client list search client', () => { @@ -54,8 +56,8 @@ describe('Client list', () => { cy.openActionDescriptor('Create ticket'); cy.waitForElement('#formModel'); cy.waitForElement('.q-form'); - cy.checkValueSelectForm(1, search); - cy.checkValueSelectForm(2, search); + cy.checkValueForm(1, search); + cy.checkValueForm(2, search); }); it('Client founded create order', () => { const search = 'Jessica Jones'; diff --git a/test/cypress/integration/client/clientSms.spec.js b/test/cypress/integration/client/clientSms.spec.js new file mode 100644 index 000000000..731522a5c --- /dev/null +++ b/test/cypress/integration/client/clientSms.spec.js @@ -0,0 +1,12 @@ +/// +describe('Client notes', () => { + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit('#/customer/1101/sms'); + }); + it('Should load layout', () => { + cy.get('.q-page').should('be.visible'); + cy.get('.q-page > :nth-child(2) > :nth-child(1)').should('be.visible'); + }); +}); diff --git a/test/cypress/integration/client/clientWebAccess.spec.js b/test/cypress/integration/client/clientWebAccess.spec.js index 47f9efa4c..6803336a3 100644 --- a/test/cypress/integration/client/clientWebAccess.spec.js +++ b/test/cypress/integration/client/clientWebAccess.spec.js @@ -3,11 +3,26 @@ describe('Client web-access', () => { beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); - cy.visit('#/customer/1110/web-access', { - timeout: 5000, - }); }); - it('Should load layout', () => { + it('Should test buttons ', () => { + cy.visit('#/customer/1101/web-access'); + cy.get('.q-page').should('be.visible'); + cy.get('#formModel').should('be.visible'); cy.get('.q-card').should('be.visible'); + cy.get('.q-btn-group > :nth-child(1)').should('not.be.disabled'); + cy.get('.q-checkbox__inner').click(); + cy.get('.q-btn-group > .q-btn--standard.q-btn--actionable').should( + 'not.be.disabled' + ); + cy.get('.q-btn-group > .q-btn--flat').should('not.be.disabled'); + cy.get('.q-btn-group > :nth-child(1)').click(); + cy.get('.q-dialog__inner > .q-card > :nth-child(1)') + .should('be.visible') + .find('.text-h6') + .should('have.text', 'Change password'); + }); + it('Should disabled buttons', () => { + cy.visit('#/customer/1110/web-access'); + cy.get('.q-btn-group > :nth-child(1)').should('be.disabled'); }); }); diff --git a/test/cypress/integration/client/credit-management/clientCreditContracts.spec.js b/test/cypress/integration/client/credit-management/clientCreditContracts.spec.js index 3c35d5ed0..1c2f935fc 100644 --- a/test/cypress/integration/client/credit-management/clientCreditContracts.spec.js +++ b/test/cypress/integration/client/credit-management/clientCreditContracts.spec.js @@ -1,5 +1,5 @@ /// -describe('Client credit opinion', () => { +describe('Client credit contracts', () => { beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); @@ -8,6 +8,6 @@ describe('Client credit opinion', () => { }); }); it('Should load layout', () => { - cy.get('.q-card').should('be.visible'); + cy.get('.q-page').should('be.visible'); }); }); diff --git a/test/cypress/integration/client/others/clientUnpaid.spec.js b/test/cypress/integration/client/others/clientUnpaid.spec.js index 9972ba0e9..ab6bc18bf 100644 --- a/test/cypress/integration/client/others/clientUnpaid.spec.js +++ b/test/cypress/integration/client/others/clientUnpaid.spec.js @@ -3,11 +3,17 @@ describe('Client unpaid', () => { beforeEach(() => { cy.viewport(1280, 720); cy.login('developer'); - cy.visit('#/customer/1110/others/unpaid', { - timeout: 5000, - }); }); - it('Should load layout', () => { + it('Should add unpaid', () => { + cy.visit('#/customer/1102/others/unpaid'); cy.get('.q-card').should('be.visible'); + cy.get('.q-checkbox__inner').click(); + cy.dataCy('customerUnpaidAmount').find('input').type('100'); + cy.dataCy('customerUnpaidDate').find('input').type('01/01/2001'); + cy.get('.q-btn-group > .q-btn--standard').click(); + cy.reload(); + cy.get('.q-checkbox__inner') + .should('be.visible') + .and('not.have.class', 'q-checkbox__inner--active'); }); }); diff --git a/test/cypress/integration/entry/stockBought.spec.js b/test/cypress/integration/entry/stockBought.spec.js index 66e06b79e..078ad19cc 100644 --- a/test/cypress/integration/entry/stockBought.spec.js +++ b/test/cypress/integration/entry/stockBought.spec.js @@ -11,7 +11,7 @@ describe('EntryStockBought', () => { cy.get('.q-notification__message').should('have.text', 'Data saved'); }); it('Should add a new reserved space for buyerBoss', () => { - cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.addBtnClick(); cy.get('input[aria-label="Reserve"]').type('1'); cy.get('input[aria-label="Date"]').eq(1).clear(); cy.get('input[aria-label="Date"]').eq(1).type('01-01'); diff --git a/test/cypress/integration/invoiceIn/invoiceInIntrastat.spec.js b/test/cypress/integration/invoiceIn/invoiceInIntrastat.spec.js index f6dac4c73..4c2550548 100644 --- a/test/cypress/integration/invoiceIn/invoiceInIntrastat.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInIntrastat.spec.js @@ -2,7 +2,7 @@ describe('InvoiceInIntrastat', () => { const firstRow = 'tbody > :nth-child(1)'; const thirdRow = 'tbody > :nth-child(3)'; - const firstRowCode = `${firstRow} > :nth-child(2)`; + const codes = `[data-cy="intrastat-code"]`; const firstRowAmount = `${firstRow} > :nth-child(3)`; beforeEach(() => { @@ -11,13 +11,12 @@ describe('InvoiceInIntrastat', () => { }); it('should edit the first line', () => { - cy.selectOption(firstRowCode, 'Plantas vivas: Esqueje/injerto, Vid'); + cy.selectOption(`${firstRow} ${codes}`, 'Plantas vivas: Esqueje/injerto, Vid'); cy.get(firstRowAmount).clear(); cy.saveCard(); - cy.get(`${firstRowCode} span`).should( - 'have.text', - '6021010:Plantas vivas: Esqueje/injerto, Vid' - ); + cy.get(codes) + .eq(0) + .should('have.value', '6021010: Plantas vivas: Esqueje/injerto, Vid'); }); it('should add a new row', () => { diff --git a/test/cypress/integration/invoiceIn/invoiceInList.spec.js b/test/cypress/integration/invoiceIn/invoiceInList.spec.js index fa0d1c5e4..d9ab3f7e7 100644 --- a/test/cypress/integration/invoiceIn/invoiceInList.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInList.spec.js @@ -1,7 +1,7 @@ /// describe('InvoiceInList', () => { const firstRow = 'tbody.q-virtual-scroll__content tr:nth-child(1)'; - const firstId = `${firstRow} > td:nth-child(1) span`; + const firstId = `${firstRow} > td:nth-child(2) span`; const firstDetailBtn = `${firstRow} .q-btn:nth-child(1)`; const summaryHeaders = '.summaryBody .header-link'; diff --git a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js index b84d743d1..f8b403a45 100644 --- a/test/cypress/integration/invoiceIn/invoiceInVat.spec.js +++ b/test/cypress/integration/invoiceIn/invoiceInVat.spec.js @@ -2,6 +2,7 @@ describe('InvoiceInVat', () => { const thirdRow = 'tbody > :nth-child(3)'; const firstLineVat = 'tbody > :nth-child(1) > :nth-child(4)'; + const vats = '[data-cy="vat-sageiva"]'; const dialogInputs = '.q-dialog label input'; const addBtn = 'tbody tr:nth-child(1) td:nth-child(2) .--add-icon'; const randomInt = Math.floor(Math.random() * 100); @@ -14,9 +15,9 @@ describe('InvoiceInVat', () => { }); it('should edit the sage iva', () => { - cy.selectOption(firstLineVat, 'H.P. IVA 21% CEE'); + cy.selectOption(`${firstLineVat} ${vats}`, 'H.P. IVA 21% CEE'); cy.saveCard(); - cy.get(`${firstLineVat} span`).should('have.text', '8:H.P. IVA 21% CEE'); + cy.get(vats).eq(0).should('have.value', '8: H.P. IVA 21% CEE'); }); it('should add a new row', () => { diff --git a/test/cypress/integration/item/ItemFixedPrice.spec.js b/test/cypress/integration/item/ItemFixedPrice.spec.js new file mode 100644 index 000000000..92dc27fda --- /dev/null +++ b/test/cypress/integration/item/ItemFixedPrice.spec.js @@ -0,0 +1,63 @@ +/// +function goTo(n = 1) { + return `.q-virtual-scroll__content > :nth-child(${n})`; +} +const firstRow = goTo(); +`.q-virtual-scroll__content > :nth-child(2)`; +describe('Handle Items FixedPrice', () => { + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('developer'); + cy.visit('/#/item/fixed-price', { timeout: 5000 }); + cy.waitForElement('.q-table'); + cy.get( + '.q-header > .q-toolbar > :nth-child(1) > .q-btn__content > .q-icon' + ).click(); + }); + it('filter', function () { + cy.get('.category-filter > :nth-child(1) > .q-btn__content > .q-icon').click(); + cy.selectOption('.list > :nth-child(2)', 'Alstroemeria'); + cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click(); + + cy.addBtnClick(); + cy.selectOption(`${firstRow} > :nth-child(2)`, '#13'); + cy.get(`${firstRow} > :nth-child(4)`).find('input').type(1); + cy.get(`${firstRow} > :nth-child(5)`).find('input').type('2'); + cy.selectOption(`${firstRow} > :nth-child(9)`, 'Warehouse One'); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + /* ==== End Cypress Studio ==== */ + }); + it('Create and delete ', function () { + cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click(); + cy.addBtnClick(); + cy.selectOption(`${firstRow} > :nth-child(2)`, '#11'); + cy.get(`${firstRow} > :nth-child(4)`).type('1'); + cy.get(`${firstRow} > :nth-child(5)`).type('2'); + cy.selectOption(`${firstRow} > :nth-child(9)`, 'Warehouse One'); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + cy.get('.q-gutter-x-sm > .q-btn > .q-btn__content > .q-icon').click(); + cy.get(`${firstRow} > .text-right > .q-btn > .q-btn__content > .q-icon`).click(); + cy.get( + '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' + ).click(); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + }); + + it('Massive edit', function () { + cy.get(' .bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner ').click(); + cy.get('#subToolbar > .q-btn--standard').click(); + cy.selectOption("[data-cy='field-to-edit']", 'Min price'); + cy.dataCy('value-to-edit').find('input').type('1'); + cy.get('.countLines').should('have.text', ' 1 '); + cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + }); + it('Massive remove', function () { + cy.get(' .bg-header > :nth-child(1) > .q-checkbox > .q-checkbox__inner ').click(); + cy.get('#subToolbar > .q-btn--flat').click(); + cy.get( + '.q-card__actions > .q-btn--unelevated > .q-btn__content > .block' + ).click(); + cy.get('.q-notification__message').should('have.text', 'Data saved'); + }); +}); diff --git a/test/cypress/integration/item/itemLastEntries.spec.js b/test/cypress/integration/item/itemLastEntries.spec.js new file mode 100644 index 000000000..c94cfa480 --- /dev/null +++ b/test/cypress/integration/item/itemLastEntries.spec.js @@ -0,0 +1,20 @@ +describe('ItemLastEntries', () => { + beforeEach(() => { + cy.viewport(1280, 720); + cy.login('buyer'); + cy.visit('/#/item/1/last-entries'); + cy.intercept('GET', /.*lastEntriesFilter/).as('item'); + cy.waitForElement('tbody'); + }); + + it('should filter by agency', () => { + cy.get('tbody > tr') + .its('length') + .then((rowCount) => { + cy.get('[data-cy="hideInventory"]').click(); + cy.wait('@item'); + cy.waitForElement('tbody'); + cy.get('tbody > tr').should('have.length.greaterThan', rowCount); + }); + }); +}); diff --git a/test/cypress/integration/route/roadMap/roadmapList.spec.js b/test/cypress/integration/route/roadMap/roadmapList.spec.js index ba602fdf6..2f5e5672f 100644 --- a/test/cypress/integration/route/roadMap/roadmapList.spec.js +++ b/test/cypress/integration/route/roadMap/roadmapList.spec.js @@ -5,7 +5,7 @@ describe('RoadMap', () => { cy.visit(`/#/route/roadmap`); }); it('Route list create roadmap and redirect', () => { - cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.addBtnClick(); cy.get('input[name="name"]').eq(1).type('roadMapTestOne{enter}'); cy.get('.q-notification__message').should('have.text', 'Data created'); cy.url().should('include', '/summary'); diff --git a/test/cypress/integration/route/routeList.spec.js b/test/cypress/integration/route/routeList.spec.js index 8020d3ea9..4da43ce8e 100644 --- a/test/cypress/integration/route/routeList.spec.js +++ b/test/cypress/integration/route/routeList.spec.js @@ -9,7 +9,7 @@ describe('Route', () => { const getRowColumn = (row, column) => `:nth-child(${row}) > :nth-child(${column})`; it('Route list create route', () => { - cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); + cy.addBtnClick(); cy.get('input[name="description"]').type('routeTestOne{enter}'); cy.get('.q-notification__message').should('have.text', 'Data created'); cy.url().should('include', '/summary'); diff --git a/test/cypress/integration/ticket/ticketList.spec.js b/test/cypress/integration/ticket/ticketList.spec.js index bbdbcea92..c1d1a0655 100644 --- a/test/cypress/integration/ticket/ticketList.spec.js +++ b/test/cypress/integration/ticket/ticketList.spec.js @@ -37,7 +37,7 @@ describe('TicketList', () => { cy.dataCy('ticketSummary').should('exist'); }); - it('Client list create new client', () => { + it.only('Client list create new client', () => { cy.dataCy('vnTableCreateBtn').should('exist'); cy.dataCy('vnTableCreateBtn').click(); const data = { @@ -47,7 +47,8 @@ describe('TicketList', () => { Landed: { val: '01-01-2024', type: 'date' }, }; cy.fillInForm(data); - cy.get('.q-mt-lg > .q-btn--standard').click(); + cy.dataCy('Agency_select').click(); + cy.dataCy('FormModelPopup_save').click(); cy.checkNotification('Data created'); cy.url().should('match', /\/ticket\/\d+\/summary/); }); diff --git a/test/cypress/integration/vnComponent/VnBreadcrumbs.spec.js b/test/cypress/integration/vnComponent/VnBreadcrumbs.spec.js index 3c839c1c7..e996a65d5 100644 --- a/test/cypress/integration/vnComponent/VnBreadcrumbs.spec.js +++ b/test/cypress/integration/vnComponent/VnBreadcrumbs.spec.js @@ -3,6 +3,7 @@ describe('VnBreadcrumbs', () => { const firstCard = '.q-infinite-scroll > :nth-child(1)'; const lastBreadcrumb = '.q-breadcrumbs--last > .q-breadcrumbs__el'; beforeEach(() => { + cy.viewport(1920, 1080); cy.login('developer'); cy.visit('/'); }); diff --git a/test/cypress/integration/vnComponent/VnLocation.spec.js b/test/cypress/integration/vnComponent/VnLocation.spec.js index b98d42fdd..82d12a1e4 100644 --- a/test/cypress/integration/vnComponent/VnLocation.spec.js +++ b/test/cypress/integration/vnComponent/VnLocation.spec.js @@ -43,11 +43,9 @@ describe('VnLocation', () => { province ); cy.get( - `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) > .q-icon` + `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) ` ).click(); - cy.get( - `#q-portal--dialog--5 > .q-dialog > ${createForm.prefix} > .vn-row > .q-select > ${createForm.sufix} > :nth-child(1) input` - ).should('have.value', province); + cy.dataCy('locationProvince').should('have.value', province); }); }); describe('Worker Create', () => { @@ -123,32 +121,16 @@ describe('VnLocation', () => { const province = randomString({ length: 4 }); cy.get(createLocationButton).click(); cy.get(dialogInputs).eq(0).type(postCode); - cy.get( - `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(2) > .q-icon` - ).click(); - cy.selectOption('#q-portal--dialog--3 .q-select', 'one'); - cy.get('#q-portal--dialog--3 .q-input').type(province); - cy.get('#q-portal--dialog--3 .q-btn--standard').click(); - cy.get('#q-portal--dialog--1 .q-btn--standard').click(); + cy.dataCy('City_icon').click(); + cy.selectOption('[data-cy="locationProvince"]:last', 'Province one'); + cy.dataCy('cityName').type(province); + cy.dataCy('FormModelPopup_save').eq(1).click(); + cy.dataCy('FormModelPopup_save').eq(0).click(); + cy.waitForElement('.q-form'); checkVnLocation(postCode, province); }); - it('Create province without country', () => { - const provinceName = 'Saskatchew'.concat(Math.random(1 * 100)); - cy.get(createLocationButton).click(); - cy.get( - `${createForm.prefix} > :nth-child(5) > .q-select > ${createForm.sufix} > :nth-child(2) ` - ) - .eq(0) - .click(); - cy.selectOption('#q-portal--dialog--3 .q-select', 'one'); - cy.countSelectOptions('#q-portal--dialog--3 .q-select', 4); - cy.get('#q-portal--dialog--3 .q-input').type(provinceName); - - cy.get('#q-portal--dialog--3 .q-btn--standard').click(); - }); - it('Create city with country', () => { const cityName = 'Saskatchew'.concat(Math.random(1 * 100)); cy.get(createLocationButton).click(); @@ -156,14 +138,23 @@ describe('VnLocation', () => { `${createForm.prefix} > :nth-child(5) > :nth-child(3) `, 'Italia' ); - cy.get( - `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(2) > .q-icon` - ).click(); - cy.selectOption('#q-portal--dialog--4 .q-select', 'Province four'); - cy.countSelectOptions('#q-portal--dialog--4 .q-select', 1); + cy.dataCy('City_icon').click(); + cy.selectOption('[data-cy="locationProvince"]:last', 'Province four'); + cy.countSelectOptions('[data-cy="locationProvince"]:last', 1); - cy.get('#q-portal--dialog--4 .q-input').type(cityName); - cy.get('#q-portal--dialog--4 .q-btn--standard').click(); + cy.dataCy('cityName').type(cityName); + cy.dataCy('FormModelPopup_save').eq(1).click(); + }); + + it('Create province without country', () => { + const provinceName = 'Saskatchew'.concat(Math.random(1 * 100)); + cy.get(createLocationButton).click(); + cy.dataCy('Province_icon').click(); + cy.selectOption('[data-cy="autonomyProvince"] ', 'Autonomy one'); + cy.countSelectOptions('[data-cy="autonomyProvince"]', 4); + cy.dataCy('provinceName').type(provinceName); + + cy.dataCy('FormModelPopup_save').eq(1).click(); }); it('Create province with country', () => { @@ -173,17 +164,13 @@ describe('VnLocation', () => { `${createForm.prefix} > :nth-child(5) > :nth-child(3) `, 'España' ); - cy.get( - `${createForm.prefix} > :nth-child(5) > .q-select > ${createForm.sufix} > :nth-child(2) ` - ) - .eq(0) - .click(); + cy.dataCy('Province_icon').click(); - cy.selectOption('#q-portal--dialog--4 .q-select', 'one'); - cy.countSelectOptions('#q-portal--dialog--4 .q-select', 2); + cy.selectOption('[data-cy="autonomyProvince"] ', 'Autonomy one'); + cy.countSelectOptions('[data-cy="autonomyProvince"]', 2); - cy.get('#q-portal--dialog--4 .q-input').type(provinceName); - cy.get('#q-portal--dialog--4 .q-btn--standard').click(); + cy.dataCy('provinceName').type(provinceName); + cy.dataCy('FormModelPopup_save').eq(1).click(); }); function checkVnLocation(postCode, province) { diff --git a/test/cypress/integration/worker/workerPda.spec.js b/test/cypress/integration/worker/workerPda.spec.js index fe8efa834..31ec19eda 100644 --- a/test/cypress/integration/worker/workerPda.spec.js +++ b/test/cypress/integration/worker/workerPda.spec.js @@ -1,6 +1,5 @@ describe('WorkerPda', () => { - const deviceProductionField = - '.vn-row > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; + const select = '[data-cy="pda-dialog-select"]'; beforeEach(() => { cy.viewport(1920, 1080); cy.login('developer'); @@ -8,8 +7,9 @@ describe('WorkerPda', () => { }); it('assign pda', () => { - cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); - cy.get(deviceProductionField).type('{downArrow}{enter}'); + cy.addBtnClick(); + cy.get(select).click(); + cy.get(select).type('{downArrow}{enter}'); cy.get('.q-notification__message').should('have.text', 'Data created'); }); diff --git a/test/cypress/integration/zone/zoneBasicData.spec.js b/test/cypress/integration/zone/zoneBasicData.spec.js index c6151a49b..6229039b7 100644 --- a/test/cypress/integration/zone/zoneBasicData.spec.js +++ b/test/cypress/integration/zone/zoneBasicData.spec.js @@ -1,5 +1,6 @@ describe('ZoneBasicData', () => { const notification = '.q-notification__message'; + const priceBasicData = '[data-cy="Price_input"]'; beforeEach(() => { cy.viewport(1280, 720); @@ -8,14 +9,20 @@ describe('ZoneBasicData', () => { }); it('should throw an error if the name is empty', () => { - cy.get('.q-card > :nth-child(1)').clear(); + cy.get('[data-cy="zone-basic-data-name"] input').type('{selectall}{backspace}'); cy.get('.q-btn-group > .q-btn--standard').click(); cy.get(notification).should('contains.text', "can't be blank"); }); + it('should throw an error if the price is empty', () => { + cy.get(priceBasicData).clear(); + cy.get('.q-btn-group > .q-btn--standard').click(); + cy.get(notification).should('contains.text', 'cannot be blank'); + }); + it("should edit the basicData's zone", () => { cy.get('.q-card > :nth-child(1)').type(' modified'); cy.get('.q-btn-group > .q-btn--standard').click(); - cy.get(notification).should('contains.text', 'Data saved'); + cy.checkNotification('Data saved'); }); }); diff --git a/test/cypress/integration/zone/zoneWarehouse.spec.js b/test/cypress/integration/zone/zoneWarehouse.spec.js index 3ffa3f69d..817e26312 100644 --- a/test/cypress/integration/zone/zoneWarehouse.spec.js +++ b/test/cypress/integration/zone/zoneWarehouse.spec.js @@ -1,10 +1,10 @@ describe('ZoneWarehouse', () => { const data = { - Warehouse: { val: 'Algemesi', type: 'select' }, + Warehouse: { val: 'Warehouse One', type: 'select' }, }; - const deviceProductionField = - '.vn-row > :nth-child(1) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container'; - const dataError = "ER_DUP_ENTRY: Duplicate entry '2-2' for key 'zoneFk'"; + + const dataError = 'ER_DUP_ENTRY: Duplicate entry'; + const saveBtn = '.q-btn--standard > .q-btn__content > .block'; beforeEach(() => { cy.viewport(1280, 720); @@ -13,22 +13,21 @@ describe('ZoneWarehouse', () => { }); it('should throw an error if the warehouse chosen is already put in the zone', () => { - cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); - cy.get(deviceProductionField).click(); - cy.get(deviceProductionField).type('{upArrow}{enter}'); - cy.get('.q-notification__message').should('have.text', dataError); + cy.addBtnClick(); + cy.selectOption('[data-cy="Warehouse_select"]', 'Warehouse Two'); + cy.get(saveBtn).click(); + cy.checkNotification(dataError); }); - it('should create a warehouse', () => { - cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon').click(); - cy.get(deviceProductionField).click(); + it('should create & remove a warehouse', () => { + cy.addBtnClick(); cy.fillInForm(data); + cy.get(saveBtn).click(); cy.get('.q-mt-lg > .q-btn--standard').click(); - }); - it('should delete a warehouse', () => { cy.get('tbody > :nth-child(2) > :nth-child(2) > .q-icon').click(); - cy.get('.q-card__actions > .q-btn--flat > .q-btn__content').click(); + cy.get('[title="Confirm"]').click(); + cy.reload(); }); }); diff --git a/test/cypress/support/commands.js b/test/cypress/support/commands.js index 53f9d1d73..df2c00e03 100755 --- a/test/cypress/support/commands.js +++ b/test/cypress/support/commands.js @@ -58,8 +58,9 @@ Cypress.Commands.add('domContentLoad', (element, timeout = 5000) => { cy.waitUntil(() => cy.document().then((doc) => doc.readyState === 'complete')); }); Cypress.Commands.add('waitForElement', (element, timeout = 5000) => { - cy.waitUntil(() => cy.get(element).then(($el) => $el.is(':visible'))); + cy.get(element, { timeout }).should('be.visible').and('not.be.disabled'); }); + Cypress.Commands.add('getValue', (selector) => { cy.get(selector).then(($el) => { if ($el.find('.q-checkbox__inner').length > 0) { @@ -86,14 +87,40 @@ Cypress.Commands.add('getValue', (selector) => { }); // Fill Inputs -Cypress.Commands.add('selectOption', (selector, option) => { - cy.waitForElement(selector); +Cypress.Commands.add('selectOption', (selector, option, timeout = 5000) => { + cy.waitForElement(selector, timeout); cy.get(selector).click(); - cy.get('.q-menu .q-item').contains(option).click(); + cy.get(selector).invoke('data', 'url').as('dataUrl'); + cy.get(selector) + .clear() + .type(option) + .then(() => { + cy.get('.q-menu', { timeout }) + .should('be.visible') // Asegurarse de que el menú está visible + .and('exist') // Verificar que el menú existe + .then(() => { + cy.get('@dataUrl').then((url) => { + if (url) { + cy.log('url: ', url); + // Esperar a que el menú no esté visible (desaparezca) + cy.get('.q-menu').should('not.be.visible'); + // Ahora esperar a que el menú vuelva a aparecer + cy.get('.q-menu').should('be.visible').and('exist'); + } + }); + }); + }); + + // Finalmente, seleccionar la opción deseada + cy.get('.q-menu:visible') // Asegurarse de que estamos dentro del menú visible + .find('.q-item') // Encontrar los elementos de las opciones + .contains(option) // Verificar que existe una opción que contenga el texto deseado + .click(); // Hacer clic en la opción }); + Cypress.Commands.add('countSelectOptions', (selector, option) => { cy.waitForElement(selector); - cy.get(selector).click(); + cy.get(selector).click({ force: true }); cy.get('.q-menu .q-item').should('have.length', option); }); @@ -109,14 +136,13 @@ Cypress.Commands.add('fillInForm', (obj, form = '.q-form > .q-card') => { const { type, val } = field; switch (type) { case 'select': - cy.wrap(el).type(val); - cy.get('.q-menu .q-item').contains(val).click(); + cy.selectOption(el, val); break; case 'date': - cy.wrap(el).type(val.split('-').join('')); + cy.get(el).type(val.split('-').join('')); break; case 'time': - cy.wrap(el).click(); + cy.get(el).click(); cy.get('.q-time .q-time__clock').contains(val.h).click(); cy.get('.q-time .q-time__clock').contains(val.m).click(); cy.get('.q-time .q-time__link').contains(val.x).click(); @@ -259,6 +285,7 @@ Cypress.Commands.add('writeSearchbar', (value) => { value ); }); + Cypress.Commands.add('validateContent', (selector, expectedValue) => { cy.get(selector).should('have.text', expectedValue); }); @@ -280,16 +307,38 @@ Cypress.Commands.add('clickButtonsDescriptor', (id) => { .click(); }); +Cypress.Commands.add('openActionDescriptor', (opt) => { + cy.openActionsDescriptor(); + const listItem = '[role="menu"] .q-list .q-item'; + cy.contains(listItem, opt).click(); + 1; +}); + +Cypress.Commands.add('clickButtonsDescriptor', (id) => { + cy.get(`.actions > .q-card__actions> .q-btn:nth-child(${id})`) + .invoke('removeAttr', 'target') + .click(); +}); + +Cypress.Commands.add('openActionDescriptor', (opt) => { + cy.openActionsDescriptor(); + const listItem = '[role="menu"] .q-list .q-item'; + cy.contains(listItem, opt).click(); + 1; +}); + +Cypress.Commands.add('clickButtonsDescriptor', (id) => { + cy.get(`.actions > .q-card__actions> .q-btn:nth-child(${id})`) + .invoke('removeAttr', 'target') + .click(); +}); + Cypress.Commands.add('openUserPanel', () => { cy.get( '.column > .q-avatar > .q-avatar__content > .q-img > .q-img__container > .q-img__image' ).click(); }); -Cypress.Commands.add('openActions', (row) => { - cy.get('tbody > tr').eq(row).find('.actions > .q-btn').click(); -}); - Cypress.Commands.add('checkNotification', (text) => { cy.get('.q-notification') .should('be.visible') @@ -300,10 +349,14 @@ Cypress.Commands.add('checkNotification', (text) => { }); }); +Cypress.Commands.add('openActions', (row) => { + cy.get('tbody > tr').eq(row).find('.actions > .q-btn').click(); +}); + Cypress.Commands.add('checkValueForm', (id, search) => { - cy.get( - `.grid-create > :nth-child(${id}) > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native > .q-field__input` - ).should('have.value', search); + cy.get(`.grid-create > :nth-child(${id}) `) + .find('input') + .should('have.value', search); }); Cypress.Commands.add('checkValueSelectForm', (id, search) => { @@ -316,6 +369,13 @@ Cypress.Commands.add('searchByLabel', (label, value) => { cy.get(`[label="${label}"] > .q-field > .q-field__inner`).type(`${value}{enter}`); }); -Cypress.Commands.add('dataCy', (dataTestId, attr = 'data-cy') => { - return cy.get(`[${attr}="${dataTestId}"]`); +Cypress.Commands.add('dataCy', (tag, attr = 'data-cy') => { + return cy.get(`[${attr}="${tag}"]`); +}); + +Cypress.Commands.add('addBtnClick', () => { + cy.get('.q-page-sticky > div > .q-btn > .q-btn__content > .q-icon') + .should('exist') + .and('be.visible') + .click(); }); diff --git a/test/vitest/__tests__/components/Paginate.spec.js b/test/vitest/__tests__/components/Paginate.spec.js index 345903c1a..a67dfcdc6 100644 --- a/test/vitest/__tests__/components/Paginate.spec.js +++ b/test/vitest/__tests__/components/Paginate.spec.js @@ -4,7 +4,11 @@ import VnPaginate from 'src/components/ui/VnPaginate.vue'; describe('VnPaginate', () => { const expectedUrl = '/api/customers'; - + const defaultData = [ + { id: 1, name: 'Tony Stark' }, + { id: 2, name: 'Jessica Jones' }, + { id: 3, name: 'Bruce Wayne' }, + ]; let vm; beforeAll(() => { const options = { @@ -28,11 +32,7 @@ describe('VnPaginate', () => { describe('paginate()', () => { it('should call to the paginate() method and set the data on the rows property', async () => { vi.spyOn(vm.arrayData, 'loadMore'); - vm.store.data = [ - { id: 1, name: 'Tony Stark' }, - { id: 2, name: 'Jessica Jones' }, - { id: 3, name: 'Bruce Wayne' }, - ]; + vm.store.data = defaultData; await vm.paginate(); @@ -42,26 +42,25 @@ describe('VnPaginate', () => { it('should call to the paginate() method and then call it again to paginate', async () => { vi.spyOn(axios, 'get').mockResolvedValue({ - data: [ - { id: 1, name: 'Tony Stark' }, - { id: 2, name: 'Jessica Jones' }, - { id: 3, name: 'Bruce Wayne' }, - ], + data: defaultData, }); vm.store.hasMoreData = true; await vm.$nextTick(); - vm.store.data = [ - { id: 1, name: 'Tony Stark' }, - { id: 2, name: 'Jessica Jones' }, - { id: 3, name: 'Bruce Wayne' }, - ]; + vm.store.data = defaultData; await vm.paginate(); expect(vm.store.skip).toEqual(3); expect(vm.store.data.length).toEqual(6); + vi.spyOn(axios, 'get').mockResolvedValue({ + data: [ + { id: 4, name: 'Peter Parker' }, + { id: 5, name: 'Clark Kent' }, + { id: 6, name: 'Barry Allen' }, + ], + }); await vm.paginate(); expect(vm.store.skip).toEqual(6); @@ -85,11 +84,7 @@ describe('VnPaginate', () => { const index = 1; const done = vi.fn(); - vm.store.data = [ - { id: 1, name: 'Tony Stark' }, - { id: 2, name: 'Jessica Jones' }, - { id: 3, name: 'Bruce Wayne' }, - ]; + vm.store.data = defaultData; await vm.onLoad(index, done); @@ -105,11 +100,7 @@ describe('VnPaginate', () => { ], }); - vm.store.data = [ - { id: 1, name: 'Tony Stark' }, - { id: 2, name: 'Jessica Jones' }, - { id: 3, name: 'Bruce Wayne' }, - ]; + vm.store.data = defaultData; expect(vm.pagination.page).toEqual(1); diff --git a/test/vitest/__tests__/components/common/VnDiscount.spec.js b/test/vitest/__tests__/components/common/VnDiscount.spec.js new file mode 100644 index 000000000..5d5be61ac --- /dev/null +++ b/test/vitest/__tests__/components/common/VnDiscount.spec.js @@ -0,0 +1,28 @@ +import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest'; +import { createWrapper } from 'app/test/vitest/helper'; +import VnDiscount from 'components/common/vnDiscount.vue'; + +describe('VnDiscount', () => { + let vm; + + beforeAll(() => { + vm = createWrapper(VnDiscount, { + props: { + data: {}, + price: 100, + quantity: 2, + discount: 10, + } + }).vm; + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + describe('total', () => { + it('should calculate total correctly', () => { + expect(vm.total).toBe(180); + }); + }); +}); \ No newline at end of file diff --git a/test/vitest/__tests__/components/common/VnLinkPhone.spec.js b/test/vitest/__tests__/components/common/VnLinkPhone.spec.js new file mode 100644 index 000000000..a34ef90a5 --- /dev/null +++ b/test/vitest/__tests__/components/common/VnLinkPhone.spec.js @@ -0,0 +1,50 @@ +import { describe, it, expect, beforeAll, vi } from 'vitest'; +import { axios } from 'app/test/vitest/helper'; +import parsePhone from 'src/filters/parsePhone'; + +describe('parsePhone filter', () => { + beforeAll(async () => { + vi.spyOn(axios, 'get').mockReturnValue({ data: { prefix: '34' } }); + }); + + it('no phone', async () => { + const phone = await parsePhone(null, '34'); + expect(phone).toBe(undefined); + }); + + it("adds prefix +34 if it doesn't have one", async () => { + const phone = await parsePhone('123456789', '34'); + expect(phone).toBe('34123456789'); + }); + + it('maintains prefix +34 if it is already correct', async () => { + const phone = await parsePhone('+34123456789', '34'); + expect(phone).toBe('34123456789'); + }); + + it('converts prefix 0034 to +34', async () => { + const phone = await parsePhone('0034123456789', '34'); + expect(phone).toBe('34123456789'); + }); + + it('converts prefix 34 without symbol to +34', async () => { + const phone = await parsePhone('34123456789', '34'); + expect(phone).toBe('34123456789'); + }); + + it('replaces incorrect prefix with the correct one', async () => { + const phone = await parsePhone('+44123456789', '34'); + expect(phone).toBe('44123456789'); + }); + + it('adds default prefix on error', async () => { + vi.spyOn(axios, 'get').mockImplementation((url) => { + if (url.includes('Prefixes')) + return Promise.reject(new Error('Network error')); + else if (url.includes('PbxConfigs')) + return Promise.resolve({ data: { defaultPrefix: '39' } }); + }); + const phone = await parsePhone('123456789', '34'); + expect(phone).toBe('39123456789'); + }); +}); diff --git a/test/vitest/__tests__/composables/getExchange.spec.js b/test/vitest/__tests__/composables/getExchange.spec.js new file mode 100644 index 000000000..dba31458e --- /dev/null +++ b/test/vitest/__tests__/composables/getExchange.spec.js @@ -0,0 +1,45 @@ +import { describe, expect, it, vi } from 'vitest'; +import axios from 'axios'; +import { getExchange } from 'src/composables/getExchange'; + +vi.mock('axios'); + +describe('getExchange()', () => { + it('should return the correct exchange rate', async () => { + axios.get.mockResolvedValue({ + data: { value: 1.2 }, + }); + + const amount = 100; + const currencyFk = 1; + const dated = '2023-01-01'; + const result = await getExchange(amount, currencyFk, dated); + + expect(result).toBe('83.33'); + }); + + it('should return the correct exchange rate with custom decimal places', async () => { + axios.get.mockResolvedValue({ + data: { value: 1.2 }, + }); + + const amount = 100; + const currencyFk = 1; + const dated = '2023-01-01'; + const decimalPlaces = 3; + const result = await getExchange(amount, currencyFk, dated, decimalPlaces); + + expect(result).toBe('83.333'); + }); + + it('should return null if the API call fails', async () => { + axios.get.mockRejectedValue(new Error('Network error')); + + const amount = 100; + const currencyFk = 1; + const dated = '2023-01-01'; + const result = await getExchange(amount, currencyFk, dated); + + expect(result).toBeNull(); + }); +}); diff --git a/test/vitest/__tests__/composables/getTotal.spec.js b/test/vitest/__tests__/composables/getTotal.spec.js new file mode 100644 index 000000000..789e3fbcf --- /dev/null +++ b/test/vitest/__tests__/composables/getTotal.spec.js @@ -0,0 +1,55 @@ +import { vi, describe, expect, it } from 'vitest'; +import { getTotal } from 'src/composables/getTotal'; + +vi.mock('src/filters', () => ({ + toCurrency: vi.fn((value, currency) => `${currency} ${value.toFixed(2)}`), +})); + +describe('getTotal()', () => { + const rows = [ + { amount: 10.5, tax: 2.1 }, + { amount: 20.75, tax: 3.25 }, + { amount: 30.25, tax: 4.75 }, + ]; + + it('should calculate the total for a given key', () => { + const total = getTotal(rows, 'amount'); + expect(total).toBe('61.50'); + }); + + it('should calculate the total with a callback function', () => { + const total = getTotal(rows, null, { cb: (row) => row.amount + row.tax }); + expect(total).toBe('71.60'); + }); + + it('should format the total as currency', () => { + const total = getTotal(rows, 'amount', { currency: 'USD' }); + expect(total).toBe('USD 61.50'); + }); + + it('should format the total as currency with default currency', () => { + const total = getTotal(rows, 'amount', { currency: 'default' }); + expect(total).toBe('undefined 61.50'); + }); + + it('should calculate the total with integer formatting', () => { + const total = getTotal(rows, 'amount', { decimalPlaces: 0 }); + expect(total).toBe('62'); + }); + + it('should calculate the total with custom decimal places', () => { + const total = getTotal(rows, 'amount', { decimalPlaces: 1 }); + expect(total).toBe('61.5'); + }); + + it('should handle rows with missing keys', () => { + const rowsWithMissingKeys = [{ amount: 10.5 }, { amount: 20.75 }, {}]; + const total = getTotal(rowsWithMissingKeys, 'amount'); + expect(total).toBe('31.25'); + }); + + it('should handle empty rows', () => { + const total = getTotal([], 'amount'); + expect(total).toBe('0.00'); + }); +}); diff --git a/test/vitest/__tests__/composables/useAccountShortToStandard.spec.js b/test/vitest/__tests__/composables/useAccountShortToStandard.spec.js new file mode 100644 index 000000000..d24585812 --- /dev/null +++ b/test/vitest/__tests__/composables/useAccountShortToStandard.spec.js @@ -0,0 +1,9 @@ +import { describe, expect, it } from 'vitest'; +import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard'; + +describe('useAccountShortToStandard()', () => { + it('should pad the decimal part with zeros for short numbers', () => { + expect(useAccountShortToStandard('123.45')).toBe('1230000045'); + expect(useAccountShortToStandard('123.')).toBe('1230000000'); + }); +}); diff --git a/test/vitest/__tests__/pages/InvoiceIn/InvoiceInIntrastat.spec.js b/test/vitest/__tests__/pages/InvoiceIn/InvoiceInIntrastat.spec.js deleted file mode 100644 index adfb054c6..000000000 --- a/test/vitest/__tests__/pages/InvoiceIn/InvoiceInIntrastat.spec.js +++ /dev/null @@ -1,34 +0,0 @@ -import { vi, describe, expect, it, beforeAll } from 'vitest'; -import { createWrapper, axios } from 'app/test/vitest/helper'; -import InvoiceInIntrastat from 'src/pages/InvoiceIn/Card/InvoiceInIntrastat.vue'; - -describe('InvoiceInIntrastat', () => { - let vm; - - beforeAll(() => { - vm = createWrapper(InvoiceInIntrastat, { - global: { - stubs: ['vnPaginate'], - mocks: { - fetch: vi.fn(), - }, - }, - }).vm; - vi.spyOn(axios, 'get').mockResolvedValue({ data: [{}] }); - }); - - describe('getTotal()', () => { - it('should correctly handle the sum', () => { - const invoceInIntrastat = [ - { amount: 10, stems: 162 }, - { amount: 20, stems: 21 }, - ]; - - const totalAmount = vm.getTotal(invoceInIntrastat, 'amount'); - const totalStems = vm.getTotal(invoceInIntrastat, 'stems'); - - expect(totalAmount).toBe(10 + 20); - expect(totalStems).toBe(162 + 21); - }); - }); -}); diff --git a/test/vitest/__tests__/pages/InvoiceIn/InvoiceInVat.spec.js b/test/vitest/__tests__/pages/InvoiceIn/InvoiceInVat.spec.js deleted file mode 100644 index 76453f65a..000000000 --- a/test/vitest/__tests__/pages/InvoiceIn/InvoiceInVat.spec.js +++ /dev/null @@ -1,38 +0,0 @@ -import { vi, describe, expect, it, beforeAll } from 'vitest'; -import { createWrapper } from 'app/test/vitest/helper'; -import InvoiceInVat from 'src/pages/InvoiceIn/Card/InvoiceInVat.vue'; - -describe('InvoiceInVat', () => { - let vm; - - beforeAll(() => { - vm = createWrapper(InvoiceInVat, { - global: { - stubs: [], - mocks: { - fetch: vi.fn(), - }, - }, - }).vm; - }); - - describe('taxRate()', () => { - it('should correctly compute the tax rate', () => { - const invoiceInTax = { taxableBase: 100, taxTypeSageFk: 1 }; - vm.sageTaxTypes = [ - { id: 1, rate: 10 }, - { id: 2, rate: 20 }, - ]; - const result = vm.taxRate(invoiceInTax); - expect(result).toBe((10 / 100) * 100); - }); - - it('should return 0 if there is not tax rate', () => { - const invoiceInTax = { taxableBase: 100, taxTypeSageFk: 1 }; - vm.sageTaxTypes = []; - - const result = vm.taxRate(invoiceInTax); - expect(result).toBe(0); - }); - }); -}); diff --git a/test/vitest/helper.js b/test/vitest/helper.js index 4bfae5dc8..ce057c7c3 100644 --- a/test/vitest/helper.js +++ b/test/vitest/helper.js @@ -44,7 +44,18 @@ vi.mock('vue-router', () => ({ vi.mock('axios'); vi.spyOn(useValidator, 'useValidator').mockImplementation(() => { - return { validate: vi.fn() }; + return { + validate: vi.fn(), + validations: () => ({ + format: vi.fn(), + presence: vi.fn(), + required: vi.fn(), + length: vi.fn(), + numericality: vi.fn(), + min: vi.fn(), + custom: vi.fn(), + }), + }; }); class FormDataMock {
{{ t('route.Select the starting date') }}