0
0
Fork 0

feat: refs #6897 add success messages for entry lock and improve data attributes for better testing

This commit is contained in:
Pablo Natek 2025-02-09 18:24:41 +01:00
parent e121cc5d5c
commit b37923e194
21 changed files with 340 additions and 70 deletions

View File

@ -181,6 +181,7 @@ const selectTravel = ({ id }) => {
color="primary"
:disabled="isLoading"
:loading="isLoading"
data-cy="save-filter-travel-form"
/>
</div>
<QTable
@ -191,9 +192,10 @@ const selectTravel = ({ id }) => {
:no-data-label="t('Enter a new search')"
class="q-mt-lg"
@row-click="(_, row) => selectTravel(row)"
data-cy="table-filter-travel-form"
>
<template #body-cell-id="{ row }">
<QTd auto-width @click.stop>
<QTd auto-width @click.stop data-cy="travelFk-travel-form">
<QBtn flat color="blue">{{ row.id }}</QBtn>
<TravelDescriptorProxy :id="row.id" />
</QTd>

View File

@ -26,6 +26,7 @@ const itemComputed = computed(() => {
:to="{ name: itemComputed.name }"
clickable
v-ripple
:data-cy="`${itemComputed.name}-menu-item`"
>
<QItemSection avatar v-if="itemComputed.icon">
<QIcon :name="itemComputed.icon" />

View File

@ -28,7 +28,6 @@ const hover = ref();
const arrayData = useArrayData($props.dataKey, { searchUrl: $props.searchUrl });
async function orderBy(name, direction) {
console.log('orderBy');
if (!name) return;
switch (direction) {
case 'DESC':
@ -41,11 +40,8 @@ async function orderBy(name, direction) {
direction = 'DESC';
break;
}
console.log('name: ', name);
if (!direction) return await arrayData.deleteOrder(name);
console.log('direction: ', direction);
console.log('name: ', name);
await arrayData.addOrder(name, direction);
}

View File

@ -133,6 +133,9 @@ const $props = defineProps({
type: Boolean,
default: false,
},
createComplement: {
type: Object,
},
});
const { t } = useI18n();
const stateStore = useStateStore();
@ -920,7 +923,7 @@ const checkbox = ref(null);
v-model="showForm"
transition-show="scale"
transition-hide="scale"
:full-width="create?.isFullWidth ?? false"
:full-width="createComplement?.isFullWidth ?? false"
@before-hide="
() => {
if (createRef.isSaveAndContinue) {
@ -929,6 +932,7 @@ const checkbox = ref(null);
}
}
"
data-cy="vn-table-create-dialog"
>
<FormModelPopup
ref="createRef"
@ -937,11 +941,11 @@ const checkbox = ref(null);
@on-data-saved="(_, res) => createForm.onDataSaved(res)"
>
<template #form-inputs="{ data }">
<div :class="create?.containerClass">
<div :style="createComplement?.containerStyle">
<div>
<slot name="previous-create-dialog" :data="data" />
</div>
<div class="grid-create" :style="create?.columnGridStyle">
<div class="grid-create" :style="createComplement?.columnGridStyle">
<slot
v-for="column of splittedColumns.create"
:key="column.name"
@ -957,6 +961,7 @@ const checkbox = ref(null);
v-model="data[column.name]"
:show-label="true"
component-prop="columnCreate"
:data-cy="`${column.name}-create-popup`"
/>
</slot>
<slot name="more-create-dialog" :data="data" />
@ -1042,11 +1047,7 @@ es:
grid-gap: 20px;
margin: 0 auto;
}
.form-container {
display: flex;
flex-wrap: wrap;
gap: 16px; /* Espacio entre los divs */
}
.flex-one {
display: flex;
flex-flow: row wrap;

View File

@ -28,6 +28,7 @@ const $props = defineProps({
hide-selected
:required="true"
action-icon="filter_alt"
:roles-allowed-to-create="['buyer']"
>
<template #form>
<FilterTravelForm @travel-selected="onFilterTravelSelected(data, $event)" />

View File

@ -73,7 +73,7 @@ onBeforeMount(async () => {
() => [$props.url, $props.filter],
async () => {
if (!isSameDataKey.value) await getData();
}
},
);
});
@ -108,7 +108,7 @@ const iconModule = computed(() => route.matched[1].meta.icon);
const toModule = computed(() =>
route.matched[1].path.split('/').length > 2
? route.matched[1].redirect
: route.matched[1].children[0].redirect
: route.matched[1].children[0].redirect,
);
</script>

View File

@ -82,7 +82,7 @@ function cancel() {
@click="cancel()"
/>
</QCardSection>
<QCardSection class="q-pb-none">
<QCardSection class="q-pb-none" data-cy="VnConfirm_message">
<span v-if="message !== false" v-html="message" />
</QCardSection>
<QCardSection class="row items-center q-pt-none">
@ -95,6 +95,7 @@ function cancel() {
:disable="isLoading"
flat
@click="cancel()"
data-cy="VnConfirm_cancel"
/>
<QBtn
:label="t('globals.confirm')"

View File

@ -11,7 +11,7 @@
<QTooltip>
{{ $t('components.cardDescriptor.moreOptions') }}
</QTooltip>
<QMenu ref="menuRef">
<QMenu ref="menuRef" data-cy="descriptor-more-opts-menu">
<QList>
<slot name="menu" :menu-ref="$refs.menuRef" />
</QList>

View File

@ -19,15 +19,17 @@ export async function checkEntryLock(entryFk, userFk) {
const entryConfig = await axios.get('EntryConfigs/findOne');
if (data?.lockerUserFk && data?.locked) {
const now = new Date().getTime();
const now = new Date(Date.vnNow()).getTime();
const lockedTime = new Date(data.locked).getTime();
const timeDiff = (now - lockedTime) / 1000;
const isMaxTimeLockExceeded = entryConfig.data.maxLockTime > timeDiff;
if (data?.lockerUserFk !== userFk && isMaxTimeLockExceeded) {
quasar
.dialog({
component: VnConfirm,
componentProps: {
'data-cy': 'entry-lock-confirm',
title: t('entry.lock.title'),
message: t('entry.lock.message', {
userName: data?.user?.nickname,
@ -56,7 +58,7 @@ export async function checkEntryLock(entryFk, userFk) {
quasar.notify({
message: t('entry.lock.success'),
color: 'positive',
position: 'top',
group: false,
}),
);
}

View File

@ -93,8 +93,7 @@ onMounted(() => {
<VnInputNumber
:label="t('entry.summary.commission')"
v-model="data.commission"
step="1"
autofocus
:step="1"
:positive="false"
/>
<VnSelect

View File

@ -214,7 +214,6 @@ const columns = [
{
align: 'center',
labelAbbreviation: 'GM',
label: t('Grouping selector'),
toolTip: t('Grouping selector'),
name: 'groupingMode',
component: 'toggle',
@ -471,7 +470,8 @@ async function beforeSave(data, getChanges) {
function invertQuantitySign(rows, sign) {
for (const row of rows) {
row.quantity = row.quantity * sign;
if (sign > 0) row.quantity = Math.abs(row.quantity);
else if (row.quantity > 0) row.quantity = -row.quantity;
}
}
function setIsChecked(rows, value) {
@ -517,18 +517,27 @@ onMounted(() => {
flat
:title="t('Invert quantity value')"
:disable="!selectedRows.length"
data-cy="change-quantity-sign"
>
<QList>
<QItem>
<QItemSection>
<QBtn flat @click="invertQuantitySign(selectedRows, -1)">
<QBtn
flat
@click="invertQuantitySign(selectedRows, -1)"
data-cy="set-negative-quantity"
>
<span style="font-size: medium">-1</span>
</QBtn>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QBtn flat @click="invertQuantitySign(selectedRows, 1)">
<QBtn
flat
@click="invertQuantitySign(selectedRows, 1)"
data-cy="set-positive-quantity"
>
<span style="font-size: medium">1</span>
</QBtn>
</QItemSection>
@ -541,6 +550,7 @@ onMounted(() => {
flat
:title="t('Check buy amount')"
:disable="!selectedRows.length"
data-cy="check-buy-amount"
>
<QTooltip>{{}}</QTooltip>
<QList>
@ -550,6 +560,7 @@ onMounted(() => {
icon="check"
flat
@click="setIsChecked(selectedRows, true)"
data-cy="check-amount"
/>
</QItemSection>
</QItem>
@ -559,6 +570,7 @@ onMounted(() => {
icon="close"
flat
@click="setIsChecked(selectedRows, false)"
data-cy="uncheck-amount"
/>
</QItemSection>
</QItem>
@ -598,16 +610,25 @@ onMounted(() => {
entryBuysRef.reload();
},
formInitialData: { entryFk: entityId, isIgnored: false },
isFullWidth: true,
containerClass: 'form-container',
showSaveAndContinueBtn: true,
columnGridStyle: {
'max-width': '50%',
flex: 1,
},
}
: null
"
:create-complement="{
isFullWidth: true,
containerStyle: {
display: 'flex',
'flex-wrap': 'wrap',
gap: '16px',
position: 'relative',
height: '450px',
},
columnGridStyle: {
'max-width': '50%',
flex: 1,
'margin-right': '30px',
},
}"
:is-editable="editableMode"
:without-header="!editableMode"
:with-filters="editableMode"
@ -642,22 +663,23 @@ onMounted(() => {
</template>
<template #column-footer-stickers>
<div>
<span style="color: var(--vn-label-color)">{{
footer?.printedStickers
}}</span>
<span>/{{ footer?.stickers }}</span>
<span style="color: var(--vn-label-color)">
{{ footer?.printedStickers }}</span
>
<span>/</span>
<span data-cy="footer-stickers">{{ footer?.stickers }}</span>
</div>
</template>
<template #column-footer-weight>
{{ footer?.weight }}
</template>
<template #column-footer-quantity>
<span :style="getQuantityStyle(footer)">
<span :style="getQuantityStyle(footer)" data-cy="footer-quantity">
{{ footer?.quantity }}
</span>
</template>
<template #column-footer-amount>
<span :style="getAmountStyle(footer)">
<span :style="getAmountStyle(footer)" data-cy="footer-amount">
{{ footer?.amount }}
</span>
</template>
@ -675,6 +697,7 @@ onMounted(() => {
}
"
:required="true"
data-cy="itemFk-create-popup"
/>
</template>
<template #column-create-groupingMode="{ data }">
@ -689,7 +712,9 @@ onMounted(() => {
/>
</template>
<template #previous-create-dialog="{ data }">
<ItemDescriptor :id="data.itemFk" />
<div style="position: absolute">
<ItemDescriptor :id="data.itemFk ?? NaN" />
</div>
</template>
</VnTable>
</template>

View File

@ -94,25 +94,53 @@ const getEntryRedirectionFilter = (entry) => {
function showEntryReport() {
openReport(`Entries/${entityId.value}/entry-order-pdf`);
}
async function recalculateRates(entity) {
const entryConfig = await axios.get('EntryConfigs/findOne');
if (entryConfig.data?.inventorySupplierFk === entity.supplierFk) {
quasar.notify({
type: 'negative',
message: t('Cannot recalculate prices because this is an inventory entry'),
});
return;
}
await axios.post(`Entries/${entityId.value}/recalcEntryPrices`);
function showNotification(type, message) {
quasar.notify({
type: type,
message: t(message),
});
}
async function recalculateRates(entity) {
try {
const entryConfig = await axios.get('EntryConfigs/findOne');
if (entryConfig.data?.inventorySupplierFk === entity.supplierFk) {
showNotification(
'negative',
'Cannot recalculate prices because this is an inventory entry',
);
return;
}
await axios.post(`Entries/${entityId.value}/recalcEntryPrices`);
showNotification('positive', 'Entry prices recalculated');
} catch (error) {
showNotification('negative', 'Failed to recalculate rates');
console.error(error);
}
}
async function cloneEntry() {
await axios
.post(`Entries/${entityId.value}/cloneEntry`)
.then((response) => push(`/entry/${response.data[0].vNewEntryFk}`));
try {
const response = await axios.post(`Entries/${entityId.value}/cloneEntry`);
push({ path: `/entry/${response.data[0].vNewEntryFk}` });
showNotification('positive', 'Entry cloned');
} catch (error) {
showNotification('negative', 'Failed to clone entry');
console.error(error);
}
}
async function deleteEntry() {
await axios.post(`Entries/${entityId.value}/deleteEntry`).then(() => push(`/entry/`));
try {
await axios.post(`Entries/${entityId.value}/deleteEntry`);
push({ path: `/entry/list` });
showNotification('positive', 'Entry deleted');
} catch (error) {
showNotification('negative', 'Failed to delete entry');
console.error(error);
}
}
</script>
@ -146,7 +174,13 @@ async function deleteEntry() {
<QItem v-ripple clickable @click="cloneEntry(entity)" data-cy="clone-entry">
<QItemSection>{{ t('Clone') }}</QItemSection>
</QItem>
<QItem v-ripple clickable @click="deleteEntry(entity)" data-cy="delete-entry">
<QItem
v-ripple
clickable
@click="deleteEntry(entity)"
data-cy="delete-entry"
v-if="entity?.travelFk"
>
<QItemSection>{{ t('Delete') }}</QItemSection>
</QItem>
</template>
@ -253,4 +287,10 @@ es:
landed: Recibido
This entry is deleted: Esta entrada está eliminada
Cannot recalculate prices because this is an inventory entry: No se pueden recalcular los precios porque es una entrada de inventario
Entry deleted: Entrada eliminada
Entry cloned: Entrada clonada
Entry prices recalculated: Precios de la entrada recalculados
Failed to recalculate rates: No se pudieron recalcular las tarifas
Failed to clone entry: No se pudo clonar la entrada
Failed to delete entry: No se pudo eliminar la entrada
</i18n>

View File

@ -176,9 +176,8 @@ onMounted(async () => {
/>
<EntryBuys
v-if="entityId"
:id="entityId"
:id="Number(entityId)"
:editable-mode="false"
:isEditable="false"
table-height="49vh"
/>
</QCard>

View File

@ -263,12 +263,12 @@ onBeforeMount(async () => {
<template>
<VnSection
:data-key="dataKey"
:columns="columns"
prefix="entry"
url="Entries/filter"
:array-data-props="{
url: 'Entries/filter',
userFilter: entryQueryFilter,
order: 'landed DESC',
userFilter: EntryFilter,
}"
>
<template #advanced-menu>
@ -281,6 +281,7 @@ onBeforeMount(async () => {
:data-key="dataKey"
url="Entries/filter"
:filter="entryQueryFilter"
order="landed DESC"
:create="{
urlCreate: 'Entries',
title: t('Create entry'),

View File

@ -2,6 +2,7 @@ entry:
lock:
title: Lock entry
message: This entry has been locked by {userName} for {time} minutes. Do you want to unlock it?
success: The entry has been locked successfully
list:
newEntry: New entry
tableVisibleColumns:

View File

@ -2,7 +2,7 @@ entry:
lock:
title: Entrada bloqueada
message: Esta entrada ha sido bloqueada por {userName} hace {time} minutos. ¿Quieres desbloquearla?
success: La entrada ha sido bloqueada correctamente
list:
newEntry: Nueva entrada
tableVisibleColumns:

View File

@ -121,7 +121,7 @@ const updateStock = async () => {
<template #value>
<span class="link">
{{ entity.itemType?.worker?.user?.name }}
<WorkerDescriptorProxy :id="entity.itemType?.worker?.id" />
<WorkerDescriptorProxy :id="entity.itemType?.worker?.id ?? NaN" />
</span>
</template>
</VnLv>

View File

@ -260,7 +260,7 @@ async function getZone(options) {
auto-load
/>
<QForm>
<VnRow>
<VnRow class="row q-gutter-md q-mb-md no-wrap">
<VnSelect
:label="t('ticketList.client')"
v-model="clientId"
@ -296,7 +296,7 @@ async function getZone(options) {
:rules="validate('ticketList.warehouse')"
/>
</VnRow>
<VnRow>
<VnRow class="row q-gutter-md q-mb-md no-wrap">
<VnSelect
:label="t('basicData.address')"
v-model="addressId"

View File

@ -109,6 +109,7 @@ export default {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/Entry/EntryList.vue'),
},
entryCard,
],

View File

@ -4,15 +4,215 @@ describe('Entry', () => {
cy.login('buyer');
cy.visit(`/#/entry/list`);
});
it('Filter deleted entries and other fields', () => {
cy.get('button[data-cy="vnTableCreateBtn"]').click();
cy.get('input[data-cy="entry-travel-select"]').type('1{enter}');
cy.get('button[data-cy="descriptor-more-opts"]').click();
cy.get('div[data-cy="delete-entry"]').click();
cy.visit(`/#/entry/list`);
createEntry();
cy.get('.q-notification__message').eq(0).should('have.text', 'Data created');
deleteEntry();
cy.typeSearchbar('{enter}');
cy.get('span[title="Date"]').click();
cy.get('span[title="Date"]').click();
cy.get('td[data-row-index="0"][data-col-field="landed"]').contains('-');
cy.get('span[title="Date"]').click().click();
cy.typeSearchbar('{enter}');
cy.url().should('include', 'order');
cy.get('td[data-row-index="0"][data-col-field="landed"]').should(
'have.text',
'-',
);
});
it('Create entry, modify travel and add buys', () => {
createEntryAndBuy();
cy.get('a[data-cy="EntryBasicData-menu-item"]').click();
selectTravel('two');
cy.saveCard();
cy.get('.q-notification__message').eq(0).should('have.text', 'Data created');
deleteEntry();
});
it('Clone entry and recalculate rates', () => {
createEntry();
cy.get('.q-notification__message').eq(0).should('have.text', 'Data created');
cy.url().then((perviousUrl) => {
cy.log('URL antes de clonar:', perviousUrl);
cy.get('[data-cy="descriptor-more-opts"]').click();
cy.get('div[data-cy="clone-entry"]').click();
cy.url().then((newUrl) => {
expect(perviousUrl).not.to.eq(newUrl);
cy.get('[data-cy="descriptor-more-opts"]').click();
cy.get('div[data-cy="recalculate-rates"]').click();
cy.get('.q-notification__message')
.eq(1)
.should('have.text', 'Entry prices recalculated');
deleteEntry();
cy.visit(perviousUrl);
deleteEntry();
});
});
});
it('Should notify when entry is lock by another user', () => {
const checkLockMessage = () => {
cy.get('[data-cy="entry-lock-confirm"]').should('be.visible');
cy.get('[data-cy="VnConfirm_message"] > span').should(
'contain.text',
'This entry has been locked by buyerNick',
);
};
createEntry();
goToEntryBuys();
cy.get('.q-notification__message')
.eq(1)
.should('have.text', 'The entry has been locked successfully');
cy.login('logistic');
cy.reload();
checkLockMessage();
cy.get('[data-cy="VnConfirm_cancel"]').click();
cy.url().should('include', 'summary');
goToEntryBuys();
checkLockMessage();
cy.get('[data-cy="VnConfirm_confirm"]').click();
cy.url().should('include', 'buys');
deleteEntry();
});
it('Edit buys and use toolbar actions', () => {
const COLORS = {
negative: 'rgb(251, 82, 82)',
positive: 'rgb(200, 228, 132)',
enabled: 'rgb(255, 255, 255)',
disable: 'rgb(168, 168, 168)',
};
const selectCell = (field, row = 0) =>
cy.get(`td[data-col-field="${field}"][data-row-index="${row}"]`);
const selectSpan = (field, row = 0) => selectCell(field, row).find('div > span');
const selectButton = (cySelector) => cy.get(`button[data-cy="${cySelector}"]`);
const clickAndType = (field, value, row = 0) =>
selectCell(field, row).click().type(value);
const checkText = (field, expectedText, row = 0) =>
selectCell(field, row).should('have.text', expectedText);
const checkColor = (field, expectedColor, row = 0) =>
selectSpan(field, row).should('have.css', 'color', expectedColor);
createEntryAndBuy();
selectCell('isIgnored')
.click()
.click()
.trigger('keydown', { key: 'Tab', keyCode: 9, which: 9 });
checkText('isIgnored', 'check');
checkColor('quantity', COLORS.negative);
clickAndType('stickers', '1');
checkText('quantity', '11');
checkText('amount', '550');
clickAndType('packing', '2');
checkText('packing', '12close');
checkText('weight', '12');
checkText('quantity', '132');
checkText('amount', '6600');
checkColor('packing', COLORS.enabled);
selectCell('groupingMode').click().click().click();
checkColor('packing', COLORS.disable);
checkColor('grouping', COLORS.enabled);
selectCell('buyingValue').click().clear().type('{backspace}{backspace}1');
checkText('amount', '132');
checkColor('minPrice', COLORS.disable);
selectCell('hasMinPrice').click().click();
checkColor('minPrice', COLORS.enabled);
selectCell('hasMinPrice').click();
cy.saveCard();
cy.get('span[data-cy="footer-stickers"]').should('have.text', '11');
cy.get('.q-notification__message').contains('Data saved');
selectButton('change-quantity-sign').should('be.disabled');
selectButton('check-buy-amount').should('be.disabled');
cy.get('tr.cursor-pointer > .q-table--col-auto-width > .q-checkbox').click();
selectButton('change-quantity-sign').should('be.enabled');
selectButton('check-buy-amount').should('be.enabled');
selectButton('change-quantity-sign').click();
selectButton('set-negative-quantity').click();
checkText('quantity', '-132');
selectButton('set-positive-quantity').click();
checkText('quantity', '132');
checkColor('amount', COLORS.disable);
selectButton('check-buy-amount').click();
selectButton('uncheck-amount').click();
checkColor('amount', COLORS.disable);
selectButton('check-amount').click();
checkColor('amount', COLORS.positive);
cy.saveCard();
cy.get('span[data-cy="footer-amount"]').should(
'have.css',
'color',
COLORS.positive,
);
deleteEntry();
});
function goToEntryBuys() {
const entryBuySelector = 'a[data-cy="EntryBuys-menu-item"]';
cy.get(entryBuySelector).should('be.visible');
cy.get(entryBuySelector).click();
cy.get(entryBuySelector).click();
}
function deleteEntry() {
cy.get('[data-cy="descriptor-more-opts"]').click();
cy.get('div[data-cy="delete-entry"]').click();
cy.url().should('include', 'list');
}
function createEntryAndBuy() {
createEntry();
createBuy();
}
function createEntry() {
cy.get('button[data-cy="vnTableCreateBtn"]').click();
selectTravel('one');
cy.get('button[data-cy="FormModelPopup_save"]').click();
cy.url().should('include', 'summary');
cy.get('.q-notification__message').eq(0).should('have.text', 'Data created');
}
function selectTravel(warehouse) {
cy.get('i[data-cy="Travel_icon"]').click();
cy.get('input[data-cy="Warehouse Out_select"]').type(warehouse);
cy.get('div[role="listbox"] > div > div[role="option"]').eq(0).click();
cy.get('button[data-cy="save-filter-travel-form"]').click();
cy.get('tr').eq(1).click();
}
function createBuy() {
cy.get('a[data-cy="EntryBuys-menu-item"]').click();
cy.get('a[data-cy="EntryBuys-menu-item"]').click();
cy.get('button[data-cy="vnTableCreateBtn"]').click();
cy.get('input[data-cy="itemFk-create-popup"]').type('1');
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.get('button[data-cy="FormModelPopup_save"]').click();
}
});

View File

@ -1,7 +1,7 @@
const waitUntil = (subject, checkFunction, originalOptions = {}) => {
if (!(checkFunction instanceof Function)) {
throw new Error(
'`checkFunction` parameter should be a function. Found: ' + checkFunction
'`checkFunction` parameter should be a function. Found: ' + checkFunction,
);
}