0
0
Fork 0

Merge branch 'dev' into 7663-setWeight

This commit is contained in:
Jorge Penadés 2024-09-11 07:40:36 +00:00
commit 2d5602e784
39 changed files with 195 additions and 213 deletions

View File

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

View File

@ -105,7 +105,7 @@ async function setProvince(id, data) {
option-label="name" option-label="name"
option-value="id" option-value="id"
:rules="validate('postcode.city')" :rules="validate('postcode.city')"
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:emit-value="false" :emit-value="false"
clearable clearable
> >

View File

@ -36,7 +36,7 @@ const itemComputed = computed(() => {
<QItemSection> <QItemSection>
{{ t(itemComputed.title) }} {{ t(itemComputed.title) }}
<QTooltip> <QTooltip>
{{ 'Ctrl + Alt + ' + item.keyBinding.toUpperCase() }} {{ 'Ctrl + Alt + ' + item?.keyBinding?.toUpperCase() }}
</QTooltip> </QTooltip>
</QItemSection> </QItemSection>
<QItemSection side> <QItemSection side>

View File

@ -38,7 +38,7 @@ async function onProvinceCreated(_, data) {
hide-selected hide-selected
v-model="provinceFk" v-model="provinceFk"
:rules="validate && validate('postcode.provinceFk')" :rules="validate && validate('postcode.provinceFk')"
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]"
> >
<template #option="{ itemProps, opt }"> <template #option="{ itemProps, opt }">
<QItem v-bind="itemProps"> <QItem v-bind="itemProps">

View File

@ -1,6 +1,7 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed } from 'vue';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import { useAcl } from 'src/composables/useAcl';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
const emit = defineEmits(['update:modelValue']); const emit = defineEmits(['update:modelValue']);
@ -11,6 +12,10 @@ const $props = defineProps({
type: Array, type: Array,
default: () => ['developer'], default: () => ['developer'],
}, },
acls: {
type: Array,
default: () => [],
},
actionIcon: { actionIcon: {
type: String, type: String,
default: 'add', default: 'add',
@ -22,15 +27,13 @@ const $props = defineProps({
}); });
const role = useRole(); const role = useRole();
const acl = useAcl();
const showForm = ref(false); const showForm = ref(false);
const isAllowedToCreate = computed(() => { const isAllowedToCreate = computed(() => {
if ($props.acls.length) return acl.hasAny($props.acls);
return role.hasAny($props.rolesAllowedToCreate); return role.hasAny($props.rolesAllowedToCreate);
}); });
const toggleForm = () => {
showForm.value = !showForm.value;
};
</script> </script>
<template> <template>
@ -41,7 +44,7 @@ const toggleForm = () => {
> >
<template v-if="isAllowedToCreate" #append> <template v-if="isAllowedToCreate" #append>
<QIcon <QIcon
@click.stop.prevent="toggleForm()" @click.stop.prevent="$refs.dialog.show()"
:name="actionIcon" :name="actionIcon"
:size="actionIcon === 'add' ? 'xs' : 'sm'" :size="actionIcon === 'add' ? 'xs' : 'sm'"
:class="['default-icon', { '--add-icon': actionIcon === 'add' }]" :class="['default-icon', { '--add-icon': actionIcon === 'add' }]"
@ -51,7 +54,7 @@ const toggleForm = () => {
> >
<QTooltip v-if="tooltip">{{ tooltip }}</QTooltip> <QTooltip v-if="tooltip">{{ tooltip }}</QTooltip>
</QIcon> </QIcon>
<QDialog v-model="showForm" transition-show="scale" transition-hide="scale"> <QDialog ref="dialog" transition-show="scale" transition-hide="scale">
<slot name="form" /> <slot name="form" />
</QDialog> </QDialog>
</template> </template>

View File

@ -119,8 +119,8 @@ watch(
); );
watch( watch(
() => [props.url, props.filter], () => [props.url, props.filter, props.userParams],
([url, filter]) => mounted.value && fetch({ url, filter }) ([url, filter, userParams]) => mounted.value && fetch({ url, filter, userParams })
); );
const addFilter = async (filter, params) => { const addFilter = async (filter, params) => {

View File

@ -16,14 +16,19 @@ export function useAcl() {
state.setAcls(acls); state.setAcls(acls);
} }
function hasAny(model, prop, accessType) { function hasAny(acls) {
const acls = state.getAcls().value[model]; for (const acl of acls) {
if (acls) let { model, props, accessType } = acl;
return ['*', prop].some((key) => { const modelAcls = state.getAcls().value[model];
const acl = acls[key]; Array.isArray(props) || (props = [props]);
if (modelAcls)
return ['*', ...props].some((key) => {
const acl = modelAcls[key];
return acl && (acl['*'] || acl[accessType]); return acl && (acl['*'] || acl[accessType]);
}); });
} }
return false;
}
return { return {
fetch, fetch,

View File

@ -37,6 +37,10 @@ a {
.link { .link {
color: $color-link; color: $color-link;
cursor: pointer; cursor: pointer;
&--white {
color: white;
}
} }
.tx-color-link { .tx-color-link {

View File

@ -2,7 +2,7 @@
import { computed, onBeforeMount, ref } from 'vue'; import { computed, onBeforeMount, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useRole } from 'src/composables/useRole'; import { useAcl } from 'src/composables/useAcl';
import axios from 'axios'; import axios from 'axios';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
@ -24,7 +24,7 @@ import InvoiceOutDescriptorProxy from 'src/pages/InvoiceOut/Card/InvoiceOutDescr
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const { sendEmail } = usePrintService(); const { sendEmail } = usePrintService();
const { t } = useI18n(); const { t } = useI18n();
const { hasAny } = useRole(); const { hasAny } = useAcl();
const session = useSession(); const session = useSession();
const tokenMultimedia = session.getTokenMultimedia(); const tokenMultimedia = session.getTokenMultimedia();
@ -284,7 +284,9 @@ const showBalancePdf = ({ id }) => {
> >
<VnInput <VnInput
v-model="scope.value" v-model="scope.value"
:disable="!hasAny(['administrative'])" :disable="
!hasAny([{ model: 'Receipt', props: '*', accessType: 'WRITE' }])
"
@keypress.enter="scope.set" @keypress.enter="scope.set"
autofocus autofocus
/> />

View File

@ -70,7 +70,7 @@ const getBankEntities = (data, formData) => {
<VnSelectDialog <VnSelectDialog
:label="t('Swift / BIC')" :label="t('Swift / BIC')"
:options="bankEntitiesOptions" :options="bankEntitiesOptions"
:roles-allowed-to-create="['salesAssistant', 'hr']" :acls="[{ model: 'BankEntity', props: '*', accessType: 'WRITE' }]"
:rules="validate('Worker.bankEntity')" :rules="validate('Worker.bankEntity')"
hide-selected hide-selected
option-label="name" option-label="name"

View File

@ -93,7 +93,7 @@ function handleLocation(data, location) {
<VnRow> <VnRow>
<VnLocation <VnLocation
:rules="validate('Worker.postcode')" :rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
v-model="data.postcode" v-model="data.postcode"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
/> />

View File

@ -86,7 +86,7 @@ function handleLocation(data, location) {
<VnRow> <VnRow>
<VnLocation <VnLocation
:rules="validate('Worker.postcode')" :rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
v-model="data.location" v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
> >

View File

@ -412,7 +412,7 @@ function handleLocation(data, location) {
> >
<template #more-create-dialog="{ data }"> <template #more-create-dialog="{ data }">
<VnLocation <VnLocation
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Province', props: '*', accessType: 'WRITE' }]"
v-model="data.location" v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
/> />

View File

@ -92,7 +92,7 @@ function handleLocation(data, location) {
<VnLocation <VnLocation
:rules="validate('Worker.postcode')" :rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
v-model="data.location" v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
/> />

View File

@ -176,7 +176,7 @@ function handleLocation(data, location) {
<div class="col"> <div class="col">
<VnLocation <VnLocation
:rules="validate('Worker.postcode')" :rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
v-model="data.postalCode" v-model="data.postalCode"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
></VnLocation> ></VnLocation>

View File

@ -5,7 +5,7 @@ import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import axios from 'axios'; import axios from 'axios';
import { toCurrency, toDate } from 'src/filters'; import { toCurrency, toDate } from 'src/filters';
import { useRole } from 'src/composables/useRole'; import { useAcl } from 'src/composables/useAcl';
import { downloadFile } from 'src/composables/downloadFile'; import { downloadFile } from 'src/composables/downloadFile';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import { usePrintService } from 'composables/usePrintService'; import { usePrintService } from 'composables/usePrintService';
@ -24,7 +24,7 @@ const $props = defineProps({ id: { type: Number, default: null } });
const { push, currentRoute } = useRouter(); const { push, currentRoute } = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const { hasAny } = useRole(); const { hasAny } = useAcl();
const { t } = useI18n(); const { t } = useI18n();
const { openReport, sendEmail } = usePrintService(); const { openReport, sendEmail } = usePrintService();
const arrayData = useArrayData(); const arrayData = useArrayData();
@ -195,7 +195,8 @@ async function cloneInvoice() {
push({ path: `/invoice-in/${data.id}/summary` }); push({ path: `/invoice-in/${data.id}/summary` });
} }
const isAdministrative = () => hasAny(['administrative']); const canEditProp = (props) =>
hasAny([{ model: 'InvoiceIn', props, accessType: 'WRITE' }]);
const isAgricultural = () => { const isAgricultural = () => {
if (!config.value) return false; if (!config.value) return false;
@ -283,7 +284,7 @@ const createInvoiceInCorrection = async () => {
<InvoiceInToBook> <InvoiceInToBook>
<template #content="{ book }"> <template #content="{ book }">
<QItem <QItem
v-if="!entity?.isBooked && isAdministrative()" v-if="!entity?.isBooked && canEditProp('toBook')"
v-ripple v-ripple
clickable clickable
@click="book(entityId)" @click="book(entityId)"
@ -293,7 +294,7 @@ const createInvoiceInCorrection = async () => {
</template> </template>
</InvoiceInToBook> </InvoiceInToBook>
<QItem <QItem
v-if="entity?.isBooked && isAdministrative()" v-if="entity?.isBooked && canEditProp('toUnbook')"
v-ripple v-ripple
clickable clickable
@click="triggerMenu('unbook')" @click="triggerMenu('unbook')"
@ -303,7 +304,7 @@ const createInvoiceInCorrection = async () => {
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem <QItem
v-if="isAdministrative()" v-if="canEditProp('deleteById')"
v-ripple v-ripple
clickable clickable
@click="triggerMenu('delete')" @click="triggerMenu('delete')"
@ -311,7 +312,7 @@ const createInvoiceInCorrection = async () => {
<QItemSection>{{ t('Delete invoice') }}</QItemSection> <QItemSection>{{ t('Delete invoice') }}</QItemSection>
</QItem> </QItem>
<QItem <QItem
v-if="isAdministrative()" v-if="canEditProp('clone')"
v-ripple v-ripple
clickable clickable
@click="triggerMenu('clone')" @click="triggerMenu('clone')"

View File

@ -7,8 +7,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue'; import ItemDescriptorImage from 'src/pages/Item/Card/ItemDescriptorImage.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import VnTitle from 'src/components/common/VnTitle.vue';
import { useRole } from 'src/composables/useRole';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -19,23 +18,10 @@ const $props = defineProps({
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
const roleState = useRole();
const entityId = computed(() => $props.id || route.params.id); const entityId = computed(() => $props.id || route.params.id);
const getUrl = (id, param) => `#/Item/${id}/${param}`;
const isBuyer = computed(() => {
return roleState.hasAny(['buyer']);
});
const isReplenisher = computed(() => {
return roleState.hasAny(['replenisher']);
});
const isAdministrative = computed(() => {
return roleState.hasAny(['administrative']);
});
</script> </script>
<template> <template>
<CardSummary <CardSummary
ref="summary" ref="summary"
@ -44,13 +30,15 @@ const isAdministrative = computed(() => {
data-key="ItemSummary" data-key="ItemSummary"
> >
<template #header-left> <template #header-left>
<router-link <QBtn
v-if="route.name !== 'ItemSummary'" v-if="$route.name !== 'ItemSummary'"
:to="{ name: 'ItemSummary', params: { id: entityId } }" :to="{ name: 'ItemSummary', params: { id: entityId } }"
class="header link" class="header link--white"
> icon="open_in_new"
<QIcon name="open_in_new" color="white" size="sm" /> flat
</router-link> dense
round
/>
</template> </template>
<template #header="{ entity: { item } }"> <template #header="{ entity: { item } }">
{{ item.id }} - {{ item.name }} {{ item.id }} - {{ item.name }}
@ -65,15 +53,10 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<component <VnTitle
:is="isBuyer ? 'router-link' : 'span'" :url="getUrl(entityId, 'basic-data')"
:to="{ name: 'ItemBasicData', params: { id: entityId } }" :text="t('item.summary.basicData')"
class="header" />
:class="{ 'header-link': isBuyer }"
>
{{ t('item.summary.basicData') }}
<QIcon v-if="isBuyer" name="open_in_new" />
</component>
<VnLv :label="t('item.summary.name')" :value="item.name" /> <VnLv :label="t('item.summary.name')" :value="item.name" />
<VnLv :label="t('item.summary.completeName')" :value="item.longName" /> <VnLv :label="t('item.summary.completeName')" :value="item.longName" />
<VnLv :label="t('item.summary.family')" :value="item.itemType.name" /> <VnLv :label="t('item.summary.family')" :value="item.itemType.name" />
@ -104,15 +87,10 @@ const isAdministrative = computed(() => {
</VnLv> </VnLv>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<component <VnTitle
:is="isBuyer ? 'router-link' : 'span'" :url="getUrl(entityId, 'basic-data')"
:to="{ name: 'ItemBasicData', params: { id: entityId } }" :text="t('item.summary.otherData')"
class="header" />
:class="{ 'header-link': isBuyer }"
>
{{ t('item.summary.otherData') }}
<QIcon v-if="isBuyer" name="open_in_new" />
</component>
<VnLv <VnLv
:label="t('item.summary.intrastatCode')" :label="t('item.summary.intrastatCode')"
:value="item.intrastat.id" :value="item.intrastat.id"
@ -137,15 +115,7 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<component <VnTitle :url="getUrl(entityId, 'tags')" :text="t('item.summary.tags')" />
:is="isBuyer || isReplenisher ? 'router-link' : 'span'"
:to="{ name: 'ItemTags', params: { id: entityId } }"
class="header"
:class="{ 'header-link': isBuyer || isReplenisher }"
>
{{ t('item.summary.tags') }}
<QIcon v-if="isBuyer || isReplenisher" name="open_in_new" />
</component>
<VnLv <VnLv
v-for="(tag, index) in tags" v-for="(tag, index) in tags"
:key="index" :key="index"
@ -154,29 +124,14 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one" v-if="item.description"> <QCard class="vn-one" v-if="item.description">
<component <VnTitle
:is="isBuyer ? 'router-link' : 'span'" :url="getUrl(entityId, 'basic-data')"
:to="{ name: 'ItemBasicData', params: { id: entityId } }" :text="t('item.summary.description')"
class="header" />
:class="{ 'header-link': isBuyer }" <p v-text="item.description" />
>
{{ t('item.summary.description') }}
<QIcon v-if="isBuyer" name="open_in_new" />
</component>
<p>
{{ item.description }}
</p>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<component <VnTitle :url="getUrl(entityId, 'tax')" :text="t('item.summary.tax')" />
:is="isBuyer || isAdministrative ? 'router-link' : 'span'"
:to="{ name: 'ItemTax', params: { id: entityId } }"
class="header"
:class="{ 'header-link': isBuyer || isAdministrative }"
>
{{ t('item.summary.tax') }}
<QIcon v-if="isBuyer || isAdministrative" name="open_in_new" />
</component>
<VnLv <VnLv
v-for="(tax, index) in item.taxes" v-for="(tax, index) in item.taxes"
:key="index" :key="index"
@ -185,15 +140,10 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<component <VnTitle
:is="isBuyer ? 'router-link' : 'span'" :url="getUrl(entityId, 'botanical')"
:to="{ name: 'ItemBotanical', params: { id: entityId } }" :text="t('item.summary.botanical')"
class="header" />
:class="{ 'header-link': isBuyer }"
>
{{ t('item.summary.botanical') }}
<QIcon v-if="isBuyer" name="open_in_new" />
</component>
<VnLv :label="t('item.summary.genus')" :value="botanical?.genus?.name" /> <VnLv :label="t('item.summary.genus')" :value="botanical?.genus?.name" />
<VnLv <VnLv
:label="t('item.summary.specie')" :label="t('item.summary.specie')"
@ -201,23 +151,19 @@ const isAdministrative = computed(() => {
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<component <VnTitle
:is="isBuyer || isReplenisher ? 'router-link' : 'span'" :url="getUrl(entityId, 'barcode')"
:to="{ name: 'ItemBarcode', params: { id: entityId } }" :text="t('item.summary.barcode')"
class="header" />
:class="{ 'header-link': isBuyer || isReplenisher }" <p
> v-for="(barcode, index) in item.itemBarcode"
{{ t('item.summary.barcode') }} :key="index"
<QIcon v-if="isBuyer || isReplenisher" name="open_in_new" /> v-text="barcode.code"
</component> />
<p v-for="(barcode, index) in item.itemBarcode" :key="index">
{{ barcode.code }}
</p>
</QCard> </QCard>
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
<i18n> <i18n>
en: en:
Este artículo necesita una foto: Este artículo necesita una foto Este artículo necesita una foto: Este artículo necesita una foto

View File

@ -83,7 +83,7 @@ function handleLocation(data, location) {
<VnRow> <VnRow>
<VnLocation <VnLocation
:rules="validate('Worker.postcode')" :rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
v-model="data.location" v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
> >

View File

@ -129,7 +129,7 @@ function handleLocation(data, location) {
<VnRow> <VnRow>
<VnLocation <VnLocation
:rules="validate('Worker.postcode')" :rules="validate('Worker.postcode')"
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
v-model="data.postCode" v-model="data.postCode"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
> >

View File

@ -4,13 +4,11 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import VnLv from 'src/components/ui/VnLv.vue'; import VnLv from 'src/components/ui/VnLv.vue';
import { useRole } from 'src/composables/useRole';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
const route = useRoute(); const route = useRoute();
const roleState = useRole();
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
@ -32,13 +30,7 @@ async function setData(data) {
} }
} }
const isAdministrative = computed(() => { const getUrl = (section) => `#/supplier/${entityId.value}/${section}`;
return roleState.hasAny(['administrative']);
});
function getUrl(section) {
return isAdministrative.value && `#/supplier/${entityId.value}/${section}`;
}
</script> </script>
<template> <template>

View File

@ -635,6 +635,7 @@ onMounted(async () => {
<template #body-cell-state="{ row }"> <template #body-cell-state="{ row }">
<QTd> <QTd>
<QBadge <QBadge
v-if="row.state"
text-color="black" text-color="black"
:color="row.classColor" :color="row.classColor"
class="q-ma-none" class="q-ma-none"
@ -642,6 +643,7 @@ onMounted(async () => {
> >
{{ row.state }} {{ row.state }}
</QBadge> </QBadge>
<span v-else> {{ dashIfEmpty(row.state) }}</span>
</QTd> </QTd>
</template> </template>
<template #body-cell-import="{ row }"> <template #body-cell-import="{ row }">

View File

@ -55,7 +55,7 @@ onMounted(async () => await getItemPackingTypes());
:data-key="props.dataKey" :data-key="props.dataKey"
:search-button="true" :search-button="true"
:hidden-tags="['search']" :hidden-tags="['search']"
:un-removable-params="['warehouseFk', 'dateFuture', 'dateToAdvance']" :unremovable-params="['warehouseFk', 'dateFuture', 'dateToAdvance']"
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
@ -119,10 +119,9 @@ onMounted(async () => await getItemPackingTypes());
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox
:label="t('params.itemPackingTypes')" :label="t('params.isFullMovable')"
v-model="params.itemPackingTypes" v-model="params.isFullMovable"
toggle-indeterminate toggle-indeterminate
:false-value="null"
@update:model-value="searchFn()" @update:model-value="searchFn()"
/> />
</QItemSection> </QItemSection>
@ -155,7 +154,7 @@ en:
dateToAdvance: Destination date dateToAdvance: Destination date
futureIpt: Origin IPT futureIpt: Origin IPT
ipt: Destination IPT ipt: Destination IPT
itemPackingTypes: 100% movable isFullMovable: 100% movable
warehouseFk: Warehouse warehouseFk: Warehouse
es: es:
Horizontal: Horizontal Horizontal: Horizontal
@ -166,6 +165,6 @@ es:
dateToAdvance: Fecha destino dateToAdvance: Fecha destino
futureIpt: IPT Origen futureIpt: IPT Origen
ipt: IPT destino ipt: IPT destino
itemPackingTypes: 100% movible isFullMovable: 100% movible
warehouseFk: Almacén warehouseFk: Almacén
</i18n> </i18n>

View File

@ -49,8 +49,8 @@ const exprBuilder = (param, value) => {
}; };
const userParams = reactive({ const userParams = reactive({
futureDated: Date.vnNew().toISOString(), futureScopeDays: Date.vnNew().toISOString(),
originDated: Date.vnNew().toISOString(), originScopeDays: Date.vnNew().toISOString(),
warehouseFk: user.value.warehouseFk, warehouseFk: user.value.warehouseFk,
}); });
@ -62,8 +62,8 @@ const arrayData = useArrayData('FutureTickets', {
const { store } = arrayData; const { store } = arrayData;
const params = reactive({ const params = reactive({
futureDated: Date.vnNew(), futureScopeDays: Date.vnNew(),
originDated: Date.vnNew(), originScopeDays: Date.vnNew(),
warehouseFk: user.value.warehouseFk, warehouseFk: user.value.warehouseFk,
}); });
@ -172,7 +172,7 @@ const ticketColumns = computed(() => [
label: t('futureTickets.availableLines'), label: t('futureTickets.availableLines'),
name: 'lines', name: 'lines',
field: 'lines', field: 'lines',
align: 'left', align: 'center',
sortable: true, sortable: true,
columnFilter: { columnFilter: {
component: VnInput, component: VnInput,
@ -234,7 +234,7 @@ const ticketColumns = computed(() => [
{ {
label: t('futureTickets.futureState'), label: t('futureTickets.futureState'),
name: 'futureState', name: 'futureState',
align: 'left', align: 'right',
sortable: true, sortable: true,
columnFilter: null, columnFilter: null,
format: (val) => dashIfEmpty(val), format: (val) => dashIfEmpty(val),
@ -458,7 +458,7 @@ onMounted(async () => {
</QTd> </QTd>
</template> </template>
<template #body-cell-shipped="{ row }"> <template #body-cell-shipped="{ row }">
<QTd> <QTd class="shipped">
<QBadge <QBadge
text-color="black" text-color="black"
:color="getDateQBadgeColor(row.shipped)" :color="getDateQBadgeColor(row.shipped)"
@ -505,7 +505,7 @@ onMounted(async () => {
</QTd> </QTd>
</template> </template>
<template #body-cell-futureShipped="{ row }"> <template #body-cell-futureShipped="{ row }">
<QTd> <QTd class="shipped">
<QBadge <QBadge
text-color="black" text-color="black"
:color="getDateQBadgeColor(row.futureShipped)" :color="getDateQBadgeColor(row.futureShipped)"
@ -532,6 +532,9 @@ onMounted(async () => {
</template> </template>
<style scoped lang="scss"> <style scoped lang="scss">
.shipped {
min-width: 132px;
}
.vertical-separator { .vertical-separator {
border-left: 4px solid white !important; border-left: 4px solid white !important;
} }

View File

@ -68,7 +68,7 @@ onMounted(async () => {
<VnFilterPanel <VnFilterPanel
:data-key="props.dataKey" :data-key="props.dataKey"
:hidden-tags="['search']" :hidden-tags="['search']"
:un-removable-params="['warehouseFk', 'originDated', 'futureDated']" :un-removable-params="['warehouseFk', 'originScopeDays ', 'futureScopeDays']"
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
@ -80,8 +80,8 @@ onMounted(async () => {
<QItem class="q-my-sm"> <QItem class="q-my-sm">
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.originDated" v-model="params.originScopeDays"
:label="t('params.originDated')" :label="t('params.originScopeDays')"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -89,8 +89,8 @@ onMounted(async () => {
<QItem class="q-my-sm"> <QItem class="q-my-sm">
<QItemSection> <QItemSection>
<VnInputDate <VnInputDate
v-model="params.futureDated" v-model="params.futureScopeDays"
:label="t('params.futureDated')" :label="t('params.futureScopeDays')"
is-outlined is-outlined
/> />
</QItemSection> </QItemSection>
@ -214,8 +214,8 @@ onMounted(async () => {
en: en:
iptInfo: IPT iptInfo: IPT
params: params:
originDated: Origin date originScopeDays: Origin date
futureDated: Destination date futureScopeDays: Destination date
futureIpt: Destination IPT futureIpt: Destination IPT
ipt: Origin IPT ipt: Origin IPT
warehouseFk: Warehouse warehouseFk: Warehouse
@ -229,8 +229,8 @@ es:
Vertical: Vertical Vertical: Vertical
iptInfo: Encajado iptInfo: Encajado
params: params:
originDated: Fecha origen originScopeDays: Fecha origen
futureDated: Fecha destino futureScopeDays: Fecha destino
futureIpt: IPT destino futureIpt: IPT destino
ipt: IPT Origen ipt: IPT Origen
warehouseFk: Almacén warehouseFk: Almacén

View File

@ -105,7 +105,7 @@ advanceTickets:
futureLines: Líneas futureLines: Líneas
futureImport: Importe futureImport: Importe
advanceTickets: Adelantar tickets con negativos advanceTickets: Adelantar tickets con negativos
advanceTicketTitle: Advance tickets advanceTicketTitle: Adelantar tickets
advanceTitleSubtitle: '¿Desea adelantar {selectedTickets} tickets?' advanceTitleSubtitle: '¿Desea adelantar {selectedTickets} tickets?'
noDeliveryZone: No hay una zona de reparto disponible para la fecha de envío seleccionada noDeliveryZone: No hay una zona de reparto disponible para la fecha de envío seleccionada
moveTicketSuccess: 'Tickets movidos correctamente {ticketsNumber}' moveTicketSuccess: 'Tickets movidos correctamente {ticketsNumber}'

View File

@ -8,7 +8,7 @@ import VnConfirm from 'components/ui/VnConfirm.vue';
import axios from 'axios'; import axios from 'axios';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { useRole } from 'src/composables/useRole'; import { useAcl } from 'src/composables/useAcl';
const $props = defineProps({ const $props = defineProps({
travel: { travel: {
@ -21,7 +21,6 @@ const { t } = useI18n();
const router = useRouter(); const router = useRouter();
const quasar = useQuasar(); const quasar = useQuasar();
const { notify } = useNotify(); const { notify } = useNotify();
const role = useRole();
const redirectToCreateView = (queryParams) => { const redirectToCreateView = (queryParams) => {
router.push({ name: 'TravelCreate', query: { travelData: queryParams } }); router.push({ name: 'TravelCreate', query: { travelData: queryParams } });
@ -42,9 +41,7 @@ const cloneTravelWithEntries = async () => {
} }
}; };
const isBuyer = computed(() => { const canDelete = computed(() => useAcl().hasAny('Travel','*','WRITE'));
return role.hasAny(['buyer']);
});
const openDeleteEntryDialog = (id) => { const openDeleteEntryDialog = (id) => {
quasar quasar
@ -81,7 +78,7 @@ const deleteTravel = async (id) => {
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem <QItem
v-if="isBuyer && travel.totalEntries === 0" v-if="canDelete && travel.totalEntries === 0"
v-ripple v-ripple
clickable clickable
@click="openDeleteEntryDialog(travel.id)" @click="openDeleteEntryDialog(travel.id)"

View File

@ -3,13 +3,13 @@ import { ref, computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import { useRole } from 'src/composables/useRole'; import { useAcl } from 'src/composables/useAcl';
import FormModel from 'components/FormModel.vue'; import FormModel from 'components/FormModel.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
const { hasAny } = useRole(); const { hasAny } = useAcl();
const { t } = useI18n(); const { t } = useI18n();
const fetchData = ref(); const fetchData = ref();
const originaLockerId = ref(); const originaLockerId = ref();
@ -57,7 +57,11 @@ const init = async (data) => {
option-label="code" option-label="code"
option-value="id" option-value="id"
hide-selected hide-selected
:readonly="!hasAny(['productionBoss', 'hr'])" :readonly="
!hasAny([
{ model: 'Worker', props: '__get__locker', accessType: 'READ' },
])
"
/> />
</template> </template>
</FormModel> </FormModel>

View File

@ -13,6 +13,7 @@ import WorkerTimeControlCalendar from 'pages/Worker/Card/WorkerTimeControlCalend
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import axios from 'axios'; import axios from 'axios';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import { useAcl } from 'src/composables/useAcl';
import { useWeekdayStore } from 'src/stores/useWeekdayStore'; import { useWeekdayStore } from 'src/stores/useWeekdayStore';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
@ -26,7 +27,6 @@ import { date } from 'quasar';
const route = useRoute(); const route = useRoute();
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
const { hasAny } = useRole();
const _state = useState(); const _state = useState();
const user = _state.getUser(); const user = _state.getUser();
const stateStore = useStateStore(); const stateStore = useStateStore();
@ -66,9 +66,11 @@ const arrayData = useArrayData('workerData');
const worker = computed(() => arrayData.store?.data); const worker = computed(() => arrayData.store?.data);
const isHr = computed(() => hasAny(['hr'])); const isHr = computed(() => useRole().hasAny(['hr']));
const isHimSelf = computed(() => user.value.id === Number(route.params.id)); const canSend = computed(() => useAcl().hasAny('WorkerTimeControl', 'sendMail', 'WRITE'));
const isHimself = computed(() => user.value.id === Number(route.params.id));
const columns = computed(() => { const columns = computed(() => {
return weekdayStore.getLocales?.map((day, index) => { return weekdayStore.getLocales?.map((day, index) => {
@ -447,7 +449,7 @@ onMounted(async () => {
<div> <div>
<QBtnGroup push class="q-gutter-x-sm" flat> <QBtnGroup push class="q-gutter-x-sm" flat>
<QBtn <QBtn
v-if="isHimSelf && state" v-if="isHimself && state"
:label="t('Satisfied')" :label="t('Satisfied')"
color="primary" color="primary"
type="submit" type="submit"
@ -455,7 +457,7 @@ onMounted(async () => {
@click="isSatisfied()" @click="isSatisfied()"
/> />
<QBtn <QBtn
v-if="isHimSelf && state" v-if="isHimself && state"
:label="t('Not satisfied')" :label="t('Not satisfied')"
color="primary" color="primary"
type="submit" type="submit"
@ -466,14 +468,14 @@ onMounted(async () => {
</QBtnGroup> </QBtnGroup>
<QBtnGroup push class="q-gutter-x-sm q-ml-none" flat> <QBtnGroup push class="q-gutter-x-sm q-ml-none" flat>
<QBtn <QBtn
v-if="reason && state && (isHimSelf || isHr)" v-if="reason && state && (isHimself || isHr)"
:label="t('Reason')" :label="t('Reason')"
color="primary" color="primary"
type="submit" type="submit"
@click="showReasonForm()" @click="showReasonForm()"
/> />
<QBtn <QBtn
v-if="isHr && state !== 'CONFIRMED' && canResend" v-if="canSend && state !== 'CONFIRMED' && canResend"
:label="state ? t('Resend') : t('globals.send')" :label="state ? t('Resend') : t('globals.send')"
color="primary" color="primary"
type="submit" type="submit"
@ -603,7 +605,7 @@ onMounted(async () => {
<WorkerTimeReasonForm <WorkerTimeReasonForm
@on-submit="isUnsatisfied($event)" @on-submit="isUnsatisfied($event)"
:reason="reason" :reason="reason"
:is-him-self="isHimSelf" :is-himself="isHimself"
/> />
</QDialog> </QDialog>
</QPage> </QPage>

View File

@ -9,7 +9,7 @@ const $props = defineProps({
type: String, type: String,
default: '', default: '',
}, },
isHimSelf: { isHimself: {
type: Boolean, type: Boolean,
default: false, default: false,
}, },
@ -40,7 +40,7 @@ const closeForm = () => {
v-model="reasonFormData" v-model="reasonFormData"
type="textarea" type="textarea"
autogrow autogrow
:disable="!isHimSelf" :disable="!isHimself"
/> />
</template> </template>
</FormPopup> </FormPopup>

View File

@ -262,7 +262,7 @@ async function autofillBic(worker) {
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnLocation <VnLocation
:roles-allowed-to-create="['deliveryAssistant']" :acls="[{ model: 'Town', props: '*', accessType: 'WRITE' }]"
:options="postcodesOptions" :options="postcodesOptions"
v-model="data.location" v-model="data.location"
@update:model-value="(location) => handleLocation(data, location)" @update:model-value="(location) => handleLocation(data, location)"
@ -311,7 +311,7 @@ async function autofillBic(worker) {
option-label="name" option-label="name"
option-value="id" option-value="id"
hide-selected hide-selected
:roles-allowed-to-create="['salesAssistant', 'hr']" :acls="[{ model: 'BankEntity', props: '*', accessType: 'WRITE' }]"
:disable="data.isFreelance" :disable="data.isFreelance"
@update:model-value="autofillBic(data)" @update:model-value="autofillBic(data)"
:filter-options="['bic', 'name']" :filter-options="['bic', 'name']"

View File

@ -60,15 +60,12 @@ export default route(function (/* { store, ssrContext } */) {
await useTokenConfig().fetch(); await useTokenConfig().fetch();
} }
const matches = to.matched; const matches = to.matched;
const hasRequiredRoles = matches.every((route) => { const hasRequiredAcls = matches.every((route) => {
const meta = route.meta; const meta = route.meta;
if (meta && meta.roles) return useRole().hasAny(meta.roles); if (!meta?.acls) return true;
return true; return useAcl().hasAny(meta.acls);
}); });
if (!hasRequiredAcls) return next({ path: '/' });
if (!hasRequiredRoles) {
return next({ path: '/' });
}
} }
next(); next();

View File

@ -80,7 +80,7 @@ export default {
meta: { meta: {
title: 'accounts', title: 'accounts',
icon: 'accessibility', icon: 'accessibility',
roles: ['itManagement'], acls: [{ model: 'Account', props: '*', accessType: '*' }],
}, },
component: () => import('src/pages/Account/AccountAccounts.vue'), component: () => import('src/pages/Account/AccountAccounts.vue'),
}, },
@ -90,7 +90,7 @@ export default {
meta: { meta: {
title: 'ldap', title: 'ldap',
icon: 'account_tree', icon: 'account_tree',
roles: ['itManagement'], acls: [{ model: 'LdapConfig', props: '*', accessType: '*' }],
}, },
component: () => import('src/pages/Account/AccountLdap.vue'), component: () => import('src/pages/Account/AccountLdap.vue'),
}, },
@ -100,7 +100,7 @@ export default {
meta: { meta: {
title: 'samba', title: 'samba',
icon: 'preview', icon: 'preview',
roles: ['itManagement'], acls: [{ model: 'SambaConfig', props: '*', accessType: '*' }],
}, },
component: () => import('src/pages/Account/AccountSamba.vue'), component: () => import('src/pages/Account/AccountSamba.vue'),
}, },

View File

@ -62,7 +62,7 @@ export default {
meta: { meta: {
title: 'basicData', title: 'basicData',
icon: 'vn:settings', icon: 'vn:settings',
roles: ['salesPerson'], acls: [{ model: 'Claim', props: 'findById', accessType: 'READ' }],
}, },
component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'), component: () => import('src/pages/Claim/Card/ClaimBasicData.vue'),
}, },
@ -99,7 +99,13 @@ export default {
meta: { meta: {
title: 'development', title: 'development',
icon: 'vn:traceability', icon: 'vn:traceability',
roles: ['claimManager'], acls: [
{
model: 'ClaimDevelopment',
props: '*',
accessType: 'WRITE',
},
],
}, },
component: () => import('src/pages/Claim/Card/ClaimDevelopment.vue'), component: () => import('src/pages/Claim/Card/ClaimDevelopment.vue'),
}, },

View File

@ -84,7 +84,6 @@ export default {
meta: { meta: {
title: 'basicData', title: 'basicData',
icon: 'vn:settings', icon: 'vn:settings',
roles: ['salesPerson'],
}, },
component: () => component: () =>
import('src/pages/InvoiceIn/Card/InvoiceInBasicData.vue'), import('src/pages/InvoiceIn/Card/InvoiceInBasicData.vue'),

View File

@ -76,7 +76,6 @@ export default {
meta: { meta: {
title: 'basicData', title: 'basicData',
icon: 'vn:settings', icon: 'vn:settings',
roles: ['salesPerson'],
}, },
component: () => import('pages/Shelving/Card/ShelvingForm.vue'), component: () => import('pages/Shelving/Card/ShelvingForm.vue'),
}, },

View File

@ -54,7 +54,6 @@ export default {
meta: { meta: {
title: 'createTicket', title: 'createTicket',
icon: 'vn:ticketAdd', icon: 'vn:ticketAdd',
roles: ['developer'],
}, },
component: () => import('src/pages/Ticket/TicketCreate.vue'), component: () => import('src/pages/Ticket/TicketCreate.vue'),
}, },

View File

@ -2,7 +2,7 @@ import axios from 'axios';
import { ref } from 'vue'; import { ref } from 'vue';
import { defineStore } from 'pinia'; import { defineStore } from 'pinia';
import { toLowerCamel } from 'src/filters'; import { toLowerCamel } from 'src/filters';
import { useRole } from 'src/composables/useRole'; import { useAcl } from 'src/composables/useAcl';
import routes from 'src/router/modules'; import routes from 'src/router/modules';
export const useNavigationStore = defineStore('navigationStore', () => { export const useNavigationStore = defineStore('navigationStore', () => {
@ -26,7 +26,7 @@ export const useNavigationStore = defineStore('navigationStore', () => {
'zone', 'zone',
]; ];
const pinnedModules = ref([]); const pinnedModules = ref([]);
const role = useRole(); const acl = useAcl();
function getModules() { function getModules() {
const modulesRoutes = ref([]); const modulesRoutes = ref([]);
@ -64,7 +64,7 @@ export const useNavigationStore = defineStore('navigationStore', () => {
title: `globals.pageTitles.${title}`, title: `globals.pageTitles.${title}`,
})); }));
if (meta && meta.roles && role.hasAny(meta.roles) === false) return; if (meta && meta.acls && acl.hasAny(meta.acls) === false) return;
const item = { const item = {
name: route.name, name: route.name,

View File

@ -52,9 +52,9 @@ describe('Login', () => {
cy.url().should('contain', '/login'); cy.url().should('contain', '/login');
}); });
it(`should get redirected to dashboard since employee can't create tickets`, () => { it(`should be redirected to dashboard since the employee is not enabled to see ldap`, () => {
cy.visit('/#/ticket/create', { failOnStatusCode: false }); cy.visit('/#/account/ldap', { failOnStatusCode: false });
cy.url().should('contain', '/#/login?redirect=/ticket/create'); cy.url().should('contain', '/#/login?redirect=/account/ldap');
cy.get('input[aria-label="Username"]').type('employee'); cy.get('input[aria-label="Username"]').type('employee');
cy.get('input[aria-label="Password"]').type('nightmare'); cy.get('input[aria-label="Password"]').type('nightmare');
cy.get('button[type="submit"]').click(); cy.get('button[type="submit"]').click();

View File

@ -48,40 +48,62 @@ describe('useAcl', () => {
describe('hasAny', () => { describe('hasAny', () => {
it('should return false if no roles matched', async () => { it('should return false if no roles matched', async () => {
expect(acl.hasAny('Worker', 'updateAttributes', 'WRITE')).toBeFalsy(); expect(
acl.hasAny([
{ model: 'Worker', props: 'updateAttributes', accessType: 'WRITE' },
])
).toBeFalsy();
}); });
it('should return false if no roles matched', async () => { it('should return false if no roles matched', async () => {
expect(acl.hasAny('Worker', 'holidays', 'READ')).toBeTruthy(); expect(
acl.hasAny([{ model: 'Worker', props: 'holidays', accessType: 'READ' }])
).toBeTruthy();
}); });
describe('*', () => { describe('*', () => {
it('should return true if an acl matched', async () => { it('should return true if an acl matched', async () => {
expect(acl.hasAny('Address', '*', 'WRITE')).toBeTruthy(); expect(
acl.hasAny([{ model: 'Address', props: '*', accessType: 'WRITE' }])
).toBeTruthy();
}); });
it('should return false if no acls matched', async () => { it('should return false if no acls matched', async () => {
expect(acl.hasAny('Worker', '*', 'READ')).toBeFalsy(); expect(
acl.hasAny([{ model: 'Worker', props: '*', accessType: 'READ' }])
).toBeFalsy();
}); });
}); });
describe('$authenticated', () => { describe('$authenticated', () => {
it('should return false if no acls matched', async () => { it('should return false if no acls matched', async () => {
expect(acl.hasAny('Url', 'getByUser', '*')).toBeFalsy(); expect(
acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: '*' }])
).toBeFalsy();
}); });
it('should return true if an acl matched', async () => { it('should return true if an acl matched', async () => {
expect(acl.hasAny('Url', 'getByUser', 'READ')).toBeTruthy(); expect(
acl.hasAny([{ model: 'Url', props: 'getByUser', accessType: 'READ' }])
).toBeTruthy();
}); });
}); });
describe('$everyone', () => { describe('$everyone', () => {
it('should return false if no acls matched', async () => { it('should return false if no acls matched', async () => {
expect(acl.hasAny('TpvTransaction', 'start', 'READ')).toBeFalsy(); expect(
acl.hasAny([
{ model: 'TpvTransaction', props: 'start', accessType: 'READ' },
])
).toBeFalsy();
}); });
it('should return false if an acl matched', async () => { it('should return false if an acl matched', async () => {
expect(acl.hasAny('TpvTransaction', 'start', 'WRITE')).toBeTruthy(); expect(
acl.hasAny([
{ model: 'TpvTransaction', props: 'start', accessType: 'WRITE' },
])
).toBeTruthy();
}); });
}); });
}); });