Merge branch 'dev' into 8684-itemRefactorAndE2e
gitea/salix-front/pipeline/pr-dev This commit is unstable Details

This commit is contained in:
Pablo Natek 2025-05-19 13:55:21 +00:00
commit cc016a4a8c
26 changed files with 953 additions and 461 deletions

View File

@ -0,0 +1,184 @@
<script setup>
import { onMounted, watch, computed, ref, useAttrs } from 'vue';
import { date } from 'quasar';
import VnDate from './VnDate.vue';
import { useRequired } from 'src/composables/useRequired';
const $attrs = useAttrs();
const { isRequired, requiredFieldRule } = useRequired($attrs);
const $props = defineProps({
isOutlined: {
type: Boolean,
default: false,
},
isPopupOpen: {
type: Boolean,
default: false,
},
multiple: {
type: Boolean,
default: true,
},
});
const model = defineModel({
type: [String, Date, Array],
default: null,
});
const vnInputDateRef = ref(null);
const dateFormat = 'DD/MM/YYYY';
const isPopupOpen = ref();
const hover = ref();
const mask = ref();
const mixinRules = [requiredFieldRule, ...($attrs.rules ?? [])];
const formattedDate = computed({
get() {
if (!model.value) return model.value;
if ($props.multiple) {
return model.value
.map((d) => date.formatDate(new Date(d), dateFormat))
.join(', ');
}
return date.formatDate(new Date(model.value), dateFormat);
},
set(value) {
if (value == model.value) return;
if ($props.multiple) return; // No permitir edición manual en modo múltiple
let newDate;
if (value) {
// parse input
if (value.includes('/') && value.length >= 10) {
if (value.at(2) == '/') value = value.split('/').reverse().join('/');
value = date.formatDate(
new Date(value).toISOString(),
'YYYY-MM-DDTHH:mm:ss.SSSZ',
);
}
const [year, month, day] = value.split('-').map((e) => parseInt(e));
newDate = new Date(year, month - 1, day);
if (model.value && !$props.multiple) {
const orgDate =
model.value instanceof Date ? model.value : new Date(model.value);
newDate.setHours(
orgDate.getHours(),
orgDate.getMinutes(),
orgDate.getSeconds(),
orgDate.getMilliseconds(),
);
}
}
if (!isNaN(newDate)) model.value = newDate.toISOString();
},
});
const popupDate = computed(() => {
if (!model.value) return model.value;
if ($props.multiple) {
return model.value.map((d) => date.formatDate(new Date(d), 'YYYY/MM/DD'));
}
return date.formatDate(new Date(model.value), 'YYYY/MM/DD');
});
onMounted(() => {
// fix quasar bug
mask.value = '##/##/####';
if ($props.multiple && !model.value) {
model.value = [];
}
});
watch(
() => model.value,
(val) => (formattedDate.value = val),
{ immediate: true },
);
const styleAttrs = computed(() => {
return $props.isOutlined
? {
dense: true,
outlined: true,
rounded: true,
}
: {};
});
const manageDate = (dates) => {
if ($props.multiple) {
model.value = dates.map((d) => new Date(d).toISOString());
} else {
formattedDate.value = dates;
}
if ($props.isPopupOpen) isPopupOpen.value = false;
};
</script>
<template>
<div @mouseover="hover = true" @mouseleave="hover = false">
<QInput
ref="vnInputDateRef"
v-model="formattedDate"
class="vn-input-date"
:mask="$props.multiple ? undefined : mask"
placeholder="dd/mm/aaaa"
v-bind="{ ...$attrs, ...styleAttrs }"
:class="{ required: isRequired }"
:rules="mixinRules"
:clearable="false"
@click="isPopupOpen = !isPopupOpen"
@keydown="isPopupOpen = false"
hide-bottom-space
:data-cy="($attrs['data-cy'] ?? $attrs.label) + '_inputDate'"
>
<template #append>
<QIcon
name="close"
size="xs"
v-if="
($attrs.clearable == undefined || $attrs.clearable) &&
hover &&
model &&
!$attrs.disable
"
@click="
vnInputDateRef.focus();
model = null;
isPopupOpen = false;
"
/>
</template>
<QMenu
v-if="$q.screen.gt.xs"
transition-show="scale"
transition-hide="scale"
v-model="isPopupOpen"
anchor="bottom left"
self="top start"
:no-focus="true"
:no-parent-event="true"
>
<VnDate
v-model="popupDate"
@update:model-value="manageDate"
:multiple="multiple"
/>
</QMenu>
<QDialog v-else v-model="isPopupOpen">
<VnDate
v-model="popupDate"
@update:model-value="manageDate"
:multiple="multiple"
/>
</QDialog>
</QInput>
</div>
</template>
<i18n>
es:
Open date: Abrir fecha
</i18n>

View File

@ -1,5 +1,5 @@
<script setup>
import { toPercentage } from 'filters/index';
import { toCurrency, toPercentage } from 'filters/index';
import { computed } from 'vue';
@ -8,6 +8,10 @@ const props = defineProps({
type: Number,
required: true,
},
format: {
type: String,
default: 'percentage', // 'currency'
},
});
const valueClass = computed(() =>
@ -21,7 +25,10 @@ const formattedValue = computed(() => props.value);
<template>
<span :class="valueClass">
<QIcon :name="iconName" size="sm" class="value-icon" />
{{ toPercentage(formattedValue) }}
<span v-if="$props.format === 'percentage'">{{
toPercentage(formattedValue)
}}</span>
<span v-if="$props.format === 'currency'">{{ toCurrency(formattedValue) }}</span>
</span>
</template>

View File

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

View File

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

View File

@ -55,7 +55,6 @@ const filterBanks = {
fields: ['id', 'bank', 'accountingTypeFk'],
include: { relation: 'accountingType' },
order: 'id',
limit: 30,
};
const filterClientFindOne = {
@ -200,7 +199,6 @@ async function getAmountPaid() {
option-label="bank"
:include="{ relation: 'accountingType' }"
sort-by="id"
:limit="0"
@update:model-value="
(value, options) => setPaymentType(data, value, options)
"

View File

@ -8,10 +8,11 @@ import VnTable from 'src/components/VnTable/VnTable.vue';
import axios from 'axios';
import { displayResults } from 'src/pages/Ticket/Negative/composables/notifyResults';
import FetchData from 'components/FetchData.vue';
import { useState } from 'src/composables/useState';
import useNotify from 'src/composables/useNotify.js';
const MATCH = 'match';
const { notifyResults } = displayResults();
const { notify } = useNotify();
const { t } = useI18n();
const $props = defineProps({
@ -42,7 +43,7 @@ const ticketConfig = ref({});
const proposalTableRef = ref(null);
const sale = computed(() => $props.sales[0]);
const saleFk = computed(() => sale.value.saleFk);
const saleFk = computed(() => sale.value?.saleFk);
const filter = computed(() => ({
where: $props.filter,
@ -56,8 +57,24 @@ const defaultColumnAttrs = {
};
const emit = defineEmits(['onDialogClosed', 'itemReplaced']);
const conditionalValuePrice = (price) =>
price > 1 + ticketConfig.value.lackAlertPrice / 100 ? 'match' : 'not-match';
const priceStatusClass = (proposalPrice) => {
const originalPrice = sale.value?.price;
if (
!originalPrice ||
!ticketConfig.value ||
typeof ticketConfig.value.lackAlertPrice !== 'number'
) {
return 'price-ok';
}
const priceIncreasePercentage =
((proposalPrice - originalPrice) / originalPrice) * 100;
return priceIncreasePercentage > ticketConfig.value.lackAlertPrice
? 'price-alert'
: 'price-ok';
};
const columns = computed(() => [
{
@ -97,7 +114,15 @@ const columns = computed(() => [
{
align: 'left',
sortable: true,
label: t('item.list.color'),
label: t('item.list.producer'),
name: 'subName',
field: 'subName',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('proposal.tag5'),
name: 'tag5',
field: 'value5',
columnClass: 'expand',
@ -105,7 +130,7 @@ const columns = computed(() => [
{
align: 'left',
sortable: true,
label: t('item.list.stems'),
label: t('proposal.tag6'),
name: 'tag6',
field: 'value6',
columnClass: 'expand',
@ -113,12 +138,27 @@ const columns = computed(() => [
{
align: 'left',
sortable: true,
label: t('item.list.producer'),
label: t('proposal.tag7'),
name: 'tag7',
field: 'value7',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('proposal.tag8'),
name: 'tag8',
field: 'value8',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('proposal.advanceable'),
name: 'advanceable',
field: 'advanceable',
columnClass: 'expand',
},
{
...defaultColumnAttrs,
label: t('proposal.price2'),
@ -169,14 +209,14 @@ function extractMatchValues(obj) {
.filter((key) => key.startsWith(MATCH))
.map((key) => parseInt(key.replace(MATCH, ''), 10));
}
const gradientStyle = (value) => {
const gradientStyleClass = (row) => {
let color = 'white';
const perc = parseFloat(value);
const value = parseFloat(row);
switch (true) {
case perc >= 0 && perc < 33:
case value >= 0 && value < 33:
color = 'primary';
break;
case perc >= 33 && perc < 66:
case value >= 33 && value < 66:
color = 'warning';
break;
@ -193,52 +233,63 @@ const statusConditionalValue = (row) => {
};
const isSelectionAvailable = (itemProposal) => {
const { price2 } = itemProposal;
const { price2, available } = itemProposal;
const salePrice = sale.value.price;
const byPrice = (100 * price2) / salePrice > ticketConfig.value.lackAlertPrice;
if (byPrice) {
return byPrice;
const { lackAlertPrice } = ticketConfig.value;
const isPriceTooHigh = (100 * price2) / salePrice > lackAlertPrice;
if (isPriceTooHigh) {
return isPriceTooHigh;
}
const byQuantity =
(100 * itemProposal.available) / Math.abs($props.itemLack.lack) <
ticketConfig.value.lackAlertPrice;
return byQuantity;
const hasEnoughQuantity =
(100 * available) / Math.abs($props.itemLack.lack) < lackAlertPrice;
return hasEnoughQuantity;
};
async function change({ itemFk: substitutionFk }) {
try {
const promises = $props.sales.map(({ saleFk, quantity }) => {
const params = {
saleFk,
substitutionFk,
quantity,
};
return axios.post('Sales/replaceItem', params);
});
const results = await Promise.allSettled(promises);
notifyResults(results, 'saleFk');
emit('itemReplaced', {
type: 'refresh',
quantity: quantity.value,
itemProposal: proposalSelected.value[0],
});
proposalSelected.value = [];
} catch (error) {
console.error(error);
async function change(itemSubstitution) {
if (!isSelectionAvailable(itemSubstitution)) {
notify(t('notAvailable'), 'warning');
return;
}
const { itemFk: substitutionFk } = itemSubstitution;
let body;
const promises = $props.sales.map(({ saleFk, quantity, ticketFk }) => {
body = {
saleFk,
substitutionFk,
quantity,
ticketFk,
};
return axios.post('Sales/replaceItem', body);
});
const results = await Promise.allSettled(promises);
notifyResults(results, 'ticketFk');
emit('itemReplaced', {
...body,
type: 'refresh',
itemProposal: proposalSelected.value[0],
});
proposalSelected.value = [];
}
async function handleTicketConfig(data) {
ticketConfig.value = data[0];
}
function filterRows(data) {
const filteredRows = data.sort(
(a, b) => isSelectionAvailable(b) - isSelectionAvailable(a),
);
proposalTableRef.value.CrudModelRef.formData = filteredRows;
}
</script>
<template>
<FetchData
url="TicketConfigs"
:filter="{ fields: ['lackAlertPrice'] }"
@on-fetch="handleTicketConfig"
></FetchData>
auto-load
/>
<QInnerLoading
:showing="isLoading"
:label="t && t('globals.pleaseWait')"
@ -255,13 +306,22 @@ async function handleTicketConfig(data) {
:user-filter="filter"
:columns="columns"
class="full-width q-mt-md"
@on-fetch="filterRows"
row-key="id"
:row-click="change"
:is-editable="false"
:right-search="false"
:without-header="true"
:disable-option="{ card: true, table: true }"
>
<template #top-right>
<QBtn
flat
class="q-mr-sm"
color="primary"
icon="refresh"
@click="proposalTableRef.reload()"
/>
</template>
<template #column-longName="{ row }">
<QTd
class="flex"
@ -269,15 +329,17 @@ async function handleTicketConfig(data) {
>
<div
class="middle full-width"
:class="[`proposal-${gradientStyle(statusConditionalValue(row))}`]"
:class="[
`proposal-${gradientStyleClass(statusConditionalValue(row))}`,
]"
>
<QTooltip> {{ statusConditionalValue(row) }}% </QTooltip>
</div>
<div style="flex: 2 0 100%; align-content: center">
<div>
<span class="link">{{ row.longName }}</span>
<span class="link" @click.stop>
{{ row.longName }}
<ItemDescriptorProxy :id="row.id" />
</div>
</span>
</div>
</QTd>
</template>
@ -290,6 +352,9 @@ async function handleTicketConfig(data) {
<template #column-tag7="{ row }">
<span :class="{ match: !row.match7 }">{{ row.value7 }}</span>
</template>
<template #column-tag8="{ row }">
<span :class="{ match: !row.match8 }">{{ row.value8 }}</span>
</template>
<template #column-counter="{ row }">
<span
:class="{
@ -304,8 +369,17 @@ async function handleTicketConfig(data) {
</template>
<template #column-price2="{ row }">
<div class="flex column items-center content-center">
<VnStockValueDisplay :value="(sales[0].price - row.price2) / 100" />
<span :class="[conditionalValuePrice(row.price2)]">{{
<!-- Use class binding for tooltip background -->
<QTooltip :offset="[0, 5]" anchor="top middle" self="bottom middle">
<div>{{ $t('proposal.price2') }}: {{ toCurrency(row.price2) }}</div>
<div>
{{ $t('proposal.itemOldPrice') }}:
{{ toCurrency(sales[0]?.price) }}
</div>
</QTooltip>
<VnStockValueDisplay :format="'currency'" :value="-row.price2 / 100" />
<!-- Use class binding for text color -->
<span :class="[priceStatusClass(row.price2)]">{{
toCurrency(row.price2)
}}</span>
</div>
@ -319,12 +393,26 @@ async function handleTicketConfig(data) {
margin-right: 2px;
flex: 2 0 5px;
}
.price-alert {
color: $negative;
&.q-tooltip {
background-color: $negative;
color: white;
}
}
.price-ok {
color: inherit;
&.q-tooltip {
background-color: $positive;
color: white;
}
}
.match {
color: $negative;
}
.not-match {
color: inherit;
}
.proposal-warning {
background-color: $warning;
}
@ -344,3 +432,9 @@ async function handleTicketConfig(data) {
font-size: smaller;
}
</style>
<i18n>
en:
notAvailable: 'Not available for replacement'
es:
notAvailable: 'No disponible para reemplazo'
</i18n>

View File

@ -23,33 +23,32 @@ const $props = defineProps({
default: () => [],
},
});
const { dialogRef } = useDialogPluginComponent();
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
const emit = defineEmits([
'onDialogClosed',
'onDialogOk',
'itemReplaced',
...useDialogPluginComponent.emits,
]);
defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() });
const itemReplaced = (data) => {
onDialogOK(data);
dialogRef.value.hide();
};
</script>
<template>
<QDialog ref="dialogRef" transition-show="scale" transition-hide="scale">
<QCard class="dialog-width">
<QCardSection class="row items-center q-pb-none">
<span class="text-h6 text-grey">{{ $t('itemProposal') }}</span>
<span class="text-h6 text-grey" v-text="$t('itemProposal')" />
<QSpace />
<QBtn icon="close" flat round dense v-close-popup />
</QCardSection>
<QCardSection>
<ItemProposal
v-bind="$props"
@item-replaced="
(data) => {
emit('itemReplaced', data);
dialogRef.hide();
}
"
></ItemProposal
></QCardSection>
<ItemProposal v-bind="$props" @item-replaced="itemReplaced"
/></QCardSection>
</QCard>
</QDialog>
</template>

View File

@ -229,6 +229,11 @@ proposal:
value6: value6
value7: value7
value8: value8
tag5: Tag5
tag6: Tag6
tag7: Tag7
tag8: Tag8
advanceable: Advanceable
available: Available
minQuantity: minQuantity
price2: Price

View File

@ -233,11 +233,16 @@ proposal:
value6: value6
value7: value7
value8: value8
tag5: Tag5
tag6: Tag6
tag7: Tag7
tag8: Tag8
available: Disponible
minQuantity: Min. cantidad
price2: Precio
located: Ubicado
counter: Contador
advanceable: Adelantable
difference: Diferencial
groupingPrice: Precio Grouping
itemOldPrice: Precio itemOld

View File

@ -1,11 +1,11 @@
import axios from 'axios';
export default async function (data, date) {
export default async function (data, landed) {
const reducedData = data.reduce((acc, item) => {
const existing = acc.find(({ ticketFk }) => ticketFk === item.id);
if (existing) {
existing.sales.push(item.saleFk);
} else {
acc.push({ ticketFk: item.ticketFk, sales: [item.saleFk], date });
acc.push({ ticketFk: item.ticketFk, sales: [item.saleFk], landed });
}
return acc;
}, []);

View File

@ -17,7 +17,6 @@ import ItemProposalProxy from 'src/pages/Item/components/ItemProposalProxy.vue';
import { useQuasar } from 'quasar';
const quasar = useQuasar();
const { t } = useI18n();
const editableStates = ref([]);
const stateStore = useStateStore();
const tableRef = ref();
const changeItemDialogRef = ref(null);
@ -70,14 +69,11 @@ const showItemProposal = () => {
})
.onOk(itemProposalEvt);
};
const isButtonDisabled = computed(() => selectedRows.value.length !== 1);
</script>
<template>
<FetchData
url="States/editableStates"
@on-fetch="(data) => (editableStates = data)"
auto-load
/>
<FetchData
:url="`Items/${entityId}/getCard`"
:fields="['longName']"
@ -99,11 +95,7 @@ const showItemProposal = () => {
>
<template #top-right>
<QBtnGroup push class="q-mr-lg" style="column-gap: 1px">
<QBtn
data-cy="transferLines"
color="primary"
:disable="!(selectedRows.length === 1)"
>
<QBtn data-cy="transferLines" color="primary" :disable="isButtonDisabled">
<template #default>
<QIcon name="vn:splitline" />
<QIcon name="vn:ticket" />
@ -124,7 +116,7 @@ const showItemProposal = () => {
<QBtn
color="primary"
@click="showItemProposal"
:disable="!(selectedRows.length === 1)"
:disable="isButtonDisabled"
data-cy="itemProposal"
>
<QIcon name="import_export" class="rotate-90" />
@ -135,7 +127,7 @@ const showItemProposal = () => {
<VnPopupProxy
data-cy="changeItem"
icon="sync"
:disable="!(selectedRows.length === 1)"
:disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeItem.title')"
>
<template #extraIcon> <QIcon name="vn:item" /> </template>
@ -149,7 +141,7 @@ const showItemProposal = () => {
<VnPopupProxy
data-cy="changeState"
icon="sync"
:disable="!(selectedRows.length === 1)"
:disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeState.title')"
>
<template #extraIcon> <QIcon name="vn:eye" /> </template>
@ -163,7 +155,7 @@ const showItemProposal = () => {
<VnPopupProxy
data-cy="changeQuantity"
icon="sync"
:disable="!(selectedRows.length === 1)"
:disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeQuantity.title')"
@click="showChangeQuantityDialog = true"
>

View File

@ -7,6 +7,8 @@ import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDateTime from 'src/components/common/VnInputDateTime.vue';
import VnInputDates from 'src/components/common/VnInputDates.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
@ -73,8 +75,8 @@ const setUserParams = (params) => {
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
:hidden-tags="['excludedDates']"
@set-user-params="setUserParams"
:unremovable-params="['warehouseFk']"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
@ -92,7 +94,7 @@ const setUserParams = (params) => {
dense
filled
@update:model-value="
(value) => {
() => {
setUserParams(params);
}
"
@ -127,8 +129,19 @@ const setUserParams = (params) => {
dense
filled
/>
</QItemSection> </QItem
><QItem>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInputDates
v-model="params.excludedDates"
filled
:label="t('negative.excludedDates')"
>
</VnInputDates>
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="categoriesOptions">
<VnSelect
:label="t('negative.categoryFk')"

View File

@ -7,6 +7,7 @@ import { onBeforeMount } from 'vue';
import { dashIfEmpty, toDate, toHour } from 'src/filters';
import { useRouter } from 'vue-router';
import { useState } from 'src/composables/useState';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import TicketLackFilter from './TicketLackFilter.vue';
@ -45,10 +46,10 @@ const columns = computed(() => [
},
{
columnClass: 'shrink',
name: 'timed',
name: 'minTimed',
align: 'center',
label: t('negative.timed'),
format: ({ timed }) => toHour(timed),
format: ({ minTimed }) => toHour(minTimed),
sortable: true,
cardVisible: true,
columnFilter: {
@ -64,9 +65,25 @@ const columns = computed(() => [
columnFilter: {
component: 'input',
type: 'number',
inWhere: false,
columnClass: 'shrink',
},
},
{
name: 'nextEntryFk',
align: 'center',
label: t('negative.nextEntryFk'),
format: ({ nextEntryFk }) => nextEntryFk,
sortable: false,
columnFilter: false,
},
{
name: 'nextEntryLanded',
align: 'center',
label: t('negative.nextEntryLanded'),
format: ({ nextEntryLanded }) => toDate(nextEntryLanded),
sortable: false,
columnFilter: false,
},
{
name: 'longName',
align: 'left',
@ -195,6 +212,12 @@ const setUserParams = (params) => {
<span @click.stop>{{ row.itemFk }}</span>
</div>
</template>
<template #column-nextEntryFk="{ row }">
<span class="link" @click.stop>
{{ row.nextEntryFk }}
<EntryDescriptorProxy :id="row.nextEntryFk" />
</span>
</template>
<template #column-longName="{ row }">
<span class="link" @click.stop>
{{ row.longName }}

View File

@ -35,6 +35,7 @@ const filterLack = ref({
order: 'ts.alertLevelCode ASC',
});
const editableStates = ref([]);
const selectedRows = ref([]);
const { t } = useI18n();
const { notify } = useNotify();
@ -135,9 +136,12 @@ const saveChange = async (field, { row }) => {
try {
switch (field) {
case 'alertLevelCode':
const { id: code } = editableStates.value.find(
({ name }) => name === row.code,
);
await axios.post(`Tickets/state`, {
ticketFk: row.ticketFk,
code: row[field],
code,
});
break;
@ -160,6 +164,11 @@ function onBuysFetched(data) {
</script>
<template>
<FetchData
url="States/editableStates"
@on-fetch="(data) => (editableStates = data)"
auto-load
/>
<FetchData
ref="fetchItemLack"
:url="`Tickets/itemLack`"
@ -309,12 +318,12 @@ function onBuysFetched(data) {
</template>
<template #column-alertLevelCode="props">
<VnSelect
url="States/editableStates"
:options="editableStates"
auto-load
hide-selected
option-value="id"
option-value="name"
option-label="name"
v-model="props.row.alertLevelCode"
v-model="props.row.code"
v-on="getInputEvents(props)"
/>
</template>

View File

@ -19,18 +19,18 @@ const $props = defineProps({
const updateItem = async () => {
try {
showChangeItemDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ saleFk, quantity }) =>
const rowsToUpdate = $props.selectedRows.map(({ saleFk, ticketFk, quantity }) =>
axios.post(`Sales/replaceItem`, {
saleFk,
ticketFk,
substitutionFk: newItem.value,
quantity,
}),
);
const result = await Promise.allSettled(rowsToUpdate);
notifyResults(result, 'saleFk');
notifyResults(result, 'ticketFk');
emit('update-item', newItem.value);
} catch (err) {
console.error('Error updating item:', err);
return err;
}
};
@ -41,6 +41,7 @@ const updateItem = async () => {
<QCardSection class="row items-center justify-center column items-stretch">
<span>{{ $t('negative.detail.modal.changeItem.title') }}</span>
<VnSelect
data-cy="New item_select"
url="Items/WithName"
:fields="['id', 'name']"
:sort-by="['id DESC']"

View File

@ -19,9 +19,9 @@ const $props = defineProps({
const updateState = async () => {
try {
showChangeStateDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ id }) =>
const rowsToUpdate = $props.selectedRows.map(({ ticketFk }) =>
axios.post(`Tickets/state`, {
ticketFk: id,
ticketFk,
code: newState.value,
}),
);
@ -49,8 +49,9 @@ const updateState = async () => {
v-model="newState"
:options="editableStates"
option-label="name"
option-value="code"
option-value="id"
autofocus
data-cy="New state_select"
/>
</QCardSection>
<QCardActions align="right">

View File

@ -14,8 +14,6 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnRow from 'src/components/ui/VnRow.vue';
import TicketFilter from './TicketFilter.vue';
import VnInput from 'src/components/common/VnInput.vue';
import FetchData from 'src/components/FetchData.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
@ -25,6 +23,7 @@ import TicketProblems from 'src/components/TicketProblems.vue';
import VnSection from 'src/components/common/VnSection.vue';
import { getAddresses } from 'src/pages/Customer/composables/getAddresses';
import { getAgencies } from 'src/pages/Route/Agency/composables/getAgencies';
import TicketNewPayment from './components/TicketNewPayment.vue';
const route = useRoute();
const router = useRouter();
@ -73,11 +72,6 @@ const initializeFromQuery = () => {
const selectedRows = ref([]);
const hasSelectedRows = computed(() => selectedRows.value.length > 0);
const showForm = ref(false);
const dialogData = ref();
const companiesOptions = ref([]);
const accountingOptions = ref([]);
const amountToReturn = ref();
const dataKey = 'TicketList';
const formInitialData = ref({});
@ -381,87 +375,18 @@ function openBalanceDialog(ticket) {
description.value.push(ticketData.id);
}
const balanceCreateDialog = ref({
const dialogData = ref({
amountPaid: amountPaid.value,
clientFk: clientFk.value,
description: `Albaran: ${description.value.join(', ')}`,
});
dialogData.value = balanceCreateDialog;
showForm.value = true;
}
async function onSubmit() {
const { data: email } = await axios.get('Clients', {
params: {
filter: JSON.stringify({ where: { id: dialogData.value.value.clientFk } }),
quasar.dialog({
component: TicketNewPayment,
componentProps: {
clientId: clientFk.value,
formData: dialogData.value,
},
});
const { data } = await axios.post(
`Clients/${dialogData.value.value.clientFk}/createReceipt`,
{
payed: dialogData.value.payed,
companyFk: dialogData.value.companyFk,
bankFk: dialogData.value.bankFk,
amountPaid: dialogData.value.value.amountPaid,
description: dialogData.value.value.description,
clientFk: dialogData.value.value.clientFk,
email: email[0].email,
},
);
if (data) notify('globals.dataSaved', 'positive');
showForm.value = false;
}
const setAmountToReturn = (newAmountGiven) => {
const amountPaid = dialogData.value.value.amountPaid;
amountToReturn.value = newAmountGiven - amountPaid;
};
function setReference(data) {
let newDescription = '';
switch (data) {
case 1:
newDescription = `${t(
'ticketList.creditCard',
)}, ${dialogData.value.value.description.replace(
/^(Credit Card, |Cash, |Transfers, )/,
'',
)}`;
break;
case 2:
newDescription = `${t(
'ticketList.cash',
)}, ${dialogData.value.value.description.replace(
/^(Credit Card, |Cash, |Transfers, )/,
'',
)}`;
break;
case 3:
newDescription = `${newDescription.replace(
/^(Credit Card, |Cash, |Transfers, )/,
'',
)}`;
break;
case 4:
newDescription = `${t(
'ticketList.transfers',
)}, ${dialogData.value.value.description.replace(
/^(Credit Card, |Cash, |Transfers, )/,
'',
)}`;
break;
case 3317:
newDescription = '';
break;
default:
break;
}
dialogData.value.value.description = newDescription;
}
function exprBuilder(param, value) {
@ -492,16 +417,6 @@ function exprBuilder(param, value) {
</script>
<template>
<FetchData
url="Companies"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
/>
<FetchData
url="Accountings"
@on-fetch="(data) => (accountingOptions = data)"
auto-load
/>
<VnSection
:data-key="dataKey"
:columns="columns"
@ -742,99 +657,6 @@ function exprBuilder(param, value) {
{{ t('ticketList.accountPayment') }}
</QTooltip>
</QPageSticky>
<QDialog ref="dialogRef" v-model="showForm">
<QCard class="q-pa-md q-mb-md">
<QForm @submit="onSubmit()" class="q-pa-sm">
{{ t('ticketList.addPayment') }}
<VnRow>
<VnInputDate
:label="t('ticketList.date')"
v-model="dialogData.payed"
/>
<VnSelect
:label="t('ticketList.company')"
v-model="dialogData.companyFk"
:options="companiesOptions"
option-label="code"
hide-selected
>
</VnSelect>
</VnRow>
<VnRow>
<VnSelect
:label="t('ticketList.bank')"
v-model="dialogData.bankFk"
:options="accountingOptions"
option-label="bank"
hide-selected
@update:model-value="setReference"
/>
<VnInput
:label="t('ticketList.amount')"
v-model="dialogData.value.amountPaid"
/>
</VnRow>
<VnRow v-if="dialogData.bankFk === 2">
<span>
{{ t('ticketList.cash') }}
</span>
</VnRow>
<VnRow v-if="dialogData.bankFk === 2">
<VnInput
:label="t('ticketList.deliveredAmount')"
v-model="dialogData.value.amountGiven"
@update:model-value="setAmountToReturn"
type="number"
/>
<VnInput
:label="t('ticketList.amountToReturn')"
:model-value="amountToReturn"
type="number"
readonly
/>
</VnRow>
<VnRow v-if="dialogData.bankFk === 3 || dialogData.bankFk === 3117">
<VnInput
:label="t('ticketList.compensation')"
v-model="dialogData.value.compensation"
type="text"
/>
</VnRow>
<VnRow>
<VnInput
:label="t('ticketList.reference')"
v-model="dialogData.value.description"
type="text"
/>
</VnRow>
<VnRow v-if="dialogData.bankFk === 2">
<QCheckbox
:label="t('ticketList.viewReceipt')"
v-model="dialogData.value.viewReceipt"
:toggle-indeterminate="false"
/>
<QCheckbox
:label="t('ticketList.sendEmail')"
v-model="dialogData.value.senEmail"
:toggle-indeterminate="false"
/>
</VnRow>
<div class="q-mt-lg row justify-end">
<QBtn
:label="t('globals.save')"
color="primary"
@click="onSubmit()"
/>
<QBtn
flat
:label="t('globals.close')"
color="primary"
v-close-popup
/>
</div>
</QForm>
</QCard>
</QDialog>
<QPageSticky v-if="hasSelectedRows" :offset="[20, 200]" style="z-index: 2">
<QBtn
@click="sendDocuware(selectedRows)"

View File

@ -0,0 +1,304 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import { useDialogPluginComponent } from 'quasar';
import { usePrintService } from 'src/composables/usePrintService';
import useNotify from 'src/composables/useNotify.js';
import FormModelPopup from 'src/components/FormModelPopup.vue';
import VnRow from 'src/components/ui/VnRow.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnAccountNumber from 'src/components/common/VnAccountNumber.vue';
import { useState } from 'src/composables/useState';
const { t } = useI18n();
const { notify } = useNotify();
const { sendEmail, openReport } = usePrintService();
const { dialogRef } = useDialogPluginComponent();
const $props = defineProps({
formData: {
type: Object,
required: true,
},
clientId: {
type: Number,
required: true,
},
promise: {
type: Function,
default: null,
},
});
const closeButton = ref(null);
const viewReceipt = ref();
const shouldSendEmail = ref(false);
const maxAmount = ref();
const accountingType = ref({});
const isCash = ref(false);
const formModelRef = ref(false);
const amountToReturn = ref();
const filterBanks = {
fields: ['id', 'bank', 'accountingTypeFk'],
include: { relation: 'accountingType' },
order: 'id',
};
const state = useState();
const user = state.getUser();
const initialData = ref({
...$props.formData,
companyFk: user.value.companyFk,
payed: Date.vnNew(),
});
function setPaymentType(data, accounting) {
data.bankFk = accounting.id;
if (!accounting) return;
accountingType.value = accounting.accountingType;
data.description = [];
data.payed = Date.vnNew();
isCash.value = accountingType.value.code == 'cash';
viewReceipt.value = isCash.value;
if (accountingType.value.daysInFuture)
data.payed.setDate(data.payed.getDate() + accountingType.value.daysInFuture);
maxAmount.value = accountingType.value && accountingType.value.maxAmount;
if (accountingType.value.code == 'compensation') return (data.description = '');
let descriptions = [];
if (accountingType.value.receiptDescription)
descriptions.push(accountingType.value.receiptDescription);
if (data.description > 0) descriptions.push(data.description);
data.description = descriptions.join(', ');
}
const calculateFromAmount = (event) => {
initialData.value.amountToReturn = Number(
(parseFloat(initialData.value.deliveredAmount) + parseFloat(event) * -1).toFixed(
2,
),
);
};
const calculateFromDeliveredAmount = (event) => {
amountToReturn.value = Number((event - initialData.value.amountPaid).toFixed(2));
};
function onBeforeSave(data) {
const exceededAmount = data.amountPaid > maxAmount.value;
if (isCash.value && exceededAmount)
return notify(t('Amount exceeded', { maxAmount: maxAmount.value }), 'negative');
if (isCash.value && shouldSendEmail.value && !data.email)
return notify(t('There is no assigned email for this client'), 'negative');
return data;
}
async function onDataSaved({ email, id }) {
try {
if (shouldSendEmail.value && isCash.value)
await sendEmail(`Receipts/${id}/receipt-email`, {
recipient: email,
});
if (viewReceipt.value) openReport(`Receipts/${id}/receipt-pdf`, {}, '_blank');
} finally {
if ($props.promise) $props.promise();
if (closeButton.value) closeButton.value.click();
}
}
async function getSupplierClientReferences(data) {
if (!data) return (initialData.value.description = '');
const params = { bankAccount: data.compensationAccount };
const { data: reference } = await axios(`Clients/getClientOrSupplierReference`, {
params,
});
if (reference.supplierId) {
data.description = t('Supplier Compensation Reference', {
supplierId: reference.supplierId,
supplierName: reference.supplierName,
});
return;
}
data.description = t('Client Compensation Reference', {
clientId: reference.clientId,
clientName: reference.clientName,
});
}
async function getAmountPaid() {
const filter = {
where: {
clientFk: $props.clientId,
companyFk: initialData.value.companyFk,
},
};
const { data } = await getClientRisk(filter);
initialData.value.amountPaid = (data?.length && data[0].amount) || undefined;
}
async function onSubmit(formData) {
const clientFk = $props.clientId;
const {
data: [{ email }],
} = await axios.get('Clients', {
params: {
filter: JSON.stringify({ where: { id: clientFk } }),
},
});
const { data } = await axios.post(`Clients/${clientFk}/createReceipt`, {
payed: formData.payed,
companyFk: formData.companyFk,
bankFk: formData.bankFk,
amountPaid: formData.amountPaid,
description: formData.description,
clientFk,
email,
});
if (data) notify('globals.dataSaved', 'positive');
await onDataSaved(data);
}
</script>
<template>
<QDialog ref="dialogRef" persistent>
<FormModelPopup
ref="formModelRef"
:form-initial-data="initialData"
:save-fn="onSubmit"
:prevent-submit="true"
:mapper="onBeforeSave"
>
<template #form-inputs="{ data, validate }">
<h5 class="q-mt-none">{{ t('New payment') }}</h5>
<VnRow>
<VnSelect
autofocus
:label="t('Bank')"
v-model="data.bankFk"
url="Accountings"
:filter="filterBanks"
option-label="bank"
:include="{ relation: 'accountingType' }"
sort-by="id"
@update:model-value="
(value, options) => setPaymentType(data, value, options)
"
:emit-value="false"
data-cy="paymentBank"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt.id }}:&ensp;{{ scope.opt.bank }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnInputNumber
:label="t('Amount')"
:required="true"
@update:model-value="calculateFromAmount($event)"
clearable
v-model.number="data.amountPaid"
data-cy="paymentAmount"
:positive="false"
/>
</VnRow>
<VnRow>
<VnInputDate
:label="t('Date')"
v-model="data.payed"
:required="true"
/>
<VnSelect
url="Companies"
:label="t('Company')"
:required="true"
:rules="validate('entry.companyFk')"
hide-selected
option-label="code"
v-model="data.companyFk"
@update:model-value="getAmountPaid()"
/>
</VnRow>
<div v-if="accountingType.code == 'compensation'">
<div class="text-h6">
{{ t('Compensation') }}
</div>
<VnRow>
<VnAccountNumber
:label="t('Compensation account')"
clearable
v-model="data.compensationAccount"
@blur="getSupplierClientReferences(data)"
/>
</VnRow>
</div>
<VnInput
:label="t('Reference')"
:required="true"
clearable
v-model="data.description"
/>
<div v-if="accountingType.code == 'cash'">
<div class="text-h6">{{ t('Cash') }}</div>
<VnRow>
<VnInputNumber
:label="t('Delivered amount')"
@update:model-value="calculateFromDeliveredAmount($event)"
clearable
v-model="data.deliveredAmount"
/>
<VnInputNumber
:label="t('Amount to return')"
disable
v-model="amountToReturn"
/>
</VnRow>
<VnRow>
<QCheckbox v-model="viewReceipt" :label="t('View recipt')" />
<QCheckbox v-model="shouldSendEmail" :label="t('Send email')" />
</VnRow>
</div>
</template>
</FormModelPopup>
</QDialog>
</template>
<i18n>
en:
Supplier Compensation Reference: ({supplierId}) Ntro Proveedor {supplierName}
Client Compensation Reference: ({clientId}) Ntro Cliente {clientName}
es:
New payment: Añadir pago
Date: Fecha
Company: Empresa
Bank: Caja
Amount: Importe
Reference: Referencia
Cash: Efectivo
Delivered amount: Cantidad entregada
Amount to return: Cantidad a devolver
View recipt: Ver recibido
Send email: Enviar correo
Compensation: Compensación
Compensation account: Cuenta para compensar
Supplier Compensation Reference: ({supplierId}) Ntro Proveedor {supplierName}
Client Compensation Reference: ({clientId}) Ntro Cliente {clientName}
There is no assigned email for this client: No hay correo asignado para este cliente
Amount exceeded: Según ley contra el fraude no se puede recibir cobros por importe igual o superior a {maxAmount}
</i18n>

View File

@ -206,7 +206,6 @@ ticketList:
toLines: Go to lines
addressNickname: Address nickname
ref: Reference
hour: Hour
rounding: Rounding
noVerifiedData: No verified data
warehouse: Warehouse
@ -215,6 +214,8 @@ ticketList:
clientFrozen: Client frozen
componentLack: Component lack
negative:
nextEntryFk: Next entry
nextEntryLanded: Next entry landed
hour: Hour
id: Id Article
longName: Article
@ -225,6 +226,7 @@ negative:
value: Negative
itemFk: Article
producer: Producer
excludedDates: Excluded dates
warehouse: Warehouse
warehouseFk: Warehouse
category: Category

View File

@ -215,6 +215,8 @@ ticketList:
addressNickname: Alias consignatario
ref: Referencia
negative:
nextEntryLanded: F. Entrada
nextEntryFk: Entrada
hour: Hora
id: Id Articulo
longName: Artículo
@ -225,7 +227,8 @@ negative:
origen: Origen
value: Negativo
warehouseFk: Almacen
producer: Producer
producer: Productor
excludedDates: Fechas excluidas
category: Categoría
categoryFk: Familia
typeFk: Familia

View File

@ -45,7 +45,7 @@ describe('OrderCatalog', { testIsolation: true }, () => {
).type('{enter}');
cy.get(':nth-child(1) > [data-cy="catalogFilterCategory"]').click();
cy.dataCy('catalogFilterValueDialogBtn').last().click();
cy.selectOption("[data-cy='catalogFilterValueDialogTagSelect']", 'Tallos');
cy.selectOption('[data-cy="catalogFilterValueDialogTagSelect"]', 'Tallos');
cy.dataCy('catalogFilterValueDialogValueInput').find('input').focus();
cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('2');
cy.dataCy('catalogFilterValueDialogValueInput').find('input').type('{enter}');

View File

@ -1,146 +1,161 @@
/// <reference types="cypress" />
describe.skip('Ticket Lack detail', () => {
beforeEach(() => {
cy.login('developer');
cy.intercept('GET', /\/api\/Tickets\/itemLack\/5.*$/, {
statusCode: 200,
body: [
{
saleFk: 33,
code: 'OK',
ticketFk: 142,
nickname: 'Malibu Point',
shipped: '2000-12-31T23:00:00.000Z',
hour: 0,
quantity: 50,
agName: 'Super-Man delivery',
alertLevel: 0,
stateName: 'OK',
stateId: 3,
itemFk: 5,
price: 1.79,
alertLevelCode: 'FREE',
zoneFk: 9,
zoneName: 'Zone superMan',
theoreticalhour: '2011-11-01T22:59:00.000Z',
isRookie: 1,
turno: 1,
peticionCompra: 1,
hasObservation: 1,
hasToIgnore: 1,
isBasket: 1,
minTimed: 0,
customerId: 1104,
customerName: 'Tony Stark',
observationTypeCode: 'administrative',
},
],
}).as('getItemLack');
cy.visit('/#/ticket/negative/5', false);
cy.wait('@getItemLack');
const firstRow = 'tr.cursor-pointer > :nth-child(1)';
const ticketId = 1000000;
const clickNotificationAction = () => {
const notification = '.q-notification';
cy.waitForElement(notification);
cy.get(notification).should('be.visible');
cy.get('.q-notification__actions > .q-btn').click();
cy.get('@open').should((openStub) => {
expect(openStub).to.be.called;
const firstArg = openStub.args[0][0];
expect(firstArg).to.match(/\/ticket\/\d+\/sale/);
expect(firstArg).to.include(`/ticket/${ticketId}/sale`);
});
describe('Table actions', () => {
it('should display only one row in the lack list', () => {
cy.location('href').should('contain', '#/ticket/negative/5');
cy.get('[data-cy="changeItem"]').should('be.disabled');
cy.get('[data-cy="changeState"]').should('be.disabled');
cy.get('[data-cy="changeQuantity"]').should('be.disabled');
cy.get('[data-cy="itemProposal"]').should('be.disabled');
cy.get('[data-cy="transferLines"]').should('be.disabled');
cy.get('tr.cursor-pointer > :nth-child(1)').click();
cy.get('[data-cy="changeItem"]').should('be.enabled');
cy.get('[data-cy="changeState"]').should('be.enabled');
cy.get('[data-cy="changeQuantity"]').should('be.enabled');
cy.get('[data-cy="itemProposal"]').should('be.enabled');
cy.get('[data-cy="transferLines"]').should('be.enabled');
};
describe('Ticket Lack detail', { testIsolation: true }, () => {
beforeEach(() => {
cy.viewport(1980, 1020);
cy.login('developer');
cy.intercept('GET', /\/api\/Tickets\/itemLack\/88.*$/).as('getItemLack');
cy.visit('/#/ticket/negative/88');
cy.window().then((win) => {
cy.stub(win, 'open').as('open');
});
cy.wait('@getItemLack').then((interception) => {
const { query } = interception.request;
const filter = JSON.parse(query.filter);
expect(filter).to.have.property('where');
expect(filter.where).to.have.property('alertLevelCode', 'FREE');
});
});
describe('Table detail', () => {
it('should open descriptors', () => {
cy.get('.q-table').should('be.visible');
cy.colField('zoneName').click();
cy.dataCy('ZoneDescriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('have.text', ' #1');
cy.colField('ticketFk').click();
cy.dataCy('TicketDescriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('have.text', ` #${ticketId}`);
cy.colField('nickname').find('.link').click();
cy.waitForElement('[data-cy="CustomerDescriptor"]');
cy.dataCy('CustomerDescriptor').should('be.visible');
cy.get('.q-item > .q-item__label').should('have.text', ' #1');
});
it('should display only one row in the lack list', () => {
cy.dataCy('changeItem').should('be.disabled');
cy.dataCy('changeState').should('be.disabled');
cy.dataCy('changeQuantity').should('be.disabled');
cy.dataCy('itemProposal').should('be.disabled');
cy.dataCy('transferLines').should('be.disabled');
cy.get('tr.cursor-pointer > :nth-child(1)').click();
cy.dataCy('changeItem').should('be.enabled');
cy.dataCy('changeState').should('be.enabled');
cy.dataCy('changeQuantity').should('be.enabled');
cy.dataCy('itemProposal').should('be.enabled');
cy.dataCy('transferLines').should('be.enabled');
});
});
describe('Split', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('transferLines').click();
});
it('Split', () => {
cy.dataCy('ticketTransferPopup').find('.flex > .q-btn').click();
cy.checkNotification(`Ticket ${ticketId}: No split`);
});
});
describe('change quantity', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeQuantity').click();
});
it('by popup', () => {
cy.dataCy('New quantity_input').type(10);
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
});
});
describe('Change state', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeState').click();
});
it('by popup', () => {
cy.dataCy('New state_select').should('be.visible');
cy.selectOption('[data-cy="New state_select"]', 'OK');
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
});
});
describe('Change Item', () => {
beforeEach(() => {
cy.get(firstRow).click();
cy.dataCy('changeItem').click();
});
it('by popup', () => {
cy.dataCy('New item_select').should('be.visible');
cy.selectOption('[data-cy="New item_select"]', 'Palito rojo');
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
cy.checkNotification('Ticket 1000000: price retrieval failed');
cy.dataCy('changeItem').click();
cy.selectOption('[data-cy="New item_select"]', 'Ranged weapon longbow 200cm');
cy.get('.q-btn--unelevated > .q-btn__content > .block').click();
clickNotificationAction();
});
after(() => {
cy.visit(`/#/ticket/${ticketId}/sale`);
const quantity = Math.floor(Math.random() * 100) + 1;
const rowIndex = 1;
cy.dataCy('ticketSaleQuantityInput')
.find('input')
.eq(rowIndex)
.clear()
.type(`${quantity}{enter}`);
cy.dataCy('ticketSaleQuantityInput')
.find('input')
.eq(rowIndex)
.should('have.value', `${quantity}`);
});
});
describe('Item proposal', () => {
beforeEach(() => {
cy.get('tr.cursor-pointer > :nth-child(1)').click();
cy.intercept('GET', /\/api\/Items\/getSimilar\?.*$/, {
statusCode: 200,
body: [
{
id: 1,
longName: 'Ranged weapon longbow 50cm',
subName: 'Stark Industries',
tag5: 'Color',
value5: 'Brown',
match5: 0,
match6: 0,
match7: 0,
match8: 1,
tag6: 'Categoria',
value6: '+1 precission',
tag7: 'Tallos',
value7: '1',
tag8: null,
value8: null,
available: 20,
calc_id: 6,
counter: 0,
minQuantity: 1,
visible: null,
price2: 1,
},
{
id: 2,
longName: 'Ranged weapon longbow 100cm',
subName: 'Stark Industries',
tag5: 'Color',
value5: 'Brown',
match5: 0,
match6: 1,
match7: 0,
match8: 1,
tag6: 'Categoria',
value6: '+1 precission',
tag7: 'Tallos',
value7: '1',
tag8: null,
value8: null,
available: 50,
calc_id: 6,
counter: 1,
minQuantity: 5,
visible: null,
price2: 10,
},
{
id: 3,
longName: 'Ranged weapon longbow 200cm',
subName: 'Stark Industries',
tag5: 'Color',
value5: 'Brown',
match5: 1,
match6: 1,
match7: 1,
match8: 1,
tag6: 'Categoria',
value6: '+1 precission',
tag7: 'Tallos',
value7: '1',
tag8: null,
value8: null,
available: 185,
calc_id: 6,
counter: 10,
minQuantity: 10,
visible: null,
price2: 100,
},
],
}).as('getItemGetSimilar');
cy.get('[data-cy="itemProposal"]').click();
cy.wait('@getItemGetSimilar');
cy.get(firstRow).click();
cy.dataCy('itemProposal').click();
});
describe.skip('Replace item if', () => {
describe('Replace item if', () => {
it('Quantity is less than available', () => {
cy.get(':nth-child(1) > .text-right > .q-btn').click();
const index = 2;
cy.colField('tag7', index).click();
cy.checkNotification('Not available for replacement');
});
it('item proposal cells', () => {
const index = 1;
cy.colField('longName', index)
.find('.no-padding > .q-td > .middle')
.should('have.class', 'proposal-primary');
cy.colField('tag5', index)
.find('.no-padding > .match')
.should('have.class', 'match');
cy.colField('tag6', index)
.find('.no-padding > .match')
.should('have.class', 'match');
cy.colField('tag7', index).click();
clickNotificationAction();
});
});
});

View File

@ -1,34 +1,16 @@
/// <reference types="cypress" />
describe('Ticket Lack list', () => {
beforeEach(() => {
cy.login('developer');
cy.intercept('GET', /Tickets\/itemLack\?.*$/, {
statusCode: 200,
body: [
{
itemFk: 5,
longName: 'Ranged weapon pistol 9mm',
warehouseFk: 1,
producer: null,
size: 15,
category: null,
warehouse: 'Warehouse One',
lack: -50,
inkFk: 'SLV',
timed: '2025-01-25T22:59:00.000Z',
minTimed: '23:59',
originFk: 'Holand',
},
],
}).as('getLack');
cy.viewport(1980, 1020);
cy.login('developer');
cy.visit('/#/ticket/negative');
});
describe('Table actions', () => {
it('should display only one row in the lack list', () => {
cy.wait('@getLack', { timeout: 10000 });
cy.get('[data-col-field="longName"]').first().click();
cy.dataCy('ItemDescriptor').should('be.visible');
cy.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click();
cy.location('href').should('contain', '#/ticket/negative/5');
});

View File

@ -22,7 +22,7 @@ describe('TicketSale', { testIsolation: true }, () => {
cy.intercept('POST', /\/api\/Sales\/\d+\/updatePrice/).as('updatePrice');
cy.dataCy('saveManaBtn').click();
handleVnConfirm();
cy.handleVnConfirm();
cy.wait('@updatePrice').its('response.statusCode').should('eq', 200);
cy.get('[data-col-field="price"]')
@ -43,7 +43,7 @@ describe('TicketSale', { testIsolation: true }, () => {
);
cy.dataCy('saveManaBtn').click();
handleVnConfirm();
cy.handleVnConfirm();
cy.wait('@updateDiscount').its('response.statusCode').should('eq', 204);
cy.get('[data-col-field="discount"]')
@ -61,7 +61,7 @@ describe('TicketSale', { testIsolation: true }, () => {
.find('[data-cy="undefined_input"]')
.type(concept)
.type('{enter}');
handleVnConfirm();
cy.handleVnConfirm();
cy.get('[data-col-field="item"]').should('contain.text', `${concept}`);
});
@ -71,13 +71,9 @@ describe('TicketSale', { testIsolation: true }, () => {
cy.dataCy('ticketSaleQuantityInput').find('input').clear();
cy.intercept('POST', '**/api').as('postRequest');
cy.dataCy('ticketSaleQuantityInput')
.find('input')
.type(quantity)
.trigger('tab');
cy.get('.q-page > :nth-child(6)').click();
cy.dataCy('ticketSaleQuantityInput').find('input').type(`${quantity}{enter}`);
handleVnConfirm();
cy.handleVnConfirm();
cy.get('[data-cy="ticketSaleQuantityInput"]')
.find('input')
@ -210,8 +206,3 @@ function selectFirstRow() {
cy.waitForElement(firstRow);
cy.get(firstRow).find('.q-checkbox__inner').click();
}
function handleVnConfirm() {
cy.confirmVnConfirm();
cy.checkNotification('Data saved');
}

View File

@ -1,3 +1,9 @@
Cypress.Commands.add('handleVnConfirm', () => {
cy.confirmVnConfirm();
cy.checkNotification('Data saved');
});
Cypress.Commands.add('confirmVnConfirm', () =>
cy.dataCy('VnConfirm_confirm').should('exist').click(),
);

View File

@ -18,3 +18,37 @@ Cypress.Commands.add('tableActions', (n = 0, child = 1) =>
`:nth-child(${child}) > .q-table--col-auto-width > [data-cy="tableAction-${n}"] > .q-btn__content > .q-icon`,
),
);
Cypress.Commands.add('validateVnTableRows', (opts = {}) => {
let { cols = [] } = opts;
const { rows = [] } = opts;
if (!Array.isArray(cols)) cols = [cols];
const rowSelector = rows.length
? rows.map((row) => `> :nth-child(${row})`).join(', ')
: '> *';
cy.get(`[data-cy="vnTable"] .q-virtual-scroll__content`).within(() => {
cy.get(`${rowSelector}`).each(($el) => {
for (const { name, type = 'string', val, operation = 'equal' } of cols) {
cy.wrap($el)
.find(`[data-cy="vnTableCell_${name}"]`)
.invoke('text')
.then((text) => {
if (type === 'string')
expect(text.trim().toLowerCase()).to[operation](
val.toLowerCase(),
);
if (type === 'number') cy.checkNumber(text, val, operation);
if (type === 'date') cy.checkDate(text, val, operation);
});
}
});
});
});
Cypress.Commands.add('colField', (name, index = null, key = 'data-col-field') => {
if (index) {
cy.get(`:nth-child(${index}) > [${key}="${name}"]`);
} else {
cy.get(`[${key}="${name}"]`);
}
});