Merge branch 'dev' into 7069-testVnAccountNumber
gitea/salix-front/pipeline/pr-dev This commit looks good Details

This commit is contained in:
Jose Antonio Tubau 2025-05-16 13:09:16 +00:00
commit feef61bf32
43 changed files with 1066 additions and 638 deletions

81
Jenkinsfile vendored
View File

@ -108,7 +108,6 @@ pipeline {
} }
stage('E2E') { stage('E2E') {
environment { environment {
CREDS = credentials('docker-registry')
COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}" COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}"
COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ." COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ."
} }
@ -118,23 +117,24 @@ pipeline {
sh 'rm -rf test/cypress/screenshots' sh 'rm -rf test/cypress/screenshots'
env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev' env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev'
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs') def modules = sh(
script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}",
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY' returnStdout: true
sh "docker-compose ${env.COMPOSE_PARAMS} pull back" ).trim()
sh "docker-compose ${env.COMPOSE_PARAMS} pull db"
sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
def modules = sh(script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}", returnStdout: true).trim()
echo "E2E MODULES: ${modules}" echo "E2E MODULES: ${modules}"
script {
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh "docker compose ${env.COMPOSE_PARAMS} up -d"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") { image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'" sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'"
} }
} }
} }
}
post { post {
always { always {
sh "docker-compose ${env.COMPOSE_PARAMS} down -v" sh "docker compose ${env.COMPOSE_PARAMS} down -v"
archiveArtifacts artifacts: 'test/cypress/screenshots/**/*', allowEmptyArchive: true archiveArtifacts artifacts: 'test/cypress/screenshots/**/*', allowEmptyArchive: true
junit( junit(
testResults: 'junit/e2e-*.xml', testResults: 'junit/e2e-*.xml',
@ -153,17 +153,8 @@ pipeline {
VERSION = readFile 'VERSION.txt' VERSION = readFile 'VERSION.txt'
} }
steps { steps {
script {
sh 'quasar build' sh 'quasar build'
dockerBuild 'salix-frontend', '.'
def baseImage = "salix-frontend:${env.VERSION}"
def image = docker.build(baseImage, ".")
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
image.push()
image.push(env.BRANCH_NAME)
if (IS_LATEST) image.push('latest')
}
}
} }
} }
stage('Deploy') { stage('Deploy') {
@ -186,3 +177,53 @@ pipeline {
} }
} }
def dockerBuild(imageName, context, dockerfile = null) {
if (dockerfile == null)
dockerfile = "${context}/Dockerfile"
def certDir = '/home/jenkins/.buildkit/certs'
def buildxArgs = [
"--name=buildkitd",
"--driver=remote",
"--driver-opt="
+ "cacert=${certDir}/ca.pem,"
+ "cert=${certDir}/cert.pem,"
+ "key=${certDir}/key.pem,"
+ "servername=buildkitd",
"tcp://buildkitd:1234"
]
def cacheImage = "${env.REGISTRY_CACHE}/${imageName}"
def pushImage = "${env.REGISTRY}/${imageName}"
def baseImage = "${pushImage}:${env.VERSION}"
def buildArgs = [
context,
"--push",
"--builder=buildkitd",
"--file=${dockerfile}",
"--cache-from=type=registry,ref=${cacheImage}:cache-${env.BRANCH_NAME}",
"--cache-from=type=registry,ref=${cacheImage}:cache-dev",
"--cache-to=type=registry,ref=${cacheImage}:cache-${env.BRANCH_NAME},mode=max",
"--tag=${pushImage}:${env.BRANCH_NAME}"
]
def isLatest = ['master', 'main'].contains(env.BRANCH_NAME)
if (isLatest)
buildArgs.push("--tag=${pushImage}:latest")
// FIXME: Nested docker.withRegistry() does not work
// https://issues.jenkins.io/browse/JENKINS-59777
withCredentials([usernamePassword(
credentialsId: 'registry-cache',
usernameVariable: 'CACHE_USR',
passwordVariable: 'CACHE_PSW')
]) {
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
sh 'echo "$CACHE_PSW" | docker login --username "$CACHE_USR" --password-stdin "http://$REGISTRY_CACHE"'
sh "docker buildx create ${buildxArgs.join(' ')}"
docker.build(baseImage, buildArgs.join(' '))
}
}
}

View File

@ -1,6 +1,6 @@
{ {
"name": "salix-front", "name": "salix-front",
"version": "25.18.0", "version": "25.22.0",
"description": "Salix frontend", "description": "Salix frontend",
"productName": "Salix", "productName": "Salix",
"author": "Verdnatura", "author": "Verdnatura",

View File

@ -1,227 +0,0 @@
/* eslint-disable */
/**
* THIS FILE IS GENERATED AUTOMATICALLY.
* 1. DO NOT edit this file directly as it won't do anything.
* 2. EDIT the original quasar.config file INSTEAD.
* 3. DO NOT git commit this file. It should be ignored.
*
* This file is still here because there was an error in
* the original quasar.config file and this allows you to
* investigate the Node.js stack error.
*
* After you fix the original file, this file will be
* deleted automatically.
**/
// quasar.config.js
import { configure } from "quasar/wrappers";
import VueI18nPlugin from "@intlify/unplugin-vue-i18n/vite";
import path from "path";
var __quasar_inject_dirname__ = "/home/jsegarra/Projects/salix-front";
var target = `http://${process.env.CI ? "back" : "localhost"}:3000`;
var quasar_config_default = configure(function() {
return {
eslint: {
// fix: true,
// include = [],
// exclude = [],
// rawOptions = {},
warnings: true,
errors: true
},
// https://v2.quasar.dev/quasar-cli/prefetch-feature
// preFetch: true,
// app boot file (/src/boot)
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: ["i18n", "axios", "vnDate", "validations", "quasar", "quasar.defaults"],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ["app.scss"],
// https://github.com/quasarframework/quasar/tree/dev/extras
extras: [
// 'ionicons-v4',
// 'mdi-v5',
// 'fontawesome-v6',
// 'eva-icons',
// 'themify',
// 'line-awesome',
// 'roboto-font-latin-ext', // this or either 'roboto-font', NEVER both!
"roboto-font",
"material-icons-outlined",
"material-symbols-outlined"
],
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#build
build: {
target: {
browser: ["es2022", "edge88", "firefox78", "chrome87", "safari13.1"],
node: "node20"
},
vueRouterMode: "hash",
// available values: 'hash', 'history'
// vueRouterBase,
// vueDevtools,
// vueOptionsAPI: false,
// rebuildCache: true, // rebuilds Vite/linter/etc cache on startup
// publicPath: '/',
// analyze: true,
// env: {},
rawDefine: {
"process.env.NODE_ENV": JSON.stringify(process.env.NODE_ENV)
},
// ignorePublicFolder: true,
// minify: false,
// polyfillModulePreload: true,
// distDir
extendViteConf(viteConf) {
delete viteConf.build.polyfillModulePreload;
viteConf.build.modulePreload = {
polyfill: false
};
},
// viteVuePluginOptions: {},
alias: {
composables: path.join(__quasar_inject_dirname__, "./src/composables"),
filters: path.join(__quasar_inject_dirname__, "./src/filters")
},
vitePlugins: [
[
VueI18nPlugin({
strictMessage: false,
runtimeOnly: false,
include: [
path.resolve(__quasar_inject_dirname__, "./src/i18n/locale/**"),
path.resolve(__quasar_inject_dirname__, "./src/pages/**/locale/**")
]
})
]
]
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#devServer
devServer: {
server: {
type: "http"
},
proxy: {
"/api": {
target,
logLevel: "debug",
changeOrigin: true,
secure: false
}
},
open: false,
allowedHosts: [
"front",
// Agrega este nombre de host
"localhost"
// Opcional, para pruebas locales
]
},
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#framework
framework: {
config: {
config: {
dark: "auto"
}
},
lang: "en-GB",
// iconSet: 'material-icons', // Quasar icon set
// lang: 'en-US', // Quasar language pack
// For special cases outside of where the auto-import strategy can have an impact
// (like functional components as one of the examples),
// you can manually specify Quasar components/directives to be available everywhere:
//
// components: [],
// directives: [],
// Quasar plugins
plugins: ["Notify", "Dialog"],
all: "auto",
autoImportComponentCase: "pascal"
},
// animations: 'all', // --- includes all animations
// https://v2.quasar.dev/options/animations
animations: [],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#property-sourcefiles
// sourceFiles: {
// rootComponent: 'src/App.vue',
// router: 'src/router/index',
// store: 'src/store/index',
// registerServiceWorker: 'src-pwa/register-service-worker',
// serviceWorker: 'src-pwa/custom-service-worker',
// pwaManifestFile: 'src-pwa/manifest.json',
// electronMain: 'src-electron/electron-main',
// electronPreload: 'src-electron/electron-preload'
// },
// https://v2.quasar.dev/quasar-cli/developing-ssr/configuring-ssr
ssr: {
// ssrPwaHtmlFilename: 'offline.html', // do NOT use index.html as name!
// will mess up SSR
// extendSSRWebserverConf (esbuildConf) {},
// extendPackageJson (json) {},
pwa: false,
// manualStoreHydration: true,
// manualPostHydrationTrigger: true,
prodPort: 3e3,
// The default port that the production server should use
// (gets superseded if process.env.PORT is specified at runtime)
middlewares: [
"render"
// keep this as last one
]
},
// https://v2.quasar.dev/quasar-cli/developing-pwa/configuring-pwa
pwa: {
workboxMode: "generateSW",
// or 'injectManifest'
injectPwaMetaTags: true,
swFilename: "sw.js",
manifestFilename: "manifest.json",
useCredentialsForManifestTag: false
// useFilenameHashes: true,
// extendGenerateSWOptions (cfg) {}
// extendInjectManifestOptions (cfg) {},
// extendManifestJson (json) {}
// extendPWACustomSWConf (esbuildConf) {}
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-cordova-apps/configuring-cordova
cordova: {
// noIosLegacyBuildFlag: true, // uncomment only if you know what you are doing
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-capacitor-apps/configuring-capacitor
capacitor: {
hideSplashscreen: true
},
// Full list of options: https://v2.quasar.dev/quasar-cli/developing-electron-apps/configuring-electron
electron: {
// extendElectronMainConf (esbuildConf)
// extendElectronPreloadConf (esbuildConf)
inspectPort: 5858,
bundler: "packager",
// 'packager' or 'builder'
packager: {
// https://github.com/electron-userland/electron-packager/blob/master/docs/api.md#options
// OS X / Mac App Store
// appBundleId: '',
// appCategoryType: '',
// osxSign: '',
// protocol: 'myapp://path',
// Windows only
// win32metadata: { ... }
},
builder: {
// https://www.electron.build/configuration/configuration
appId: "salix-frontend"
}
},
// Full list of options: https://v2.quasar.dev/quasar-cli-vite/developing-browser-extensions/configuring-bex
bex: {
contentScripts: ["my-content-script"]
// extendBexScriptsConf (esbuildConf) {}
// extendBexManifestJson (json) {}
}
};
});
export {
quasar_config_default as default
};

View File

@ -102,6 +102,10 @@ const $props = defineProps({
type: Boolean, type: Boolean,
default: false, default: false,
}, },
customMethod: {
type: String,
default: null,
},
}); });
const emit = defineEmits(['onFetch', 'onDataSaved', 'submit']); const emit = defineEmits(['onFetch', 'onDataSaved', 'submit']);
const modelValue = computed( const modelValue = computed(
@ -237,7 +241,9 @@ async function save() {
const url = const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url; $props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
const response = await Promise.resolve( const response = await Promise.resolve(
$props.saveFn ? $props.saveFn(body) : axios[method](url, body), $props.saveFn
? $props.saveFn(body)
: axios[$props.customMethod ?? method](url, body),
); );
if ($props.urlCreate) notify('globals.dataCreated', 'positive'); if ($props.urlCreate) notify('globals.dataCreated', 'positive');

View File

@ -182,8 +182,8 @@ const col = computed(() => {
if ($props.default && !newColumn.component) newColumn.component = $props.default; if ($props.default && !newColumn.component) newColumn.component = $props.default;
if (typeof newColumn.component !== 'string') { if (typeof newColumn.component !== 'string') {
newColumn.attrs = { ...newColumn.component.attrs, autofocus: $props.autofocus }; newColumn.attrs = { ...newColumn.component?.attrs, autofocus: $props.autofocus };
newColumn.event = { ...newColumn.component.event, ...$props?.eventHandlers }; newColumn.event = { ...newColumn.component?.event, ...$props?.eventHandlers };
} }
return newColumn; return newColumn;
}); });

View File

@ -0,0 +1,136 @@
<script setup>
import { ref } from 'vue';
import { useClipboard } from 'src/composables/useClipboard';
const { copyText } = useClipboard();
const target = ref();
const qmenuRef = ref();
const colField = ref();
let colValue = '';
let textValue = '';
defineExpose({ handler });
const arrayData = defineModel({
type: Object,
});
function handler(event) {
const clickedElement = event.target.closest('td');
if (!clickedElement) return;
target.value = event.target;
qmenuRef.value.show();
colField.value = clickedElement.getAttribute('data-col-field');
colValue = isNaN(+clickedElement.getAttribute('data-col-value'))
? clickedElement.getAttribute('data-col-value')
: +clickedElement.getAttribute('data-col-value');
textValue = getDeepestText(clickedElement);
}
function getDeepestText(node) {
const walker = document.createTreeWalker(node, NodeFilter.SHOW_TEXT, {
acceptNode: (textNode) => {
return textNode.nodeValue.trim()
? NodeFilter.FILTER_ACCEPT
: NodeFilter.FILTER_REJECT;
},
});
let lastText = '';
while (walker.nextNode()) {
lastText = walker.currentNode.nodeValue.trim();
}
return lastText;
}
async function selectionFilter() {
await arrayData.value.addFilter({ params: { [colField.value]: colValue } });
}
async function selectionExclude() {
await arrayData.value.addFilter({
params: { [colField.value]: { neq: colValue } },
});
}
async function selectionRemoveFilter() {
await arrayData.value.addFilter({ params: { [colField.value]: undefined } });
}
async function removeAllFilters() {
await arrayData.value.applyFilter({ params: {} });
}
function copyValue() {
copyText(textValue, {
component: {
copyValue: textValue,
},
});
}
</script>
<template>
<QMenu
ref="qmenuRef"
:target
class="column q-pa-sm justify-left"
auto-close
no-parent-event
>
<QBtn
flat
icon="filter_list"
@click="selectionFilter()"
class="text-weight-regular"
align="left"
:label="$t('Filter by selection')"
no-caps
/>
<QBtn
flat
icon="dangerous"
@click="selectionExclude()"
class="text-weight-regular"
align="left"
:label="$t('Exclude selection')"
no-caps
/>
<QBtn
flat
icon="filter_list_off"
@click="selectionRemoveFilter()"
class="text-weight-regular"
align="left"
:label="$t('Remove filter')"
no-caps
/>
<QBtn
flat
icon="filter_list_off"
@click="removeAllFilters()"
class="text-weight-regular"
align="left"
:label="$t('Remove all filters')"
no-caps
/>
<QBtn
flat
icon="file_copy"
@click="copyValue()"
class="text-weight-regular"
align="left"
:label="$t('Copy value')"
no-caps
/>
</QMenu>
</template>
<i18n>
es:
Filter by selection: Filtro por selección
Exclude selection: Excluir selección
Remove filter: Quitar filtro por selección
Remove all filters: Eliminar todos los filtros
Copy value: Copiar valor
</i18n>

View File

@ -136,6 +136,9 @@ async function addFilter(value, name) {
value = value === '' ? undefined : value; value = value === '' ? undefined : value;
let field = columnFilter.value?.name ?? $props.column.name ?? name; let field = columnFilter.value?.name ?? $props.column.name ?? name;
delete arrayData.store?.userParams?.[field];
delete arrayData.store?.filter?.where?.[field];
if (columnFilter.value?.inWhere) { if (columnFilter.value?.inWhere) {
if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field; if (columnFilter.value.alias) field = columnFilter.value.alias + '.' + field;
return await arrayData.addFilterWhere({ [field]: value }); return await arrayData.addFilterWhere({ [field]: value });

View File

@ -33,6 +33,7 @@ import VnTableOrder from 'src/components/VnTable/VnOrder.vue';
import VnTableFilter from './VnTableFilter.vue'; import VnTableFilter from './VnTableFilter.vue';
import { getColAlign } from 'src/composables/getColAlign'; import { getColAlign } from 'src/composables/getColAlign';
import RightMenu from '../common/RightMenu.vue'; import RightMenu from '../common/RightMenu.vue';
import VnContextMenu from './VnContextMenu.vue';
import VnScroll from '../common/VnScroll.vue'; import VnScroll from '../common/VnScroll.vue';
import VnCheckboxMenu from '../common/VnCheckboxMenu.vue'; import VnCheckboxMenu from '../common/VnCheckboxMenu.vue';
import VnCheckbox from '../common/VnCheckbox.vue'; import VnCheckbox from '../common/VnCheckbox.vue';
@ -178,8 +179,9 @@ const app = inject('app');
const tableHeight = useTableHeight(); const tableHeight = useTableHeight();
const vnScrollRef = ref(null); const vnScrollRef = ref(null);
const editingRow = ref(null); const editingRow = ref();
const editingField = ref(null); const editingField = ref();
const contextMenuRef = ref({});
const isTableMode = computed(() => mode.value == TABLE_MODE); const isTableMode = computed(() => mode.value == TABLE_MODE);
const selectRegex = /select/; const selectRegex = /select/;
const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']); const emit = defineEmits(['onFetch', 'update:selected', 'saveChanges']);
@ -216,6 +218,10 @@ onBeforeMount(() => {
onMounted(async () => { onMounted(async () => {
if ($props.isEditable) document.addEventListener('click', clickHandler); if ($props.isEditable) document.addEventListener('click', clickHandler);
document.addEventListener('contextmenu', (event) => {
event.preventDefault();
contextMenuRef.value.handler(event);
});
mode.value = mode.value =
quasar.platform.is.mobile && !$props.disableOption?.card quasar.platform.is.mobile && !$props.disableOption?.card
? CARD_MODE ? CARD_MODE
@ -239,6 +245,7 @@ onMounted(async () => {
onUnmounted(async () => { onUnmounted(async () => {
if ($props.isEditable) document.removeEventListener('click', clickHandler); if ($props.isEditable) document.removeEventListener('click', clickHandler);
document.removeEventListener('contextmenu', {});
}); });
watch( watch(
@ -835,6 +842,7 @@ const handleHeaderSelection = (evt, data) => {
]" ]"
:data-row-index="rowIndex" :data-row-index="rowIndex"
:data-col-field="col?.name" :data-col-field="col?.name"
:data-col-value="row?.[col?.name]"
> >
<div <div
class="no-padding no-margin" class="no-padding no-margin"
@ -1145,6 +1153,7 @@ const handleHeaderSelection = (evt, data) => {
</template> </template>
</FormModelPopup> </FormModelPopup>
</QDialog> </QDialog>
<VnContextMenu ref="contextMenuRef" v-model="arrayData" />
<VnScroll <VnScroll
ref="vnScrollRef" ref="vnScrollRef"
v-if="isTableMode" v-if="isTableMode"

View File

@ -77,7 +77,12 @@ function columnName(col) {
<template #tags="{ tag, formatFn, getLocale }"> <template #tags="{ tag, formatFn, getLocale }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
<strong>{{ getLocale(`${tag.label}`) }}: </strong> <strong>{{ getLocale(`${tag.label}`) }}: </strong>
<span>{{ formatFn(tag.value) }}</span> <span
:class="{
'text-decoration-line-through': typeof chip === 'object',
}"
>{{ formatFn(tag) }}</span
>
</div> </div>
</template> </template>
<template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName"> <template v-for="(_, slotName) in $slots" #[slotName]="slotData" :key="slotName">

View File

@ -1,7 +1,7 @@
<script setup> <script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue'; import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router'; import { useRoute } from 'vue-router';
import axios from 'axios'; import axios from 'axios';
import { date } from 'quasar'; import { date } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
@ -21,7 +21,6 @@ const stateStore = useStateStore();
const validationsStore = useValidator(); const validationsStore = useValidator();
const { models } = validationsStore; const { models } = validationsStore;
const route = useRoute(); const route = useRoute();
const router = useRouter();
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
model: { model: {
@ -273,7 +272,7 @@ onUnmounted(() => {
:data-key :data-key
:url="dataKey + 's'" :url="dataKey + 's'"
:user-filter="filter" :user-filter="filter"
:filter="{ where: { and: [{ originFk: route.params.id }] } }" :user-params="{ originFk: route.params.id }"
:skeleton="false" :skeleton="false"
auto-load auto-load
@on-fetch="setLogTree" @on-fetch="setLogTree"

View File

@ -124,6 +124,7 @@ const {
} = toRefs($props); } = toRefs($props);
const myOptions = ref([]); const myOptions = ref([]);
const myOptionsOriginal = ref([]); const myOptionsOriginal = ref([]);
const myOptionsMap = ref(new Map());
const vnSelectRef = ref(); const vnSelectRef = ref();
const lastVal = ref(); const lastVal = ref();
const noOneText = t('globals.noOne'); const noOneText = t('globals.noOne');
@ -140,7 +141,7 @@ const styleAttrs = computed(() => {
} }
: {}; : {};
}); });
const isLoading = ref(false); const hasFocus = ref(false);
const useURL = computed(() => $props.url); const useURL = computed(() => $props.url);
const value = computed({ const value = computed({
get() { get() {
@ -166,6 +167,10 @@ const computedSortBy = computed(() => {
return $props.sortBy || $props.optionLabel + ' ASC'; return $props.sortBy || $props.optionLabel + ' ASC';
}); });
const valueIsObject = computed(
() => modelValue.value && typeof modelValue.value == 'object',
);
const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val); const getVal = (val) => ($props.useLike ? { like: `%${val}%` } : val);
watch(options, (newValue) => { watch(options, (newValue) => {
@ -173,12 +178,22 @@ watch(options, (newValue) => {
}); });
watch(modelValue, async (newValue) => { watch(modelValue, async (newValue) => {
if (newValue?.neq) newValue = newValue.neq;
if (!myOptions?.value?.some((option) => option[optionValue.value] == newValue)) if (!myOptions?.value?.some((option) => option[optionValue.value] == newValue))
await fetchFilter(newValue); await fetchFilter(newValue);
if ($props.noOne) myOptions.value.unshift(noOneOpt.value); if ($props.noOne) myOptions.value.unshift(noOneOpt.value);
}); });
watch(
() => myOptionsOriginal.value,
(newValue) => {
for (const item of newValue) {
myOptionsMap.value.set(item[optionValue.value], item);
}
},
);
onMounted(() => { onMounted(() => {
setOptions(options.value); setOptions(options.value);
if (useURL.value && $props.modelValue && !findKeyInOptions()) if (useURL.value && $props.modelValue && !findKeyInOptions())
@ -187,7 +202,7 @@ onMounted(() => {
}); });
const someIsLoading = computed( const someIsLoading = computed(
() => (isLoading.value || !!arrayData?.isLoading?.value) && !isMenuOpened.value, () => !!arrayData?.isLoading?.value && !isMenuOpened.value,
); );
function findKeyInOptions() { function findKeyInOptions() {
if (!$props.options) return; if (!$props.options) return;
@ -224,6 +239,9 @@ function filter(val, options) {
async function fetchFilter(val) { async function fetchFilter(val) {
if (!$props.url) return; if (!$props.url) return;
if (val && typeof val == 'object') {
val = val.neq;
}
const { fields, include, limit } = $props; const { fields, include, limit } = $props;
const sortBy = computedSortBy.value; const sortBy = computedSortBy.value;
@ -298,13 +316,11 @@ async function onScroll({ to, direction, from, index }) {
if (from === 0 && index === 0) return; if (from === 0 && index === 0) return;
if (!useURL.value && !$props.fetchRef) return; if (!useURL.value && !$props.fetchRef) return;
if (direction === 'decrease') return; if (direction === 'decrease') return;
if (to === lastIndex && arrayData.store.hasMoreData && !isLoading.value) { if (to === lastIndex && arrayData.store.hasMoreData) {
isLoading.value = true;
await arrayData.loadMore(); await arrayData.loadMore();
setOptions(arrayData.store.data); setOptions(arrayData.store.data);
vnSelectRef.value.scrollTo(lastIndex); vnSelectRef.value.scrollTo(lastIndex);
await nextTick(); await nextTick();
isLoading.value = false;
} }
} }
@ -347,22 +363,30 @@ function getCaption(opt) {
if (optionCaption.value === false) return; if (optionCaption.value === false) return;
return opt[optionCaption.value] || opt[optionValue.value]; return opt[optionCaption.value] || opt[optionValue.value];
} }
function getOptionLabel(property) {
if (!myOptionsMap.value.size) return;
let value = modelValue.value;
if (property) {
value = modelValue.value[property];
}
return myOptionsMap.value.get(value)?.[optionLabel.value];
}
</script> </script>
<template> <template>
<QSelect <QSelect
ref="vnSelectRef"
v-model="value" v-model="value"
:options="myOptions" :options="myOptions"
:option-label="optionLabel" :option-label="optionLabel"
:option-value="optionValue" :option-value="optionValue"
v-bind="{ ...$attrs, ...styleAttrs }" v-bind="{ ...$attrs, ...styleAttrs, hideSelected: hasFocus }"
@filter="filterHandler" @filter="filterHandler"
:emit-value="nullishToTrue($attrs['emit-value'])" :emit-value="nullishToTrue($attrs['emit-value'])"
:map-options="nullishToTrue($attrs['map-options'])" :map-options="nullishToTrue($attrs['map-options'])"
:use-input="nullishToTrue($attrs['use-input'])" :use-input="hasFocus || !value"
:hide-selected="nullishToTrue($attrs['hide-selected'])" :fill-input="false"
:fill-input="nullishToTrue($attrs['fill-input'])"
ref="vnSelectRef"
lazy-rules lazy-rules
:class="{ required: isRequired }" :class="{ required: isRequired }"
:rules="mixinRules" :rules="mixinRules"
@ -372,10 +396,20 @@ function getCaption(opt) {
:loading="someIsLoading" :loading="someIsLoading"
@virtual-scroll="onScroll" @virtual-scroll="onScroll"
@popup-hide="isMenuOpened = false" @popup-hide="isMenuOpened = false"
@popup-show="isMenuOpened = true" @popup-show="
async () => {
isMenuOpened = true;
hasFocus = true;
await $nextTick();
vnSelectRef?.$el?.querySelector('input')?.focus();
}
"
@keydown="handleKeyDown" @keydown="handleKeyDown"
:data-cy="$attrs.dataCy ?? $attrs.label + '_select'" :data-cy="$attrs.dataCy ?? $attrs.label + '_select'"
:data-url="url" :data-url="url"
@blur="hasFocus = false"
@update:model-value="() => vnSelectRef.blur()"
:is-required="false"
> >
<template #append> <template #append>
<QIcon <QIcon
@ -425,6 +459,17 @@ function getCaption(opt) {
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
<template #selected v-if="valueIsObject && nullishToTrue($attrs['emit-value'])">
<span class="nowrap">
<span
class="text-strike"
v-if="modelValue?.neq"
v-text="getOptionLabel('neq')"
:title="getOptionLabel('neq')"
/>
<span v-else>{{ JSON.stringify(modelValue) }}</span>
</span>
</template>
</QSelect> </QSelect>
</template> </template>
@ -432,4 +477,12 @@ function getCaption(opt) {
.q-field--outlined { .q-field--outlined {
max-width: 100%; max-width: 100%;
} }
.q-field__native {
@extend .nowrap;
}
.nowrap {
display: flex;
flex-wrap: nowrap !important;
}
</style> </style>

View File

@ -42,7 +42,7 @@ const card = toRef(props, 'item');
</div> </div>
<div class="content"> <div class="content">
<span class="link" @click.stop> <span class="link" @click.stop>
{{ card.name }} {{ card.longName }}
<ItemDescriptorProxy :id="card.id" /> <ItemDescriptorProxy :id="card.id" />
</span> </span>
<p class="subName">{{ card.subName }}</p> <p class="subName">{{ card.subName }}</p>
@ -57,11 +57,12 @@ const card = toRef(props, 'item');
<QIcon name="production_quantity_limits" size="xs" /> <QIcon name="production_quantity_limits" size="xs" />
{{ card.minQuantity }} {{ card.minQuantity }}
</div> </div>
<div class="footer"> <div class="footer q-mt-auto">
<div class="price"> <div class="price">
<p v-if="isCatalog"> <p v-if="isCatalog">
{{ card.available }} {{ t('to') }} <span class="text-primary">{{ card.available }}</span>
{{ toCurrency(card.price) }} {{ t('to') }}
<span class="text-bold" >{{ toCurrency(card.price) }}</span>
</p> </p>
<slot name="price" /> <slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon"> <QIcon v-if="isCatalog" name="add_circle" class="icon">
@ -144,6 +145,7 @@ const card = toRef(props, 'item');
} }
.footer { .footer {
.price { .price {
overflow: hidden; overflow: hidden;
white-space: nowrap; white-space: nowrap;

View File

@ -186,6 +186,7 @@ async function remove(key) {
function formatValue(value) { function formatValue(value) {
if (typeof value === 'boolean') return value ? t('Yes') : t('No'); if (typeof value === 'boolean') return value ? t('Yes') : t('No');
if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value); if (isNaN(value) && !isNaN(Date.parse(value))) return toDate(value);
if (value && typeof value === 'object') return '';
return `"${value}"`; return `"${value}"`;
} }

View File

@ -313,6 +313,7 @@ export function useArrayData(key, userOptions) {
const { params, limit } = getCurrentFilter(); const { params, limit } = getCurrentFilter();
store.currentFilter = JSON.parse(JSON.stringify(params)); store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include; delete store.currentFilter.filter.include;
delete store.currentFilter.filter.fields;
store.currentFilter.filter = JSON.stringify(store.currentFilter.filter); store.currentFilter.filter = JSON.stringify(store.currentFilter.filter);
return { params, limit }; return { params, limit };
} }

View File

@ -896,6 +896,7 @@ components:
rate3: Packing price rate3: Packing price
minPrice: Min. Price minPrice: Min. Price
itemFk: Item id itemFk: Item id
dated: Date
userPanel: userPanel:
copyToken: Token copied to clipboard copyToken: Token copied to clipboard
settings: Settings settings: Settings

View File

@ -980,6 +980,7 @@ components:
rate3: Precio packing rate3: Precio packing
minPrice: Precio mínimo minPrice: Precio mínimo
itemFk: Id item itemFk: Id item
dated: Fecha
userPanel: userPanel:
copyToken: Token copiado al portapapeles copyToken: Token copiado al portapapeles
settings: Configuración settings: Configuración

View File

@ -2,14 +2,20 @@
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useQuasar } from 'quasar';
import { toCurrency, toDateHourMin } from 'src/filters'; import { toCurrency, toDateHourMin } from 'src/filters';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const quasar = useQuasar();
const tableRef = ref(); const tableRef = ref();
@ -45,26 +51,45 @@ const columns = computed(() => [
align: 'right', align: 'right',
field: 'rating', field: 'rating',
label: t('customer.summary.rating'), label: t('customer.summary.rating'),
name: 'rating', name: 'rating'
create: true,
columnCreate: {
component: 'number',
autofocus: true,
},
}, },
{ {
align: 'right', align: 'right',
field: 'recommendedCredit', field: 'recommendedCredit',
format: ({ recommendedCredit }) => toCurrency(recommendedCredit), format: ({ recommendedCredit }) => toCurrency(recommendedCredit),
label: t('customer.summary.recommendCredit'), label: t('customer.summary.recommendCredit'),
name: 'recommendedCredit', name: 'recommendedCredit'
create: true,
columnCreate: {
component: 'number',
autofocus: true,
},
}, },
]); ]);
const defaultInitialData = {
rating: null,
recommendedCredit: null
};
const createRating = async (data) => {
await axios.post(`Clients/${route.params.id}/setRating`, data);
tableRef.value?.reload();
};
const handleSave = async (data) => {
if (data.rating || data.recommendedCredit) {
await createRating(data);
return;
}
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('terminationTitle'),
message: t('terminationMessage'),
},
})
.onOk(async () => {
await createRating({ rating: 0, recommendedCredit: 0 });
});
};
</script> </script>
<template> <template>
@ -72,8 +97,7 @@ const columns = computed(() => [
ref="tableRef" ref="tableRef"
data-key="ClientInformas" data-key="ClientInformas"
url="ClientInformas" url="ClientInformas"
:filter="filter" :user-filter="filter"
:order="['created DESC']"
:columns="columns" :columns="columns"
:right-search="false" :right-search="false"
:is-editable="false" :is-editable="false"
@ -82,22 +106,43 @@ const columns = computed(() => [
:disable-option="{ card: true }" :disable-option="{ card: true }"
auto-load auto-load
:create="{ :create="{
urlCreate: `Clients/${route.params.id}/setRating`,
title: 'Create rating', title: 'Create rating',
onDataSaved: () => tableRef.reload(), onDataSaved: ()=> tableRef.reload(),
formInitialData: {}, formInitialData: defaultInitialData,
saveFn: handleSave
}" }"
> >
<template #column-employee="{ row }"> <template #column-employee="{ row }">
<span class="link">{{ row.worker.user.nickname }}</span> <span class="link">{{ row.worker.user.nickname }}</span>
<WorkerDescriptorProxy :id="row.worker.id" /> <WorkerDescriptorProxy :id="row.worker.id" />
</template> </template>
<template #more-create-dialog="{ data }">
<VnRow>
<VnInputNumber
v-model="data.rating"
:label="t('customer.summary.rating')"
:required="!!data.recommendedCredit"
/>
<VnInputNumber
v-model="data.recommendedCredit"
:label="t('customer.summary.recommendCredit')"
:required="!!data.rating"
/>
</VnRow>
</template>
</VnTable> </VnTable>
</template> </template>
<i18n> <i18n>
en:
terminationTitle: Confirm contract termination
terminationMessage: Are you sure you want to terminate the contract? This action will set the rating and recommended credit to 0.
es: es:
Recommended credit: Crédito recomendado Recommended credit: Crédito recomendado
Since: Desde Since: Desde
Employee: Empleado Employee: Empleado
Create rating: Crear calificación
terminationTitle: Confirmar baja de contrato
terminationMessage: ¿Está seguro que desea dar de baja el contrato? Esta acción establecerá la calificación y el crédito recomendado en 0.
</i18n> </i18n>

View File

@ -80,7 +80,6 @@ function isRequired({ isTaxDataChecked: taxDataChecked }) {
@on-data-saved="checkEtChanges" @on-data-saved="checkEtChanges"
> >
<template #form="{ data, validate, validations }"> <template #form="{ data, validate, validations }">
{{ isTaxDataChecked }} {{ data.isTaxDataChecked }}
<VnRow> <VnRow>
<VnInput <VnInput
:label="t('Social name')" :label="t('Social name')"

View File

@ -1,16 +1,21 @@
<script setup> <script setup>
import { ref, computed, markRaw } from 'vue'; import { ref, computed, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { getTotal } from 'src/composables/getTotal'; import { getTotal } from 'src/composables/getTotal';
import { toCurrency } from 'src/filters'; import { toCurrency } from 'src/filters';
import FetchData from 'src/components/FetchData.vue'; import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CreateNewExpenseForm from 'src/components/CreateNewExpenseForm.vue';
import { getExchange } from 'src/composables/getExchange'; import { getExchange } from 'src/composables/getExchange';
import VnTable from 'src/components/VnTable/VnTable.vue'; import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
import VnSelectExpense from 'src/components/common/VnSelectExpense.vue';
const { t } = useI18n(); const { t } = useI18n();
const arrayData = useArrayData(); const arrayData = useArrayData();
const route = useRoute(); const route = useRoute();
const invoiceIn = computed(() => arrayData.store.data); const invoiceIn = computed(() => arrayData.store.data);
@ -19,142 +24,100 @@ const expenses = ref([]);
const sageTaxTypes = ref([]); const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]); const sageTransactionTypes = ref([]);
const rowsSelected = ref([]); const rowsSelected = ref([]);
const invoiceInVatTableRef = ref(); const invoiceInFormRef = ref();
defineProps({ actionIcon: { type: String, default: 'add' } }); defineProps({
actionIcon: {
function taxRate(invoiceInTax) { type: String,
const sageTaxTypeId = invoiceInTax.taxTypeSageFk; default: 'add',
const taxRateSelection = sageTaxTypes.value.find( },
(transaction) => transaction.id == sageTaxTypeId, });
);
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return (taxTypeSage / 100) * taxableBase;
}
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'expenseFk', name: 'expense',
label: t('Expense'), label: t('Expense'),
component: markRaw(VnSelectExpense), field: (row) => row.expenseFk,
format: (row) => { options: expenses.value,
const expense = expenses.value.find((e) => e.id === row.expenseFk); model: 'expenseFk',
return expense ? `${expense.id}: ${expense.name}` : row.expenseFk; optionValue: 'id',
}, optionLabel: (row) => `${row.id}: ${row.name}`,
sortable: true, sortable: true,
align: 'left', align: 'left',
isEditable: true,
create: true,
width: '250px',
}, },
{ {
name: 'taxableBase', name: 'taxablebase',
label: t('Taxable base'), label: t('Taxable base'),
component: 'number', field: (row) => row.taxableBase,
attrs: { model: 'taxableBase',
clearable: true,
'clear-icon': 'close',
},
sortable: true, sortable: true,
align: 'left', align: 'left',
isEditable: true,
create: true,
}, },
{ {
name: 'isDeductible', name: 'isDeductible',
label: t('invoiceIn.isDeductible'), label: t('invoiceIn.isDeductible'),
component: 'checkbox', field: (row) => row.isDeductible,
model: 'isDeductible',
align: 'center', align: 'center',
isEditable: true,
create: true,
createAttrs: {
defaultValue: true,
},
width: '100px',
}, },
{ {
name: 'taxTypeSageFk', name: 'sageiva',
label: t('Sage iva'), label: t('Sage iva'),
component: 'select', field: (row) => row.taxTypeSageFk,
attrs: {
options: sageTaxTypes.value, options: sageTaxTypes.value,
model: 'taxTypeSageFk',
optionValue: 'id', optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`, optionLabel: (row) => `${row.id}: ${row.vat}`,
filterOptions: ['id', 'vat'],
'data-cy': 'vat-sageiva',
},
format: (row) => {
const taxType = sageTaxTypes.value.find((t) => t.id === row.taxTypeSageFk);
return taxType ? `${taxType.id}: ${taxType.vat}` : row.taxTypeSageFk;
},
sortable: true, sortable: true,
align: 'left', align: 'left',
isEditable: true,
create: true,
}, },
{ {
name: 'transactionTypeSageFk', name: 'sagetransaction',
label: t('Sage transaction'), label: t('Sage transaction'),
component: 'select', field: (row) => row.transactionTypeSageFk,
attrs: {
options: sageTransactionTypes.value, options: sageTransactionTypes.value,
model: 'transactionTypeSageFk',
optionValue: 'id', optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`, optionLabel: (row) => `${row.id}: ${row.transaction}`,
filterOptions: ['id', 'transaction'],
},
format: (row) => {
const transType = sageTransactionTypes.value.find(
(t) => t.id === row.transactionTypeSageFk,
);
return transType
? `${transType.id}: ${transType.transaction}`
: row.transactionTypeSageFk;
},
sortable: true, sortable: true,
align: 'left', align: 'left',
isEditable: true,
create: true,
}, },
{ {
name: 'rate', name: 'rate',
label: t('Rate'), label: t('Rate'),
sortable: false, sortable: true,
format: (row) => taxRate(row).toFixed(2), field: (row) => taxRate(row, row.taxTypeSageFk),
align: 'left', align: 'left',
}, },
{ {
name: 'foreignValue', name: 'foreignvalue',
label: t('Foreign value'), label: t('Foreign value'),
component: 'number',
sortable: true, sortable: true,
field: (row) => row.foreignValue,
align: 'left', align: 'left',
create: true,
disable: !isNotEuro(currency.value),
}, },
{ {
name: 'total', name: 'total',
label: t('Total'), label: 'Total',
align: 'left', align: 'left',
format: (row) => (Number(row.taxableBase || 0) + Number(taxRate(row))).toFixed(2),
}, },
]); ]);
const tableRows = computed(
() => invoiceInVatTableRef.value?.CrudModelRef?.formData || [],
);
const taxableBaseTotal = computed(() => { const taxableBaseTotal = computed(() => {
return getTotal(tableRows.value, 'taxableBase'); return getTotal(invoiceInFormRef.value.formData, 'taxableBase');
}); });
const taxRateTotal = computed(() => { const taxRateTotal = computed(() => {
return tableRows.value.reduce((sum, row) => sum + Number(taxRate(row)), 0); return getTotal(invoiceInFormRef.value.formData, null, {
cb: taxRate,
});
}); });
const combinedTotal = computed(() => { const combinedTotal = computed(() => {
return +taxableBaseTotal.value + +taxRateTotal.value; return +taxableBaseTotal.value + +taxRateTotal.value;
}); });
const filter = computed(() => ({ const filter = {
fields: [ fields: [
'id', 'id',
'invoiceInFk', 'invoiceInFk',
@ -168,75 +131,389 @@ const filter = computed(() => ({
where: { where: {
invoiceInFk: route.params.id, invoiceInFk: route.params.id,
}, },
})); };
const isNotEuro = (code) => code != 'EUR'; const isNotEuro = (code) => code != 'EUR';
async function handleForeignValueUpdate(val, row) { function taxRate(invoiceInTax) {
if (!isNotEuro(currency.value)) return; const sageTaxTypeId = invoiceInTax.taxTypeSageFk;
row.taxableBase = await getExchange( const taxRateSelection = sageTaxTypes.value.find(
val, (transaction) => transaction.id == sageTaxTypeId,
invoiceIn.value?.currencyFk,
invoiceIn.value?.issued,
); );
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return ((taxTypeSage / 100) * taxableBase).toFixed(2);
}
function autocompleteExpense(evt, row, col, ref) {
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),
);
ref.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
}
function setCursor(ref) {
nextTick(() => {
const select = ref.vnSelectDialogRef
? ref.vnSelectDialogRef.vnSelectRef
: ref.vnSelectRef;
select.$el.querySelector('input').setSelectionRange(0, 0);
});
} }
</script> </script>
<template> <template>
<FetchData url="Expenses" auto-load @on-fetch="(data) => (expenses = data)" /> <FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
<FetchData url="SageTaxTypes" auto-load @on-fetch="(data) => (sageTaxTypes = data)" /> <FetchData url="SageTaxTypes" auto-load @on-fetch="(data) => (sageTaxTypes = data)" />
<FetchData <FetchData
url="sageTransactionTypes" url="sageTransactionTypes"
auto-load auto-load
@on-fetch="(data) => (sageTransactionTypes = data)" @on-fetch="(data) => (sageTransactionTypes = data)"
/> />
<VnTable <CrudModel
ref="invoiceInFormRef"
v-if="invoiceIn" v-if="invoiceIn"
ref="invoiceInVatTableRef"
data-key="InvoiceInTaxes" data-key="InvoiceInTaxes"
url="InvoiceInTaxes" url="InvoiceInTaxes"
save-url="InvoiceInTaxes/crud"
:filter="filter" :filter="filter"
:data-required="{ invoiceInFk: $route.params.id }" :data-required="{ invoiceInFk: $route.params.id }"
:insert-on-load="true" :insert-on-load="true"
auto-load auto-load
v-model:selected="rowsSelected" v-model:selected="rowsSelected"
:columns="columns" :go-to="`/invoice-in/${$route.params.id}/due-day`"
:is-editable="true"
:table="{ selection: 'multiple', 'row-key': '$index' }"
footer
:right-search="false"
:column-search="false"
:disable-option="{ card: true }"
class="q-pa-none"
:create="{
urlCreate: 'InvoiceInTaxes',
title: t('Add tax'),
formInitialData: { invoiceInFk: $route.params.id, isDeductible: true },
onDataSaved: () => invoiceInVatTableRef.reload(),
}"
:crud-model="{ goTo: `/invoice-in/${$route.params.id}/due-day` }"
> >
<template #column-footer-taxableBase> <template #body="{ rows }">
<QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
:grid="$q.screen.lt.sm"
>
<template #body-cell-expense="{ row, col }">
<QTd>
<VnSelectDialog
:ref="`expenseRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
:acls="[
{ model: 'Expense', props: '*', accessType: 'WRITE' },
]"
@keydown.tab.prevent="
autocompleteExpense(
$event,
row,
col,
$refs[`expenseRef-${row.$index}`],
)
"
@update:model-value="
setCursor($refs[`expenseRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #form>
<CreateNewExpenseForm
@on-data-saved="$refs.expensesRef.fetch()"
/>
</template>
</VnSelectDialog>
</QTd>
</template>
<template #body-cell-isDeductible="{ row }">
<QTd align="center">
<QCheckbox
v-model="row.isDeductible"
data-cy="isDeductible_checkbox"
/>
</QTd>
</template>
<template #body-cell-taxablebase="{ row }">
<QTd shrink>
<VnInputNumber
clear-icon="close"
v-model="row.taxableBase"
clearable
/>
</QTd>
</template>
<template #body-cell-sageiva="{ row, col }">
<QTd>
<VnSelect
:ref="`sageivaRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'vat']"
data-cy="vat-sageiva"
@update:model-value="
setCursor($refs[`sageivaRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.vat }}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-sagetransaction="{ row, col }">
<QTd>
<VnSelect
:ref="`sagetransactionRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'transaction']"
@update:model-value="
setCursor($refs[`sagetransactionRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.transaction
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-foreignvalue="{ row }">
<QTd shrink>
<VnInputNumber
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
: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,
);
}
"
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
<QTd />
<QTd>
{{ toCurrency(taxableBaseTotal) }} {{ toCurrency(taxableBaseTotal) }}
</template> </QTd>
<template #column-footer-rate> <QTd />
<QTd />
<QTd />
<QTd>
{{ toCurrency(taxRateTotal) }} {{ toCurrency(taxRateTotal) }}
</template> </QTd>
<template #column-footer-total> <QTd />
<QTd>
{{ toCurrency(combinedTotal) }} {{ toCurrency(combinedTotal) }}
</QTd>
</QTr>
</template> </template>
</VnTable>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat class="q-my-xs">
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList>
<QItem>
<VnSelectDialog
:label="t('Expense')"
class="full-width"
v-model="props.row['expenseFk']"
:options="expenses"
option-value="id"
:option-label="(row) => `${row.id}:${row.name}`"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #form>
<CreateNewExpenseForm />
</template>
</VnSelectDialog>
</QItem>
<QItem>
<VnInputNumber
:label="t('Taxable base')"
:class="{
'no-pointer-events': isNotEuro(currency),
}"
class="full-width"
:disable="isNotEuro(currency)"
clear-icon="close"
v-model="props.row.taxableBase"
clearable
/>
</QItem>
<QItem>
<VnSelect
:label="t('Sage iva')"
class="full-width"
v-model="props.row['taxTypeSageFk']"
:options="sageTaxTypes"
option-value="id"
:option-label="(row) => `${row.id}:${row.vat}`"
:filter-options="['id', 'vat']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.vat
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItem>
<QItem>
<VnSelect
class="full-width"
v-model="props.row['transactionTypeSageFk']"
:options="sageTransactionTypes"
option-value="id"
:option-label="
(row) => `${row.id}:${row.transaction}`
"
:filter-options="['id', 'transaction']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.transaction
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItem>
<QItem>
{{ toCurrency(taxRate(props.row), currency) }}
</QItem>
<QItem>
<VnInputNumber
:label="t('Foreign value')"
class="full-width"
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
v-model="props.row.foreignValue"
/>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
v-shortcut="'+'"
round
@click="invoiceInFormRef.insert()"
>
<QTooltip>{{ t('Add tax') }}</QTooltip>
</QBtn>
</QPageSticky>
</template> </template>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
@media (max-width: $breakpoint-xs) {
.q-dialog {
.q-card {
&__section:not(:first-child) {
.q-item {
flex-direction: column;
.q-checkbox {
margin-top: 2rem;
}
}
}
}
}
}
.q-item {
min-height: 0;
}
.default-icon {
cursor: pointer;
border-radius: 50px;
background-color: $primary;
}
</style>
<i18n> <i18n>
es: es:
Expense: Gasto Expense: Gasto
Create a new expense: Crear nuevo gasto Create a new expense: Crear nuevo gasto
Add tax: Añadir Gasto/IVA # Changed label slightly Add tax: Crear gasto
Taxable base: Base imp. Taxable base: Base imp.
Sage iva: Sage iva # Kept original label Sage tax: Sage iva
Sage transaction: Sage transacción Sage transaction: Sage transacción
Rate: Cuota # Changed label Rate: Tasa
Foreign value: Divisa Foreign value: Divisa
Total: Total
invoiceIn.isDeductible: Deducible
</i18n> </i18n>

View File

@ -1,9 +1,11 @@
<script setup> <script setup>
import { onMounted, ref, onUnmounted, computed, watch } from 'vue'; import { onMounted, ref, onUnmounted, computed, watch } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { beforeSave } from 'src/composables/updateMinPriceBeforeSave'; import { beforeSave } from 'src/composables/updateMinPriceBeforeSave';
import FetchedTags from 'components/ui/FetchedTags.vue'; import FetchedTags from 'components/ui/FetchedTags.vue';
@ -14,13 +16,13 @@ import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import VnColor from 'src/components/common/VnColor.vue'; import VnColor from 'src/components/common/VnColor.vue';
import { toDate } from 'src/filters'; import { toDate, toCurrency } from 'src/filters';
import { isLower, isBigger } from 'src/filters/date.js'; import { isLower, isBigger } from 'src/filters/date.js';
import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue'; import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue'; import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { toCurrency } from 'src/filters';
const stateStore = useStateStore(); const stateStore = useStateStore();
const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
const editFixedPriceForm = ref(null); const editFixedPriceForm = ref(null);
@ -218,11 +220,36 @@ const dateStyle = (date) =>
} }
: { color: dateColor, 'background-color': 'transparent' }; : { color: dateColor, 'background-color': 'transparent' };
const onDataSaved = () => { const onDataSaved = async (data) => {
tableRef.value.CrudModelRef.saveChanges(); for (const row of data) {
await axios.patch('FixedPrices/upsertFixedPrice', row);
}
selectedRows.value = []; selectedRows.value = [];
tableRef.value.reload();
tableRef.value.CrudModelRef.reset();
}; };
async function saveData(data, getChanges) {
const changes = getChanges();
if (changes?.updates?.length) {
for (const change of changes.updates) {
const row = data.find((row) => row.id === change.where.id);
await axios.patch('FixedPrices/upsertFixedPrice', row);
}
}
if (data?.deletes?.length) {
for (const deleteItem of data.deletes) {
await axios.delete(`FixedPrices/${deleteItem}`);
quasar.notify({
message: t('globals.dataDeleted'),
color: 'positive',
});
}
}
tableRef.value.reload();
}
onMounted(() => { onMounted(() => {
if (tableRef.value) { if (tableRef.value) {
tableRef.value.showForm = false; tableRef.value.showForm = false;
@ -273,7 +300,8 @@ watch(
data-key="ItemFixedPrices" data-key="ItemFixedPrices"
url="FixedPrices/filter" url="FixedPrices/filter"
:order="'name DESC'" :order="'name DESC'"
save-url="FixedPrices/crud" save-url="FixedPrices/upsertFixedPrice"
:saveFn="saveData"
:columns="columns" :columns="columns"
:is-editable="true" :is-editable="true"
:right-search="false" :right-search="false"
@ -283,7 +311,8 @@ watch(
}" }"
v-model:selected="selectedRows" v-model:selected="selectedRows"
:create="{ :create="{
urlCreate: 'FixedPrices', urlCreate: 'FixedPrices/upsertFixedPrice',
customMethod: 'patch',
title: t('Create fixed price'), title: t('Create fixed price'),
formInitialData: { warehouseFk: warehouse }, formInitialData: { warehouseFk: warehouse },
onDataSaved: () => tableRef.reload(), onDataSaved: () => tableRef.reload(),

View File

@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n';
import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue'; import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue';
const { t } = useI18n(); const { t } = useI18n();
@ -51,42 +52,41 @@ const props = defineProps({
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QSeparator />
<QItemSection>
<QIcon name="info" size="sm" class="info-icon cursor-pointer">
<QTooltip>{{ t('params.incompatibleFilters') }}</QTooltip>
</QIcon>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.started" v-model="params.dated"
:label="t('params.started')" :label="t('params.date')"
filled filled
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection>
<VnInputDate
v-model="params.ended"
:label="t('params.ended')"
filled
@update:model-value="searchFn()"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <VnCheckbox
v-model="params.showBadDates"
:label="t(`params.showBadDates`)"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
</QItemSection>
<QSeparator />
<QItem>
<QItemSection>
<VnCheckbox
:label="t('params.mine')" :label="t('params.mine')"
v-model="params.mine" v-model="params.mine"
toggle-indeterminate toggle-indeterminate
@update:model-value="searchFn()" @update:model-value="searchFn()"
/> />
<VnCheckbox
<QCheckbox
v-model="params.showBadDates"
:label="t(`params.showBadDates`)"
toggle-indeterminate
@update:model-value="searchFn()"
>
</QCheckbox>
<QCheckbox
:label="t('params.hasMinPrice')" :label="t('params.hasMinPrice')"
v-model="params.hasMinPrice" v-model="params.hasMinPrice"
toggle-indeterminate toggle-indeterminate
@ -97,6 +97,13 @@ const props = defineProps({
</template> </template>
</ItemsFilterPanel> </ItemsFilterPanel>
</template> </template>
<style lang="scss" scoped>
.info-icon {
position: relative;
top: 0;
left: 90%;
}
</style>
<i18n> <i18n>
en: en:
params: params:
@ -107,6 +114,8 @@ en:
mine: Mine mine: Mine
showBadDates: Show future items showBadDates: Show future items
hasMinPrice: Has Min Price hasMinPrice: Has Min Price
date: Date
incompatibleFilters: Cannot select "Date" and "Show future items" at the same time
es: es:
params: params:
buyerFk: Comprador buyerFk: Comprador
@ -116,4 +125,6 @@ es:
mine: Para mi mine: Para mi
showBadDates: Ver items a futuro showBadDates: Ver items a futuro
hasMinPrice: Precio mínimo hasMinPrice: Precio mínimo
date: Fecha
incompatibleFilters: No se puede seleccionar "Fecha" y "Ver items a futuro" a la vez
</i18n> </i18n>

View File

@ -26,7 +26,6 @@ const $props = defineProps({
default: () => {}, default: () => {},
}, },
}); });
const { t } = useI18n(); const { t } = useI18n();
const emit = defineEmits(['onDataSaved']); const emit = defineEmits(['onDataSaved']);
@ -38,7 +37,7 @@ const inputs = {
select: markRaw(VnSelect), select: markRaw(VnSelect),
}; };
const newValue = ref(null); const newFieldValue = ref(null);
const selectedField = ref(null); const selectedField = ref(null);
const closeButton = ref(null); const closeButton = ref(null);
const isLoading = ref(false); const isLoading = ref(false);
@ -46,7 +45,11 @@ const isLoading = ref(false);
const onSubmit = async () => { const onSubmit = async () => {
isLoading.value = true; isLoading.value = true;
$props.rows.forEach((row) => { $props.rows.forEach((row) => {
row[selectedField.value.name] = newValue.value; const newValue =
selectedField.value.component === 'number'
? parseInt(newFieldValue.value)
: newFieldValue.value;
row[selectedField.value.name] = newValue;
}); });
emit('onDataSaved', $props.rows); emit('onDataSaved', $props.rows);
closeForm(); closeForm();
@ -71,17 +74,26 @@ const closeForm = () => {
class="editOption" class="editOption"
:label="t('Field to edit')" :label="t('Field to edit')"
:options="fieldsOptions" :options="fieldsOptions"
hide-selected
option-label="label" option-label="label"
option-value="name"
v-model="selectedField" v-model="selectedField"
data-cy="EditFixedPriceSelectOption" data-cy="EditFixedPriceSelectOption"
@update:model-value="newValue = null" @update:model-value="newValue = null"
:class="{ 'is-select': selectedField?.component === 'select' }" :class="{ 'is-select': selectedField?.component === 'select' }"
/> :emit-value="false"
>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps" class="q-pa-xs row items-center">
<QItemSection class="col-9 justify-center">
<span>{{ opt?.label }}</span>
</QItemSection>
</QItem>
</template>
</VnSelect>
<component <component
:is="inputs[selectedField?.component || 'input']" :is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}" v-bind="selectedField?.attrs || {}"
v-model="newValue" v-model="newFieldValue"
:label="t('Value')" :label="t('Value')"
data-cy="EditFixedPriceValueOption" data-cy="EditFixedPriceValueOption"
style="width: 200px" style="width: 200px"

View File

@ -19,7 +19,7 @@ const { t } = useI18n();
const dataKey = 'OrderCatalogList'; const dataKey = 'OrderCatalogList';
const catalogParams = { const catalogParams = {
orderFk: route.params.id, orderFk: route.params.id,
orderBy: JSON.stringify({ field: 'relevancy DESC, name', way: 'ASC', isTag: false }), orderBy: JSON.stringify({ field: 'relevancy DESC, longName', way: 'ASC', isTag: false }),
}; };
const arrayData = useArrayData(dataKey, { const arrayData = useArrayData(dataKey, {
url: 'Orders/CatalogFilter', url: 'Orders/CatalogFilter',

View File

@ -463,6 +463,32 @@ function setReference(data) {
dialogData.value.value.description = newDescription; dialogData.value.value.description = newDescription;
} }
function exprBuilder(param, value) {
switch (param) {
case 'stateFk':
return { 'ts.stateFk': value };
case 'provinceFk':
return { 'a.provinceFk': value };
case 'hour':
return { 'z.hour': value };
case 'shipped':
return {
't.shipped': {
between: this.dateRange(value),
},
};
case 'departmentFk':
return { 'c.departmentFk': value };
case 'id':
case 'refFk':
case 'zoneFk':
case 'nickname':
case 'agencyModeFk':
case 'warehouseFk':
return { [`t.${param}`]: value };
}
}
</script> </script>
<template> <template>
@ -657,17 +683,14 @@ function setReference(data) {
</VnSelect> </VnSelect>
</VnRow> </VnRow>
<VnRow> <VnRow>
<div class="col">
<VnInputDate <VnInputDate
placeholder="dd-mm-aaa" placeholder="dd-mm-aaa"
:label="t('globals.landed')" :label="t('globals.landed')"
v-model="data.landed" v-model="data.landed"
@update:model-value="() => fetchAvailableAgencies(data)" @update:model-value="() => fetchAvailableAgencies(data)"
/> />
</div>
</VnRow> </VnRow>
<VnRow> <VnRow>
<div class="col">
<VnSelect <VnSelect
url="Warehouses" url="Warehouses"
:sort-by="['name']" :sort-by="['name']"
@ -680,10 +703,8 @@ function setReference(data) {
}" }"
@update:model-value="() => fetchAvailableAgencies(data)" @update:model-value="() => fetchAvailableAgencies(data)"
/> />
</div>
</VnRow> </VnRow>
<VnRow> <VnRow>
<div class="col">
<VnSelect <VnSelect
:label="t('globals.agency')" :label="t('globals.agency')"
v-model="data.agencyModeId" v-model="data.agencyModeId"
@ -692,7 +713,6 @@ function setReference(data) {
option-label="agencyMode" option-label="agencyMode"
hide-selected hide-selected
/> />
</div>
</VnRow> </VnRow>
</template> </template>
</VnTable> </VnTable>

View File

@ -46,6 +46,28 @@ const { openConfirmationModal } = useVnConfirm();
:summary="$props.summary" :summary="$props.summary"
:to-module="{ name: 'WorkerDepartment' }" :to-module="{ name: 'WorkerDepartment' }"
data-key="Department" data-key="Department"
:filter="{
include: [
{
relation: 'client',
scope: {
fields: ['id', 'name'],
},
},
{
relation: 'worker',
scope: {
fields: ['id', 'name'],
include: {
relation: 'user',
scope: {
fields: ['id', 'name'],
},
},
},
},
],
}"
> >
<template #menu="{}"> <template #menu="{}">
<QItem <QItem

View File

@ -1,4 +1,3 @@
version: '3.7'
services: services:
back: back:
image: 'registry.verdnatura.es/salix-back:${COMPOSE_TAG:-dev}' image: 'registry.verdnatura.es/salix-back:${COMPOSE_TAG:-dev}'

View File

@ -7,7 +7,7 @@ function checkSageFields(isRequired = false) {
describe('Client fiscal data', () => { describe('Client fiscal data', () => {
describe('#1008', () => { describe('#1008', () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1920, 1080);
cy.login('developer'); cy.login('developer');
cy.visit('#/customer/1108/fiscal-data'); cy.visit('#/customer/1108/fiscal-data');
}); });
@ -27,12 +27,22 @@ describe('Client fiscal data', () => {
}); });
describe('#1007', () => { describe('#1007', () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(1280, 720); cy.viewport(1920, 1080);
cy.login('developer'); cy.login('developer');
cy.visit('#/customer/1107/fiscal-data'); cy.visit('#/customer/1107/fiscal-data');
}); });
it('check as equalizated', () => { it.skip('check as equalizated', () => {
cy.get('[data-cy="vnCheckboxInvoice by address"] > .q-checkbox__inner').then(
($el) => {
if (!$el.hasClass('q-checkbox__inner--truthy')) {
cy.wrap($el).click({ force: true });
}
},
);
cy.get(
'[data-cy="vnCheckboxInvoice by address"] > .q-checkbox__inner',
).should('have.class', 'q-checkbox__inner--truthy');
cy.dataCy('vnCheckboxIs equalizated').click(); cy.dataCy('vnCheckboxIs equalizated').click();
cy.get('.q-btn-group > .q-btn--standard > .q-btn__content').click(); cy.get('.q-btn-group > .q-btn--standard > .q-btn__content').click();
@ -40,7 +50,6 @@ describe('Client fiscal data', () => {
'contain', 'contain',
'You changed the equalization tax', 'You changed the equalization tax',
); );
cy.get('.q-card > :nth-child(2) > span').should( cy.get('.q-card > :nth-child(2) > span').should(
'have.text', 'have.text',
'Do you want to spread the change?', 'Do you want to spread the change?',

View File

@ -58,8 +58,8 @@ describe('Client list', { testIsolation: true }, () => {
cy.waitForElement('.q-form'); cy.waitForElement('.q-form');
cy.checkValueForm(1, search); cy.checkValueForm(1, search);
cy.checkValueForm(2, search); cy.checkValueForm(2, search);
cy.dataCy('Customer_select').should('have.value', search); cy.dataCy('Customer_select').contains(search);
cy.dataCy('Address_select').should('have.value', search); cy.dataCy('Address_select').contains(search);
}); });
it('Client founded create order', () => { it('Client founded create order', () => {
@ -74,7 +74,7 @@ describe('Client list', { testIsolation: true }, () => {
cy.waitForElement('#formModel'); cy.waitForElement('#formModel');
cy.waitForElement('.q-form'); cy.waitForElement('.q-form');
cy.checkValueForm(1, search); cy.checkValueForm(1, search);
cy.dataCy('Client_select').should('have.value', search); cy.dataCy('Client_select').contains(search);
cy.dataCy('Address_select').should('have.value', search); cy.dataCy('Address_select').contains(search);
}); });
}); });

View File

@ -1,5 +1,6 @@
import '../commands.js'; import '../commands.js';
describe('EntryBuys', () => { // https://redmine.verdnatura.es/issues/9008
describe.skip('EntryBuys', () => {
beforeEach(() => { beforeEach(() => {
cy.viewport(1920, 1080); cy.viewport(1920, 1080);
cy.login('buyer'); cy.login('buyer');
@ -95,7 +96,7 @@ describe('EntryBuys', () => {
cy.get('input[data-cy="itemFk-create-popup"]').type('1'); cy.get('input[data-cy="itemFk-create-popup"]').type('1');
cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click(); cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click();
cy.get('input[data-cy="Grouping mode_select"]').should('have.value', 'packing'); cy.dataCy('Grouping mode_select').contains('packing');
cy.get('button[data-cy="FormModelPopup_save"]').click(); cy.get('button[data-cy="FormModelPopup_save"]').click();
} }
}); });

View File

@ -17,11 +17,9 @@ describe('EntryNotes', () => {
const editObservation = (rowIndex, type, description) => { const editObservation = (rowIndex, type, description) => {
cy.get(`td[data-col-field="description"][data-row-index="${rowIndex}"]`) cy.get(`td[data-col-field="description"][data-row-index="${rowIndex}"]`)
.click() .click()
.clear()
.type(description); .type(description);
cy.get(`td[data-col-field="observationTypeFk"][data-row-index="${rowIndex}"]`) cy.get(`td[data-col-field="observationTypeFk"][data-row-index="${rowIndex}"]`)
.click() .click()
.clear()
.type(type); .type(type);
cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click(); cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click();
cy.saveCard(); cy.saveCard();

View File

@ -44,9 +44,9 @@ describe('invoiceInCorrective', { testIsolation: true }, () => {
cy.url().should('include', `/invoice-in/${correctingFk}/summary`); cy.url().should('include', `/invoice-in/${correctingFk}/summary`);
cy.visit(`/#/invoice-in/${correctingFk}/corrective`); cy.visit(`/#/invoice-in/${correctingFk}/corrective`);
cy.dataCy('invoiceInCorrective_class').should('be.disabled'); checkIsDisabled('class');
cy.dataCy('invoiceInCorrective_type').should('be.disabled'); checkIsDisabled('type');
cy.dataCy('invoiceInCorrective_reason').should('be.disabled'); checkIsDisabled('reason');
}); });
}); });
@ -56,4 +56,10 @@ describe('invoiceInCorrective', { testIsolation: true }, () => {
cy.clickDescriptorAction(4); cy.clickDescriptorAction(4);
cy.get('[data-cy="InvoiceInCorrective-menu-item"]').should('exist'); cy.get('[data-cy="InvoiceInCorrective-menu-item"]').should('exist');
}); });
function checkIsDisabled(column) {
cy.dataCy(`invoiceInCorrective_${column}`)
.parents('.q-field')
.should('have.class', 'q-field--disabled');
}
}); });

View File

@ -132,9 +132,9 @@ function createCorrective() {
const correctingId = response.body; const correctingId = response.body;
cy.url().should('include', `/invoice-in/${correctingId}/summary`); cy.url().should('include', `/invoice-in/${correctingId}/summary`);
cy.visit(`/#/invoice-in/${correctingId}/corrective`); cy.visit(`/#/invoice-in/${correctingId}/corrective`);
cy.dataCy('invoiceInCorrective_class').should('contain.value', 'R2'); cy.dataCy('invoiceInCorrective_class').contains('R2');
cy.dataCy('invoiceInCorrective_type').should('contain.value', 'diferencias'); cy.dataCy('invoiceInCorrective_type').contains('diferencias');
cy.dataCy('invoiceInCorrective_reason').should('contain.value', 'sales details'); cy.dataCy('invoiceInCorrective_reason').contains('sales details');
}); });
} }

View File

@ -12,11 +12,8 @@ describe('InvoiceInIntrastat', () => {
it('should edit the first line', () => { it('should edit the first line', () => {
cy.selectOption(`${firstRow} ${codes}`, 'Plantas vivas: Esqueje/injerto, Vid'); cy.selectOption(`${firstRow} ${codes}`, 'Plantas vivas: Esqueje/injerto, Vid');
cy.get(firstRowAmount).clear();
cy.saveCard(); cy.saveCard();
cy.get(codes) cy.get(codes).eq(0).contains('6021010: Plantas vivas: Esqueje/injerto, Vid');
.eq(0)
.should('have.value', '6021010: Plantas vivas: Esqueje/injerto, Vid');
}); });
it('should add a new row', () => { it('should add a new row', () => {
@ -30,7 +27,7 @@ describe('InvoiceInIntrastat', () => {
'FR', 'FR',
]); ]);
cy.saveCard(); cy.saveCard();
cy.get('.q-notification__message').should('have.text', 'Data saved'); cy.checkNotification('Data saved');
}); });
it('should remove the first line', () => { it('should remove the first line', () => {

View File

@ -52,7 +52,7 @@ describe('InvoiceInList', () => {
title: mockInvoiceRef, title: mockInvoiceRef,
listBox: { 0: '11/16/2001', 3: 'The farmer' }, listBox: { 0: '11/16/2001', 3: 'The farmer' },
}); });
cy.dataCy('invoiceInBasicDataCompanyFk').should('have.value', 'ORN'); cy.dataCy('invoiceInBasicDataCompanyFk').contains('ORN');
}); });
}); });
}); });

View File

@ -15,7 +15,7 @@ describe('InvoiceInVat', () => {
it('should edit the sage iva', () => { it('should edit the sage iva', () => {
cy.selectOption(`${firstLineVat} ${vats}`, 'H.P. IVA 21% CEE'); cy.selectOption(`${firstLineVat} ${vats}`, 'H.P. IVA 21% CEE');
cy.saveCard(); cy.saveCard();
cy.get(vats).eq(0).should('have.value', '8: H.P. IVA 21% CEE'); cy.get(vats).eq(0).contains('8: H.P. IVA 21% CEE');
}); });
it('should mark the line as deductible VAT', () => { it('should mark the line as deductible VAT', () => {
@ -23,9 +23,7 @@ describe('InvoiceInVat', () => {
cy.saveCard(); cy.saveCard();
cy.get(`${firstLineVat} [data-cy="isDeductible_checkbox"]`) cy.get(`${firstLineVat} [data-cy="isDeductible_checkbox"]`).click();
.click();
cy.saveCard(); cy.saveCard();
}); });

View File

@ -59,8 +59,8 @@ describe('OrderList', { testIsolation: true }, () => {
).click(); ).click();
cy.dataCy('vnTableCreateBtn').click(); cy.dataCy('vnTableCreateBtn').click();
cy.get(clientCreateSelect).should('have.value', 'Bruce Wayne'); cy.get(clientCreateSelect).contains('Bruce Wayne');
cy.get(addressCreateSelect).should('have.value', 'Bruce Wayne'); cy.get(addressCreateSelect).contains('Bruce Wayne');
cy.dataCy('landedDate').find('input').type('06/01/2001'); cy.dataCy('landedDate').find('input').type('06/01/2001');
cy.selectOption(agencyCreateSelect, 1); cy.selectOption(agencyCreateSelect, 1);

View File

@ -1,8 +1,7 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('ParkingBasicData', () => { describe('ParkingBasicData', () => {
const codeInput = 'form .q-card .q-input input'; const codeInput = 'form .q-card .q-input input';
const sectorSelect = 'form .q-card .q-select input'; const sectorSelect = 'form .q-card .q-select';
const sectorOpt = '.q-menu .q-item';
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.login('developer');
cy.visit(`/#/shelving/parking/1/basic-data`); cy.visit(`/#/shelving/parking/1/basic-data`);
@ -17,8 +16,7 @@ describe('ParkingBasicData', () => {
}); });
it('should edit the code and sector', () => { it('should edit the code and sector', () => {
cy.get(sectorSelect).type('First'); cy.selectOption(sectorSelect, 'First');
cy.get(sectorOpt).click();
cy.get(codeInput).eq(0).clear(); cy.get(codeInput).eq(0).clear();
cy.get(codeInput).eq(0).type('700-01'); cy.get(codeInput).eq(0).type('700-01');
@ -27,7 +25,7 @@ describe('ParkingBasicData', () => {
cy.saveCard(); cy.saveCard();
cy.checkNotification('Data saved'); cy.checkNotification('Data saved');
cy.get(sectorSelect).should('have.value', 'First sector'); cy.get(sectorSelect).contains('First sector');
cy.get(codeInput).should('have.value', '700-01'); cy.get(codeInput).should('have.value', '700-01');
cy.dataCy('Picking order_input').should('have.value', 80230); cy.dataCy('Picking order_input').should('have.value', 80230);
}); });

View File

@ -44,11 +44,11 @@ describe('TicketList', () => {
cy.intercept('GET', /\/api\/Clients\?filter/).as('clientFilter'); cy.intercept('GET', /\/api\/Clients\?filter/).as('clientFilter');
cy.vnTableCreateBtn(); cy.vnTableCreateBtn();
cy.wait('@clientFilter'); cy.wait('@clientFilter');
cy.dataCy('Customer_select').should('have.value', 'Bruce Wayne'); cy.dataCy('Customer_select').contains('Bruce Wayne');
cy.dataCy('Address_select').click(); cy.dataCy('Address_select').click();
cy.getOption().click(); cy.getOption().click();
cy.dataCy('Address_select').should('have.value', 'Bruce Wayne'); cy.dataCy('Address_select').contains('Bruce Wayne');
}); });
it('Client list create new ticket', () => { it('Client list create new ticket', () => {
cy.vnTableCreateBtn(); cy.vnTableCreateBtn();

View File

@ -5,48 +5,27 @@ describe('UserPanel', { testIsolation: true }, () => {
cy.login('developer'); cy.login('developer');
cy.visit(`/#dashboard`); cy.visit(`/#dashboard`);
cy.waitForElement('.q-page', 6000); cy.waitForElement('.q-page', 6000);
});
it('should notify when update user warehouse', () => {
const userWarehouse =
'.q-menu .q-gutter-xs > :nth-child(3) > .q-field > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native> .q-field__input';
// Abro el panel
cy.openUserPanel(); cy.openUserPanel();
// Compruebo la opcion inicial
cy.get(userWarehouse).should('have.value', 'VNL').click();
// Actualizo la opción
cy.getOption(3);
//Compruebo la notificación
cy.get('.q-notification').should('be.visible');
cy.get(userWarehouse).should('have.value', 'VNH');
//Restauro el valor
cy.get(userWarehouse).click();
cy.getOption(2);
}); });
it('should notify when update user company', () => { it('should notify when update user company', () => {
const userCompany = changeSelect('User company', 'VNH', 'VNL');
'.q-menu .q-gutter-xs > :nth-child(2) > .q-field--float > .q-field__inner > .q-field__control > .q-field__control-container > .q-field__native> .q-field__input';
// Abro el panel
cy.openUserPanel();
// Compruebo la opcion inicial
cy.get(userCompany).should('have.value', 'Warehouse One').click();
//Actualizo la opción
cy.getOption(3);
//Compruebo la notificación
cy.get('.q-notification').should('be.visible');
cy.get(userCompany).should('have.value', 'TestingWarehouse');
//Restauro el valor
cy.get(userCompany).click();
cy.getOption(1);
}); });
it('should notify when update user warehouse', () => {
changeSelect('User warehouse', 'TestingWarehouse', 'Warehouse One');
});
function changeSelect(field, newOption, oldOption) {
cy.get('.q-menu')
.contains(field)
.then(($field) => {
cy.wrap($field).contains(oldOption);
cy.selectOption($field, newOption);
cy.checkNotification('Data saved');
cy.wrap($field).contains(newOption);
// Restore
cy.selectOption($field, oldOption);
});
}
}); });

View File

@ -41,7 +41,7 @@ describe('VnLocation', { testIsolation: true }, () => {
cy.get( cy.get(
`${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) `, `${createForm.prefix} > :nth-child(4) > .q-select > ${createForm.sufix} > :nth-child(3) `,
).click(); ).click();
cy.dataCy('locationProvince').should('have.value', province); cy.dataCy('locationProvince').contains(province);
}); });
}); });
describe('Worker Create', () => { describe('Worker Create', () => {

View File

@ -11,6 +11,6 @@ describe('WorkerLocker', () => {
it('should allocates a locker', () => { it('should allocates a locker', () => {
cy.selectOption(lockerSelect, lockerCode); cy.selectOption(lockerSelect, lockerCode);
cy.saveCard(); cy.saveCard();
cy.get(lockerSelect).invoke('val').should('eq', lockerCode); cy.get(lockerSelect).contains(lockerCode);
}); });
}); });

View File

@ -40,7 +40,7 @@ describe('ZoneCalendar', { testIsolation: true }, () => {
it('should exclude an event', () => { it('should exclude an event', () => {
cy.get('.q-mb-sm > .q-radio__inner').click(); cy.get('.q-mb-sm > .q-radio__inner').click();
cy.get('.q-current-day > .q-calendar-month__day--label__wrapper').click(); cy.get('.q-current-day > .q-calendar-month__day--label__wrapper').click();
cy.get('.q-mt-lg > .q-btn--standard').click(); cy.get(submitBtn).click();
cy.get( cy.get(
'.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]', '.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]',
).click(); ).click();
@ -48,19 +48,15 @@ describe('ZoneCalendar', { testIsolation: true }, () => {
cy.dataCy('VnConfirm_confirm').click(); cy.dataCy('VnConfirm_confirm').click();
}); });
it( it('should not exclude an event if there are tickets for that zone and day', () => {
'should not exclude an event if there are tickets for that zone and day', cy.get('[data-cy="vn-searchbar_input"]').type('3{enter}');
{ testIsoaltion: true }, cy.get('[aria-label="Exclude"] > .q-radio__label').click();
() => {
cy.visit(`/#/zone/3/events`);
cy.get('.q-mb-sm > .q-radio__inner').click();
cy.get( cy.get(
'.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]', '.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]',
).click(); ).click();
cy.get('.q-mt-lg > .q-btn--standard').click(); cy.get(submitBtn).click();
cy.checkNotification( cy.checkNotification(
'Can not close this zone because there are tickets programmed for that day', 'Can not close this zone because there are tickets programmed for that day',
); );
}, });
);
}); });

View File

@ -178,33 +178,34 @@ Cypress.Commands.add('fillInForm', (obj, opts = {}) => {
cy.waitSpinner(); cy.waitSpinner();
const { form = '.q-form > .q-card', attr = 'aria-label' } = opts; const { form = '.q-form > .q-card', attr = 'aria-label' } = opts;
cy.waitForElement(form); cy.waitForElement(form);
cy.get(`${form} input`).each(([el]) => {
cy.wrap(el) cy.get(`${form} .q-field`).each(($el) => {
.invoke('attr', attr) cy.wrap($el).then(($element) => {
.then((key) => { const key = $element.attr(attr) || $element.find(`[${attr}]`).attr(attr);
const field = obj[key]; const field = obj[key];
if (!field) return; if (!field) return;
if (typeof field == 'string')
return cy
.wrap(el)
.type(`{selectall}{backspace}${field}`, { delay: 0 });
const { type, val } = field; const { type, val } =
typeof field === 'string' ? { type: 'string', val: field } : field;
switch (type) { switch (type) {
case 'select': case 'select':
cy.selectOption(el, val); cy.selectOption($el, val);
break; break;
case 'date': case 'date':
cy.get(el).type(`{selectall}{backspace}${val}`).blur(); cy.wrap($el)
.find('input')
.type(`{selectall}{backspace}${val}`)
.blur();
break; break;
case 'time': case 'time':
cy.get(el).click(); cy.wrap($el).click();
cy.get('.q-time .q-time__clock').contains(val.h).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__clock').contains(val.m).click();
cy.get('.q-time .q-time__link').contains(val.x).click(); cy.get('.q-time .q-time__link').contains(val.x).click();
break; break;
default: default:
cy.wrap(el).type(`{selectall}${val}`, { delay: 0 }); cy.wrap($el).find('input').type(`{selectall}${val}`, { delay: 0 });
break; break;
} }
}); });