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

This commit is contained in:
Jose Antonio Tubau 2025-05-20 09:27:34 +00:00
commit 0dcbc49e35
87 changed files with 2193 additions and 1365 deletions

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

Before

Width:  |  Height:  |  Size: 6.9 KiB

After

Width:  |  Height:  |  Size: 6.9 KiB

View File

@ -18,7 +18,7 @@ const arrayData = defineModel({
function handler(event) { function handler(event) {
const clickedElement = event.target.closest('td'); const clickedElement = event.target.closest('td');
if (!clickedElement) return; if (!clickedElement) return;
event.preventDefault();
target.value = event.target; target.value = event.target;
qmenuRef.value.show(); qmenuRef.value.show();
colField.value = clickedElement.getAttribute('data-col-field'); colField.value = clickedElement.getAttribute('data-col-field');

View File

@ -110,7 +110,6 @@ const components = {
component: markRaw(VnCheckbox), component: markRaw(VnCheckbox),
event: updateEvent, event: updateEvent,
attrs: { attrs: {
class: $props.showTitle ? 'q-py-sm' : 'q-px-md q-py-xs fit',
'toggle-indeterminate': true, 'toggle-indeterminate': true,
size: 'sm', size: 'sm',
}, },

View File

@ -222,10 +222,7 @@ onBeforeMount(() => {
onMounted(async () => { onMounted(async () => {
if ($props.isEditable) document.addEventListener('click', clickHandler); if ($props.isEditable) document.addEventListener('click', clickHandler);
document.addEventListener('contextmenu', (event) => { document.addEventListener('contextmenu', contextMenuRef.value.handler);
event.preventDefault();
contextMenuRef.value.handler(event);
});
mode.value = mode.value =
quasar.platform.is.mobile && !$props.disableOption?.card quasar.platform.is.mobile && !$props.disableOption?.card
? CARD_MODE ? CARD_MODE
@ -386,16 +383,20 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
} }
} }
function isEditableColumn(column) { function isEditableColumn(column, row) {
const isEditableCol = column?.isEditable ?? true; const isEditableCol =
typeof column?.isEditable == 'function'
? column?.isEditable(row)
: (column?.isEditable ?? true);
const isVisible = column?.visible ?? true; const isVisible = column?.visible ?? true;
const hasComponent = column?.component; const hasComponent = column?.component;
return $props.isEditable && isVisible && hasComponent && isEditableCol; return $props.isEditable && isVisible && hasComponent && isEditableCol;
} }
function hasEditableFormat(column) { function hasEditableFormat(column, row) {
if (isEditableColumn(column)) return 'editable-text'; if (isEditableColumn(column, row)) return 'editable-text';
} }
const clickHandler = async (event) => { const clickHandler = async (event) => {
@ -409,7 +410,7 @@ const clickHandler = async (event) => {
if (isDateElement || isTimeElement || isQSelectDropDown || isDialog) return; if (isDateElement || isTimeElement || isQSelectDropDown || isDialog) return;
if (clickedElement === null) { if (clickedElement === null) {
await destroyInput(editingRow.value, editingField.value); destroyInput(editingRow.value, editingField.value);
return; return;
} }
const rowIndex = clickedElement.getAttribute('data-row-index'); const rowIndex = clickedElement.getAttribute('data-row-index');
@ -419,20 +420,25 @@ const clickHandler = async (event) => {
if (editingRow.value !== null && editingField.value !== null) { if (editingRow.value !== null && editingField.value !== null) {
if (editingRow.value == rowIndex && editingField.value == colField) return; if (editingRow.value == rowIndex && editingField.value == colField) return;
await destroyInput(editingRow.value, editingField.value); destroyInput(editingRow.value, editingField.value);
} }
if (isEditableColumn(column)) { if (
await renderInput(Number(rowIndex), colField, clickedElement); isEditableColumn(
column,
CrudModelRef.value.formData[rowIndex ?? editingRow.value],
)
) {
renderInput(Number(rowIndex), colField, clickedElement);
} }
}; };
async function handleTabKey(event, rowIndex, colField) { function handleTabKey(event, rowIndex, colField) {
if (editingRow.value == rowIndex && editingField.value == colField) if (editingRow.value == rowIndex && editingField.value == colField)
await destroyInput(editingRow.value, editingField.value); destroyInput(editingRow.value, editingField.value);
const direction = event.shiftKey ? -1 : 1; const direction = event.shiftKey ? -1 : 1;
const { nextRowIndex, nextColumnName } = await handleTabNavigation( const { nextRowIndex, nextColumnName } = handleTabNavigation(
rowIndex, rowIndex,
colField, colField,
direction, direction,
@ -441,10 +447,10 @@ async function handleTabKey(event, rowIndex, colField) {
if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return; if (nextRowIndex < 0 || nextRowIndex >= arrayData.store.data.length) return;
event.preventDefault(); event.preventDefault();
await renderInput(nextRowIndex, nextColumnName, null); renderInput(nextRowIndex, nextColumnName, null);
} }
async function renderInput(rowId, field, clickedElement) { function renderInput(rowId, field, clickedElement) {
editingField.value = field; editingField.value = field;
editingRow.value = rowId; editingRow.value = rowId;
@ -482,19 +488,22 @@ async function renderInput(rowId, field, clickedElement) {
} else row[column.name] = value; } else row[column.name] = value;
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
}, },
keyup: async (event) => { keyup: (event) => {
if (event.key === 'Enter') if (event.key === 'Enter') {
await destroyInput(rowId, field, clickedElement); destroyInput(rowId, field, clickedElement);
event.stopPropagation();
}
}, },
keydown: async (event) => { keydown: (event) => {
await column?.cellEvent?.['keydown']?.(event, row); column?.cellEvent?.['keydown']?.(event, row);
switch (event.key) { switch (event.key) {
case 'Tab': case 'Tab':
await handleTabKey(event, rowId, field); handleTabKey(event, rowId, field);
event.stopPropagation(); event.stopPropagation();
break; break;
case 'Escape': case 'Escape':
await destroyInput(rowId, field, clickedElement); destroyInput(rowId, field, clickedElement);
event.stopPropagation();
break; break;
default: default:
break; break;
@ -527,25 +536,32 @@ async function updateSelectValue(value, column, row, oldValue) {
await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row); await column?.cellEvent?.['update:modelValue']?.(value, oldValue, row);
} }
async function destroyInput(rowIndex, field, clickedElement) { function destroyInput(rowIndex, field, clickedElement) {
if (!clickedElement) if (!clickedElement)
clickedElement = document.querySelector( clickedElement = document.querySelector(
`[data-row-index="${rowIndex}"][data-col-field="${field}"]`, `[data-row-index="${rowIndex}"][data-col-field="${field}"]`,
); );
if (clickedElement) { if (clickedElement) {
await nextTick(); const column = $props.columns.find((col) => col.name === field);
render(null, clickedElement); if (typeof column?.beforeDestroy === 'function')
Array.from(clickedElement.childNodes).forEach((child) => { column.beforeDestroy(CrudModelRef.value.formData[rowIndex]);
child.style.visibility = 'visible';
child.style.position = ''; nextTick().then(() => {
render(null, clickedElement);
Array.from(clickedElement.childNodes).forEach((child) => {
child.style.visibility = 'visible';
child.style.position = '';
});
}); });
} }
if (editingRow.value !== rowIndex || editingField.value !== field) return; if (editingRow.value !== rowIndex || editingField.value !== field) return;
editingRow.value = null; editingRow.value = null;
editingField.value = null; editingField.value = null;
} }
async function handleTabNavigation(rowIndex, colName, direction) { function handleTabNavigation(rowIndex, colName, direction) {
const columns = $props.columns; const columns = $props.columns;
const totalColumns = columns.length; const totalColumns = columns.length;
let currentColumnIndex = columns.findIndex((col) => col.name === colName); let currentColumnIndex = columns.findIndex((col) => col.name === colName);
@ -557,7 +573,13 @@ async function handleTabNavigation(rowIndex, colName, direction) {
iterations++; iterations++;
newColumnIndex = (newColumnIndex + direction + totalColumns) % totalColumns; newColumnIndex = (newColumnIndex + direction + totalColumns) % totalColumns;
if (isEditableColumn(columns[newColumnIndex])) break; if (
isEditableColumn(
columns[newColumnIndex],
CrudModelRef.value.formData[rowIndex],
)
)
break;
} while (iterations < totalColumns); } while (iterations < totalColumns);
if (iterations >= totalColumns + 1) return; if (iterations >= totalColumns + 1) return;
@ -884,19 +906,19 @@ const handleHeaderSelection = (evt, data) => {
: getToggleIcon(row[col?.name]) : getToggleIcon(row[col?.name])
" "
style="color: var(--vn-text-color)" style="color: var(--vn-text-color)"
:class="hasEditableFormat(col)" :class="hasEditableFormat(col, row)"
size="14px" size="14px"
/> />
<QIcon <QIcon
v-else-if="col?.component === 'checkbox'" v-else-if="col?.component === 'checkbox'"
:name="getCheckboxIcon(row[col?.name])" :name="getCheckboxIcon(row[col?.name])"
style="color: var(--vn-text-color)" style="color: var(--vn-text-color)"
:class="hasEditableFormat(col)" :class="hasEditableFormat(col, row)"
size="14px" size="14px"
/> />
<span <span
v-else v-else
:class="hasEditableFormat(col)" :class="hasEditableFormat(col, row)"
:style=" :style="
typeof col?.style == 'function' typeof col?.style == 'function'
? col.style(row) ? col.style(row)
@ -922,7 +944,11 @@ const handleHeaderSelection = (evt, data) => {
v-for="(btn, index) of col.actions" v-for="(btn, index) of col.actions"
v-show="btn.show ? btn.show(row) : true" v-show="btn.show ? btn.show(row) : true"
:key="index" :key="index"
:title="btn.title" :title="
typeof btn.title === 'function'
? btn.title(row)
: btn.title
"
:icon="btn.icon" :icon="btn.icon"
class="q-pa-xs" class="q-pa-xs"
flat flat
@ -1231,7 +1257,7 @@ es:
} }
.bg-header { .bg-header {
background-color: var(--vn-accent-color); background-color: var(--vn-section-color);
color: var(--vn-text-color); color: var(--vn-text-color);
} }

View File

@ -36,8 +36,6 @@ const validate = async () => {
isLoading.value = true; isLoading.value = true;
await props.submitFn(newPassword, oldPassword); await props.submitFn(newPassword, oldPassword);
emit('onSubmit'); emit('onSubmit');
} catch (e) {
notify('errors.writeRequest', 'negative');
} finally { } finally {
changePassDialog.value.hide(); changePassDialog.value.hide();
isLoading.value = false; isLoading.value = false;

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

@ -162,7 +162,6 @@ async function fetch() {
align-items: start; align-items: start;
.label { .label {
color: var(--vn-label-color); color: var(--vn-label-color);
width: 9em;
overflow: hidden; overflow: hidden;
white-space: wrap; white-space: wrap;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -236,10 +236,6 @@ const toModule = computed(() => {
.label { .label {
color: var(--vn-label-color); color: var(--vn-label-color);
font-size: 14px; font-size: 14px;
&:not(:has(a))::after {
content: ':';
}
} }
&.ellipsis > .value { &.ellipsis > .value {
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -1,8 +1,9 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref } from 'vue';
import { useSession } from 'src/composables/useSession'; import { useSession } from 'src/composables/useSession';
import noImage from '/no-user.png'; import noUser from '/no-user.png';
import { useRole } from 'src/composables/useRole'; import { useRole } from 'src/composables/useRole';
import { getDarkSuffix } from 'src/composables/getDarkSuffix';
const $props = defineProps({ const $props = defineProps({
storage: { storage: {
@ -43,7 +44,7 @@ const getUrl = (zoom = false) => {
return `/api/${$props.storage}/${$props.id}/downloadFile?access_token=${token}`; return `/api/${$props.storage}/${$props.id}/downloadFile?access_token=${token}`;
return isEmployee return isEmployee
? `/api/${$props.storage}/${$props.collection}/${curResolution}/${$props.id}/download?access_token=${token}&${timeStamp.value}` ? `/api/${$props.storage}/${$props.collection}/${curResolution}/${$props.id}/download?access_token=${token}&${timeStamp.value}`
: noImage; : noUser;
}; };
const reload = () => { const reload = () => {
timeStamp.value = `timestamp=${Date.now()}`; timeStamp.value = `timestamp=${Date.now()}`;
@ -60,6 +61,7 @@ defineExpose({
v-bind="$attrs" v-bind="$attrs"
@click.stop="show = $props.zoom" @click.stop="show = $props.zoom"
spinner-color="primary" spinner-color="primary"
:error-src="`/no_image${getDarkSuffix()}.png`"
/> />
<QDialog v-if="$props.zoom" v-model="show"> <QDialog v-if="$props.zoom" v-model="show">
<QImg <QImg

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import { Dark } from 'quasar';
import { computed } from 'vue'; import { computed } from 'vue';
import { getDarkSuffix } from 'src/composables/getDarkSuffix';
const $props = defineProps({ const $props = defineProps({
logo: { logo: {
@ -12,7 +12,7 @@ const $props = defineProps({
const src = computed({ const src = computed({
get() { get() {
return new URL( return new URL(
`../../assets/${$props.logo}${Dark.isActive ? '_dark' : ''}.svg`, `../../assets/${$props.logo}${getDarkSuffix()}.svg`,
import.meta.url, import.meta.url,
).href; ).href;
}, },

View File

@ -42,12 +42,10 @@ const val = computed(() => $props.value);
<div v-if="label || $slots.label" class="label"> <div v-if="label || $slots.label" class="label">
<slot name="label"> <slot name="label">
<QTooltip v-if="tooltip">{{ tooltip }}</QTooltip> <QTooltip v-if="tooltip">{{ tooltip }}</QTooltip>
<span style="color: var(--vn-label-color)"> <span style="color: var(--vn-label-color)"> {{ label }}: </span>
{{ label }}
</span>
</slot> </slot>
</div> </div>
<div class="value" v-if="value || $slots.value"> <div class="value">
<slot name="value"> <slot name="value">
<span :title="value" style="text-overflow: ellipsis"> <span :title="value" style="text-overflow: ellipsis">
{{ dash ? dashIfEmpty(value) : value }} {{ dash ? dashIfEmpty(value) : value }}
@ -75,21 +73,13 @@ const val = computed(() => $props.value);
visibility: visible; visibility: visible;
cursor: pointer; cursor: pointer;
} }
.label,
.value {
white-space: pre-line;
word-wrap: break-word;
}
.copy { .copy {
visibility: hidden; visibility: hidden;
} }
.info { .info {
margin-left: 5px; margin-left: 5px;
} }
} }
:deep(.q-checkbox.disabled) { :deep(.q-checkbox.disabled) {
opacity: 1 !important; opacity: 1 !important;
} }

View File

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

View File

@ -9,8 +9,6 @@ export function getColAlign(col) {
case 'number': case 'number':
align = 'right'; align = 'right';
break; break;
case 'time':
case 'date':
case 'checkbox': case 'checkbox':
align = 'center'; align = 'center';
break; break;

View File

@ -0,0 +1,4 @@
import { Dark } from 'quasar';
export function getDarkSuffix() {
return Dark.isActive ? '_dark' : '';
}

View File

@ -16,6 +16,8 @@ body.body--light {
--vn-black-text-color: black; --vn-black-text-color: black;
--vn-text-color-contrast: white; --vn-text-color-contrast: white;
--vn-link-color: #1e90ff; --vn-link-color: #1e90ff;
--vn-input-underline-color: #bdbdbd;
--vn-input-icons-color: #797979;
background-color: var(--vn-page-color); background-color: var(--vn-page-color);
@ -29,7 +31,7 @@ body.body--light {
body.body--dark { body.body--dark {
--vn-header-color: #5d5d5d; --vn-header-color: #5d5d5d;
--vn-page-color: #222; --vn-page-color: #222;
--vn-section-color: #3d3d3d; --vn-section-color: #3c3b3b;
--vn-section-hover-color: #747474; --vn-section-hover-color: #747474;
--vn-text-color: white; --vn-text-color: white;
--vn-label-color: #a8a8a8; --vn-label-color: #a8a8a8;
@ -40,6 +42,8 @@ body.body--dark {
--vn-black-text-color: black; --vn-black-text-color: black;
--vn-text-color-contrast: black; --vn-text-color-contrast: black;
--vn-link-color: #66bfff; --vn-link-color: #66bfff;
--vn-input-underline-color: #545353;
--vn-input-icons-color: #888787;
background-color: var(--vn-page-color); background-color: var(--vn-page-color);
@ -155,7 +159,6 @@ select:-webkit-autofill {
cursor: pointer; cursor: pointer;
} }
/* Estilo para el asterisco en campos requeridos */
.q-field.required .q-field__label:after { .q-field.required .q-field__label:after {
content: ' *'; content: ' *';
} }
@ -290,6 +293,18 @@ input::-webkit-inner-spin-button {
.expand { .expand {
max-width: 400px; max-width: 400px;
} }
th {
border-bottom: 1px solid var(--vn-page-color) !important;
}
td {
border-color: var(--vn-page-color);
}
div.q-field__append.q-field__marginal {
color: var(--vn-input-icons-color) !important;
}
.q-field__control:before {
border-color: var(--vn-input-underline-color) !important;
}
} }
.edit-photo-btn { .edit-photo-btn {

View File

@ -513,26 +513,6 @@ entry:
isRaid: Raid isRaid: Raid
invoiceNumber: Invoice invoiceNumber: Invoice
reference: Ref/Alb/Guide reference: Ref/Alb/Guide
params:
isExcludedFromAvailable: Excluir del inventario
isOrdered: Pedida
isConfirmed: Lista para etiquetar
isReceived: Recibida
isRaid: Redada
landed: Fecha
supplierFk: Proveedor
invoiceNumber: Nº Factura
reference: Ref/Alb/Guía
agencyModeId: Agencia
isBooked: Asentado
companyFk: Empresa
travelFk: Envio
evaNotes: Notas
warehouseOutFk: Origen
warehouseInFk: Destino
entryTypeDescription: Tipo entrada
invoiceAmount: Importe
dated: Fecha
ticket: ticket:
params: params:
ticketFk: Ticket ID ticketFk: Ticket ID
@ -898,6 +878,7 @@ components:
minPrice: Min. Price minPrice: Min. Price
itemFk: Item id itemFk: Item id
dated: Date dated: Date
date: Date
userPanel: userPanel:
copyToken: Token copied to clipboard copyToken: Token copied to clipboard
settings: Settings settings: Settings

View File

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

View File

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

View File

@ -8,6 +8,6 @@ import filter from './EntryFilter.js';
data-key="Entry" data-key="Entry"
url="Entries" url="Entries"
:descriptor="EntryDescriptor" :descriptor="EntryDescriptor"
:filter="{ ...filter, where: { id: $route.params.id } }" :filter="filter"
/> />
</template> </template>

View File

@ -52,5 +52,22 @@ export default {
fields: ['code', 'description'], fields: ['code', 'description'],
}, },
}, },
{
relation: 'dms',
scope: {
fields: [
'dmsTypeFk',
'reference',
'hardCopyNumber',
'workerFk',
'description',
'hasFile',
'file',
'created',
'companyFk',
'warehouseFk',
],
},
},
], ],
}; };

View File

@ -11,6 +11,8 @@ import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorP
import InvoiceIntoBook from '../InvoiceInToBook.vue'; import InvoiceIntoBook from '../InvoiceInToBook.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import InvoiceInDescriptorMenu from './InvoiceInDescriptorMenu.vue'; import InvoiceInDescriptorMenu from './InvoiceInDescriptorMenu.vue';
import VehicleDescriptorProxy from 'src/pages/Route/Vehicle/Card/VehicleDescriptorProxy.vue';
import { getTotal } from 'src/composables/getTotal';
const props = defineProps({ id: { type: [Number, String], default: 0 } }); const props = defineProps({ id: { type: [Number, String], default: 0 } });
const { t } = useI18n(); const { t } = useI18n();
@ -161,6 +163,22 @@ const intrastatColumns = ref([
}, },
]); ]);
const vehicleColumns = ref([
{
name: 'numberPlate',
label: 'globals.vehicle',
field: (row) => row.vehicleInvoiceIn?.numberPlate,
format: (value) => value,
align: 'left',
},
{
name: 'amount',
label: 'invoiceIn.list.amount',
field: (row) => toCurrency(row.vehicleInvoiceIn?.amount),
align: 'left',
},
]);
onMounted(async () => { onMounted(async () => {
invoiceInUrl.value = `${await getUrl('')}invoiceIn/${entityId.value}/`; invoiceInUrl.value = `${await getUrl('')}invoiceIn/${entityId.value}/`;
}); });
@ -218,6 +236,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<VnTitle <VnTitle
:url="getLink('basic-data')" :url="getLink('basic-data')"
:text="t('globals.pageTitles.basicData')" :text="t('globals.pageTitles.basicData')"
data-cy="basicDataTitleLink"
/> />
<div class="vn-card-group"> <div class="vn-card-group">
<div class="vn-card-content"> <div class="vn-card-content">
@ -282,7 +301,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
:value="entity.expenseDeductible?.name" :value="entity.expenseDeductible?.name"
/> />
<VnLv <VnLv
:label="t('invoiceIn.card.company')" :label="t('globals.company')"
:value="entity.company?.code" :value="entity.company?.code"
/> />
<VnLv <VnLv
@ -319,8 +338,12 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</div> </div>
</QCard> </QCard>
<!--Vat--> <!--Vat-->
<QCard v-if="entity.invoiceInTax.length" class="vat"> <QCard v-if="entity.invoiceInTax.length" class="col-extend">
<VnTitle :url="getLink('vat')" :text="t('invoiceIn.card.vat')" /> <VnTitle
:url="getLink('vat')"
:text="t('globals.pageTitles.vat')"
data-cy="vatTitleLink"
/>
<QTable <QTable
:columns="vatColumns" :columns="vatColumns"
:rows="entity.invoiceInTax" :rows="entity.invoiceInTax"
@ -372,13 +395,18 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
currency, currency,
) )
}}</QTd> }}</QTd>
<QTd></QTd>
</QTr> </QTr>
</template> </template>
</QTable> </QTable>
</QCard> </QCard>
<!--Due Day--> <!--Due Day-->
<QCard v-if="entity.invoiceInDueDay.length" class="due-day"> <QCard v-if="entity.invoiceInDueDay.length" class="col-shrink">
<VnTitle :url="getLink('due-day')" :text="t('invoiceIn.card.dueDay')" /> <VnTitle
:url="getLink('due-day')"
:text="t('globals.pageTitles.dueDay')"
data-cy="dueDayTitleLink"
/>
<QTable :columns="dueDayColumns" :rows="entity.invoiceInDueDay" flat> <QTable :columns="dueDayColumns" :rows="entity.invoiceInDueDay" flat>
<template #header="dueDayProps"> <template #header="dueDayProps">
<QTr :props="dueDayProps" class="bg"> <QTr :props="dueDayProps" class="bg">
@ -413,10 +441,11 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</QTable> </QTable>
</QCard> </QCard>
<!--Intrastat--> <!--Intrastat-->
<QCard v-if="entity.invoiceInIntrastat.length"> <QCard v-if="entity.invoiceInIntrastat.length" class="col-extend">
<VnTitle <VnTitle
:url="getLink('intrastat')" :url="getLink('intrastat')"
:text="t('invoiceIn.card.intrastat')" :text="t('globals.pageTitles.intrastat')"
data-cy="intrastatTitleLink"
/> />
<QTable <QTable
:columns="intrastatColumns" :columns="intrastatColumns"
@ -450,6 +479,53 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</template> </template>
</QTable> </QTable>
</QCard> </QCard>
<!-- Vehicle -->
<QCard v-if="entity?.vehicleInvoiceIn?.length" class="col-shrink">
<VnTitle
:url="getLink('vehicle')"
:text="t('globals.vehicle')"
data-cy="vehicleTitleLink"
/>
<QTable :columns="vehicleColumns" :rows="entity.vehicleInvoiceIn" flat>
<template #header="vehicleProps">
<QTr :props="vehicleProps" class="bg">
<QTh
v-for="col in vehicleProps.cols"
:key="col.name"
:props="vehicleProps"
>
{{ t(col.label) }}
</QTh>
</QTr>
</template>
<template #body="vehicleProps">
<QTr :props="vehicleProps">
<QTd>
<span class="link" data-cy="invoiceInSummary_vehicle">
{{ vehicleProps.row.vehicle.numberPlate }}
<VehicleDescriptorProxy
:id="vehicleProps.row.vehicleFk"
/> </span
></QTd>
<QTd align="left">{{
toCurrency(vehicleProps.row.amount)
}}</QTd>
</QTr>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd></QTd>
<QTd>
{{
toCurrency(
getTotal(entity.vehicleInvoiceIn, 'amount'),
)
}}
</QTd>
</QTr>
</template>
</QTable>
</QCard>
</template> </template>
</CardSummary> </CardSummary>
</template> </template>
@ -463,15 +539,15 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
@media (min-width: $breakpoint-md) { @media (min-width: $breakpoint-md) {
.summaryBody { .summaryBody {
.vat { .col-extend {
flex: 65%; flex: 65%;
} }
.due-day { .col-shrink {
flex: 30%; flex: 30%;
} }
.vat, .col-extend,
.due-day { .col-shrink {
.q-table th { .q-table th {
padding-right: 0; padding-right: 0;
} }

View File

@ -9,7 +9,6 @@ import FetchData from 'src/components/FetchData.vue';
import { getExchange } from 'src/composables/getExchange'; import { getExchange } from 'src/composables/getExchange';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import VnSelectExpense from 'src/components/common/VnSelectExpense.vue'; import VnSelectExpense from 'src/components/common/VnSelectExpense.vue';
import axios from 'axios';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard'; import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
const { t } = useI18n(); const { t } = useI18n();
@ -51,7 +50,7 @@ const columns = computed(() => [
create: true, create: true,
width: 'max-content', width: 'max-content',
cellEvent: { cellEvent: {
keydown: async (evt, row) => { keydown: (evt, row) => {
if (evt.key !== 'Tab') return; if (evt.key !== 'Tab') return;
const val = evt.target.value; const val = evt.target.value;
if (!val || isNaN(val)) return; if (!val || isNaN(val)) return;

View File

@ -0,0 +1,107 @@
<script setup>
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import { toCurrency } from 'src/filters';
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js';
import axios from 'axios';
import VehicleDescriptorProxy from 'src/pages/Route/Vehicle/Card/VehicleDescriptorProxy.vue';
const tableRef = ref();
const { t } = useI18n();
const route = useRoute();
const { openConfirmationModal } = useVnConfirm();
const { notify } = useNotify();
const dataKey = 'InvoiceInVehicleList';
const filter = {
include: [{ relation: 'vehicle', scope: { fields: ['id', 'numberPlate'] } }],
};
const columns = computed(() => [
{
align: 'left',
name: 'vehicleFk',
label: t('globals.vehicle'),
component: 'select',
attrs: {
url: 'vehicles',
fields: ['id', 'numberPlate'],
optionLabel: 'numberPlate',
optionFilterValue: 'numberPlate',
find: {
value: 'vehicleFk',
label: 'vehiclePlateNumber',
},
},
create: true,
format: (row) => row.vehicle?.numberPlate,
cardVisible: true,
},
{
align: 'left',
name: 'amount',
label: t('invoiceIn.list.amount'),
component: 'number',
create: true,
format: (row) => toCurrency(row.amount),
cardVisible: true,
},
{
align: 'right',
name: 'tableActions',
actions: [
{
title: t('invoiceIn.unlinkVehicle'),
icon: 'delete',
action: (row) =>
openConfirmationModal(
t('invoiceIn.unlinkVehicle'),
t('invoiceIn.unlinkVehicleConfirmation'),
() => unassignVehicle(row.id),
),
isPrimary: true,
},
],
},
]);
async function unassignVehicle(id) {
try {
await axios.delete(`VehicleInvoiceIns/${id}`);
notify(t('invoiceIn.unlinkedVehicle'), 'positive');
tableRef.value.reload();
} catch (e) {
throw e;
}
}
</script>
<template>
<VnTable
ref="tableRef"
:data-key="dataKey"
url="VehicleInvoiceIns"
:user-filter="filter"
:filter="{ where: { invoiceInFk: route.params.id } }"
:columns="columns"
:column-search="false"
:right-search="false"
:create="{
urlCreate: 'VehicleInvoiceIns',
title: t('invoiceIn.linkVehicleToInvoiceIn'),
onDataSaved: () => tableRef.reload(),
formInitialData: { invoiceInFk: route.params.id },
}"
auto-load
>
<template #column-vehicleFk="{ row }">
<span class="link" @click.stop>
{{ row.vehicle?.numberPlate }}
<VehicleDescriptorProxy :id="row?.vehicleFk" />
</span>
</template>
</VnTable>
</template>

View File

@ -34,14 +34,6 @@ invoiceIn:
originalInvoice: Original invoice originalInvoice: Original invoice
entry: Entry entry: Entry
emailEmpty: The email can't be empty emailEmpty: The email can't be empty
card:
client: Client
company: Company
customerCard: Customer card
ticketList: Ticket List
vat: Vat
dueDay: Due day
intrastat: Intrastat
summary: summary:
currency: Currency currency: Currency
issued: Expedition date issued: Expedition date
@ -71,3 +63,7 @@ invoiceIn:
correctingFk: Rectificative correctingFk: Rectificative
issued: Issued issued: Issued
noMatch: No match with the vat({totalTaxableBase}) noMatch: No match with the vat({totalTaxableBase})
linkVehicleToInvoiceIn: Link vehicle to invoice
unlinkedVehicle: Unlinked vehicle
unlinkVehicle: Unlink vehicle
unlinkVehicleConfirmation: This vehicle will be unlinked from this invoice! Continue anyway?

View File

@ -33,13 +33,6 @@ invoiceIn:
originalInvoice: Factura origen originalInvoice: Factura origen
entry: Entrada entry: Entrada
emailEmpty: El email no puede estar vacío emailEmpty: El email no puede estar vacío
card:
client: Cliente
company: Empresa
customerCard: Ficha del cliente
ticketList: Listado de tickets
vat: Iva
dueDay: Fecha de vencimiento
summary: summary:
currency: Divisa currency: Divisa
docNumber: Número documento docNumber: Número documento
@ -52,7 +45,7 @@ invoiceIn:
expense: Gasto expense: Gasto
taxableBase: Base imp. taxableBase: Base imp.
rate: Tasa rate: Tasa
sageTransaction: Sage transación sageTransaction: Sage transacción
dueDay: Fecha dueDay: Fecha
bank: Caja bank: Caja
foreignValue: Divisa foreignValue: Divisa
@ -69,3 +62,7 @@ invoiceIn:
correctingFk: Rectificativa correctingFk: Rectificativa
issued: Fecha de emisión issued: Fecha de emisión
noMatch: No cuadra con el iva({totalTaxableBase}) noMatch: No cuadra con el iva({totalTaxableBase})
linkVehicleToInvoiceIn: Vincular vehículo a factura
unlinkedVehicle: Vehículo desvinculado
unlinkVehicle: Desvincular vehículo
unlinkVehicleConfirmation: Este vehículo se desvinculará de esta factura! ¿Continuar de todas formas?

View File

@ -61,7 +61,7 @@ const onIntrastatCreated = (response, formData) => {
:clear-store-on-unmount="false" :clear-store-on-unmount="false"
> >
<template #form="{ data }"> <template #form="{ data }">
<VnRow> <VnRow class="q-py-sm">
<VnSelect <VnSelect
:label="t('item.basicData.type')" :label="t('item.basicData.type')"
v-model="data.typeFk" v-model="data.typeFk"
@ -71,6 +71,7 @@ const onIntrastatCreated = (response, formData) => {
hide-selected hide-selected
map-options map-options
required required
data-cy="itemBasicDataItemType"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -83,23 +84,30 @@ const onIntrastatCreated = (response, formData) => {
</QItem> </QItem>
</template> </template>
</VnSelect> </VnSelect>
<VnInput :label="t('item.basicData.reference')" v-model="data.comment" /> <VnInput
:label="t('item.basicData.reference')"
v-model="data.comment"
data-cy="itemBasicDataReference"
/>
<VnInput <VnInput
:label="t('item.basicData.relevancy')" :label="t('item.basicData.relevancy')"
type="number" type="number"
v-model="data.relevancy" v-model="data.relevancy"
data-cy="itemBasicDataRelevancy"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="q-py-sm">
<VnInput <VnInput
:label="t('item.basicData.stems')" :label="t('item.basicData.stems')"
type="number" type="number"
v-model="data.stems" v-model="data.stems"
data-cy="itemBasicDataStems"
/> />
<VnInput <VnInput
:label="t('item.basicData.multiplier')" :label="t('item.basicData.multiplier')"
type="number" type="number"
v-model="data.stemMultiplier" v-model="data.stemMultiplier"
data-cy="itemBasicDataMultiplier"
/> />
<VnSelectDialog <VnSelectDialog
:label="t('item.basicData.generic')" :label="t('item.basicData.generic')"
@ -112,6 +120,7 @@ const onIntrastatCreated = (response, formData) => {
map-options map-options
hide-selected hide-selected
action-icon="filter_alt" action-icon="filter_alt"
data-cy="itemBasicDataGeneric"
> >
<template #form> <template #form>
<FilterItemForm <FilterItemForm
@ -129,7 +138,12 @@ const onIntrastatCreated = (response, formData) => {
</template> </template>
</VnSelectDialog> </VnSelectDialog>
</VnRow> </VnRow>
<VnRow> <VnRow class="q-py-sm">
<VnCheckbox
v-model="data.isCustomInspectionRequired"
:label="t('item.basicData.isCustomInspectionRequired')"
data-cy="itemBasicDataCustomInspection"
/>
<VnSelectDialog <VnSelectDialog
:label="t('item.basicData.intrastat')" :label="t('item.basicData.intrastat')"
v-model="data.intrastatFk" v-model="data.intrastatFk"
@ -138,6 +152,7 @@ const onIntrastatCreated = (response, formData) => {
option-label="description" option-label="description"
map-options map-options
hide-selected hide-selected
data-cy="itemBasicDataIntrastat"
> >
<template #form> <template #form>
<CreateIntrastatForm <CreateIntrastatForm
@ -165,78 +180,81 @@ const onIntrastatCreated = (response, formData) => {
option-label="name" option-label="name"
hide-selected hide-selected
map-options map-options
data-cy="itemBasicDataExpense"
/> />
</div> </div>
</VnRow> </VnRow>
<VnRow> <VnRow class="q-py-sm">
<VnCheckbox
v-model="data.hasKgPrice"
:label="t('item.basicData.hasKgPrice')"
data-cy="itemBasicDataHasKgPrice"
/>
<VnInput <VnInput
:label="t('item.basicData.weightByPiece')" :label="t('item.basicData.weightByPiece')"
v-model.number="data.weightByPiece" v-model.number="data.weightByPiece"
:min="0" :min="0"
type="number" type="number"
data-cy="itemBasicDataWeightByPiece"
/> />
<VnInput <VnInput
:label="t('item.basicData.boxUnits')" :label="t('item.basicData.boxUnits')"
v-model.number="data.packingOut" v-model.number="data.packingOut"
:min="0" :min="0"
type="number" type="number"
data-cy="itemBasicDataBoxUnits"
/> />
</VnRow>
<VnRow class="q-py-sm">
<VnCheckbox
v-model="data.isActive"
:label="t('item.basicData.isActive')"
data-cy="itemBasicDataIsActive"
/>
<VnCheckbox
v-model="data.isFragile"
:label="t('item.basicData.isFragile')"
:info="t('item.basicData.isFragileTooltip')"
data-cy="itemBasicDataIsFragile"
/>
<VnCheckbox
v-model="data.isPhotoRequested"
:label="t('item.basicData.isPhotoRequested')"
:info="t('item.basicData.isPhotoRequestedTooltip')"
data-cy="itemBasicDataIsPhotoRequested"
/>
</VnRow>
<VnRow class="q-py-sm">
<VnInput <VnInput
:label="t('item.basicData.recycledPlastic')" :label="t('item.basicData.recycledPlastic')"
v-model.number="data.recycledPlastic" v-model.number="data.recycledPlastic"
:min="0" :min="0"
type="number" type="number"
data-cy="itemBasicDataRecycledPlastic"
/> />
<VnInput <VnInput
:label="t('item.basicData.nonRecycledPlastic')" :label="t('item.basicData.nonRecycledPlastic')"
v-model.number="data.nonRecycledPlastic" v-model.number="data.nonRecycledPlastic"
:min="0" :min="0"
type="number" type="number"
data-cy="itemBasicDataNonRecycledPlastic"
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="q-py-sm">
<QCheckbox
v-model="data.isActive"
:label="t('item.basicData.isActive')"
/>
<QCheckbox
v-model="data.hasKgPrice"
:label="t('item.basicData.hasKgPrice')"
/>
<QCheckbox
v-model="data.isCustomInspectionRequired"
:label="t('item.basicData.isCustomInspectionRequired')"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnCheckbox
v-model="data.isFragile"
:label="t('item.basicData.isFragile')"
:info="t('item.basicData.isFragileTooltip')"
class="q-mr-sm"
size="xs"
/>
<VnCheckbox
v-model="data.isPhotoRequested"
:label="t('item.basicData.isPhotoRequested')"
:info="t('item.basicData.isPhotoRequestedTooltip')"
class="q-mr-sm"
size="xs"
/>
</VnRow>
<VnRow>
<VnInput <VnInput
:label="t('item.basicData.description')" :label="t('item.basicData.description')"
type="textarea" type="textarea"
v-model="data.description" v-model="data.description"
fill-input fill-input
data-cy="itemBasicDataDescription"
/> />
<VnInput <VnInput
v-show="data.isPhotoRequested"
type="textarea" type="textarea"
:label="t('globals.comment')" :label="t('item.basicData.photoMotivation')"
v-model="data.photoMotivation" v-model="data.photoMotivation"
fill-input fill-input
data-cy="itemBasicDataPhotoMotivation"
/> />
</VnRow> </VnRow>
</template> </template>

View File

@ -102,20 +102,21 @@ const columns = computed(() => [
label: t('itemDiary.in'), label: t('itemDiary.in'),
field: 'invalue', field: 'invalue',
name: 'in', name: 'in',
align: 'left', align: 'right',
format: (val) => dashIfEmpty(val), format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('itemDiary.out'), label: t('itemDiary.out'),
field: 'out', field: 'out',
name: 'out', name: 'out',
align: 'left', align: 'right',
format: (val) => dashIfEmpty(val), format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('itemDiary.balance'), label: t('itemDiary.balance'),
name: 'balance', name: 'balance',
align: 'left', align: 'right',
class: 'q-px-sm',
}, },
]); ]);
@ -174,7 +175,11 @@ async function updateWarehouse(warehouseFk) {
<template> <template>
<FetchData <FetchData
url="Warehouses" url="Warehouses"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }" :filter="{
fields: ['id', 'name'],
order: 'name ASC',
where: { isDestiny: true },
}"
auto-load auto-load
@on-fetch="(data) => (warehousesOptions = data)" @on-fetch="(data) => (warehousesOptions = data)"
/> />
@ -217,7 +222,8 @@ async function updateWarehouse(warehouseFk) {
<QTable <QTable
:rows="itemBalances" :rows="itemBalances"
:columns="columns" :columns="columns"
class="full-width q-mt-md" class="full-width q-mt-md q-px-md"
style="background-color: var(--vn-section-color)"
:no-data-label="t('globals.noResults')" :no-data-label="t('globals.noResults')"
> >
<template #body-cell-claim="{ row }"> <template #body-cell-claim="{ row }">
@ -294,14 +300,14 @@ async function updateWarehouse(warehouseFk) {
</QTd> </QTd>
</template> </template>
<template #body-cell-in="{ row }"> <template #body-cell-in="{ row }">
<QTd @click.stop> <QTd @click.stop class="text-right">
<span :class="{ 'is-in': row.invalue }"> <span :class="{ 'is-in': row.invalue }">
{{ dashIfEmpty(row.invalue) }} {{ dashIfEmpty(row.invalue) }}
</span> </span>
</QTd> </QTd>
</template> </template>
<template #body-cell-balance="{ row }"> <template #body-cell-balance="{ row }">
<QTd @click.stop> <QTd @click.stop class="text-right">
<QBadge <QBadge
class="balance-negative" class="balance-negative"
:color=" :color="

View File

@ -48,7 +48,7 @@ const columns = computed(() => [
label: t('itemDiary.warehouse'), label: t('itemDiary.warehouse'),
name: 'warehouse', name: 'warehouse',
field: 'warehouse', field: 'warehouse',
align: 'center', align: 'left',
}, },
{ {
label: t('lastEntries.landed'), label: t('lastEntries.landed'),
@ -60,7 +60,7 @@ const columns = computed(() => [
label: t('lastEntries.entry'), label: t('lastEntries.entry'),
name: 'entry', name: 'entry',
field: 'stateName', field: 'stateName',
align: 'center', align: 'right',
format: (val) => dashIfEmpty(val), format: (val) => dashIfEmpty(val),
}, },
{ {
@ -75,14 +75,14 @@ const columns = computed(() => [
label: t('lastEntries.printedStickers'), label: t('lastEntries.printedStickers'),
name: 'printedStickers', name: 'printedStickers',
field: 'printedStickers', field: 'printedStickers',
align: 'center', align: 'right',
format: (val) => dashIfEmpty(val), format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('lastEntries.label'), label: t('lastEntries.label'),
name: 'stickers', name: 'stickers',
field: 'stickers', field: 'stickers',
align: 'center', align: 'right',
format: (val) => dashIfEmpty(val), format: (val) => dashIfEmpty(val),
style: (row) => highlightedRow(row), style: (row) => highlightedRow(row),
}, },
@ -90,39 +90,39 @@ const columns = computed(() => [
label: 'Packing', label: 'Packing',
name: 'packing', name: 'packing',
field: 'packing', field: 'packing',
align: 'center', align: 'right',
}, },
{ {
label: t('lastEntries.grouping'), label: t('lastEntries.grouping'),
name: 'grouping', name: 'grouping',
field: 'grouping', field: 'grouping',
align: 'center', align: 'right',
}, },
{ {
label: t('itemBasicData.stems'), label: t('itemBasicData.stems'),
name: 'stems', name: 'stems',
field: 'stems', field: 'stems',
align: 'center', align: 'right',
style: (row) => highlightedRow(row), style: (row) => highlightedRow(row),
}, },
{ {
label: t('lastEntries.quantity'), label: t('lastEntries.quantity'),
name: 'quantity', name: 'quantity',
field: 'quantity', field: 'quantity',
align: 'center', align: 'right',
style: (row) => highlightedRow(row), style: (row) => highlightedRow(row),
}, },
{ {
label: t('lastEntries.cost'), label: t('lastEntries.cost'),
name: 'cost', name: 'cost',
field: 'cost', field: 'right',
align: 'center', align: 'right',
}, },
{ {
label: 'Kg', label: 'Kg',
name: 'weight', name: 'weight',
field: 'weight', field: 'weight',
align: 'center', align: 'right',
style: (row) => highlightedRow(row), style: (row) => highlightedRow(row),
}, },
{ {
@ -136,7 +136,7 @@ const columns = computed(() => [
label: t('lastEntries.supplier'), label: t('lastEntries.supplier'),
name: 'supplier', name: 'supplier',
field: 'supplier', field: 'supplier',
align: 'center', align: 'left',
}, },
]); ]);
@ -269,7 +269,7 @@ function highlightedRow(row) {
</template> </template>
<template #body-cell-entry="{ row }"> <template #body-cell-entry="{ row }">
<QTd @click.stop :style="highlightedRow(row)"> <QTd @click.stop :style="highlightedRow(row)">
<div class="full-width flex justify-center"> <div class="full-width text-right">
<EntryDescriptorProxy :id="row.entryFk" class="q-ma-none" dense /> <EntryDescriptorProxy :id="row.entryFk" class="q-ma-none" dense />
<span class="link">{{ row.entryFk }}</span> <span class="link">{{ row.entryFk }}</span>
</div> </div>
@ -282,16 +282,16 @@ function highlightedRow(row) {
</QTd> </QTd>
</template> </template>
<template #body-cell-printedStickers="{ row }"> <template #body-cell-printedStickers="{ row }">
<QTd @click.stop class="text-center" :style="highlightedRow(row)"> <QTd @click.stop class="text-right" :style="highlightedRow(row)">
<span style="color: var(--vn-label-color)"> <span style="color: var(--vn-label-color)">
{{ row.printedStickers }}</span {{ row.printedStickers }}</span
> >
</QTd> </QTd>
</template> </template>
<template #body-cell-packing="{ row }"> <template #body-cell-packing="{ row }">
<QTd @click.stop :style="highlightedRow(row)"> <QTd @click.stop :style="highlightedRow(row)" class="text-right">
<QBadge <QBadge
class="center-content" class="grouping-badge"
:class="getBadgeClass(row.groupingMode, 'packing')" :class="getBadgeClass(row.groupingMode, 'packing')"
rounded rounded
> >
@ -301,9 +301,9 @@ function highlightedRow(row) {
</QTd> </QTd>
</template> </template>
<template #body-cell-grouping="{ row }"> <template #body-cell-grouping="{ row }">
<QTd @click.stop :style="highlightedRow(row)"> <QTd @click.stop :style="highlightedRow(row)" class="text-right">
<QBadge <QBadge
class="center-content" class="grouping-badge"
:class="getBadgeClass(row.groupingMode, 'grouping')" :class="getBadgeClass(row.groupingMode, 'grouping')"
rounded rounded
> >
@ -313,7 +313,7 @@ function highlightedRow(row) {
</QTd> </QTd>
</template> </template>
<template #body-cell-cost="{ row }"> <template #body-cell-cost="{ row }">
<QTd @click.stop class="text-center" :style="highlightedRow(row)"> <QTd @click.stop class="text-right" :style="highlightedRow(row)">
<span> <span>
{{ toCurrency(row.cost, 'EUR', 3) }} {{ toCurrency(row.cost, 'EUR', 3) }}
<QTooltip> <QTooltip>
@ -357,10 +357,7 @@ function highlightedRow(row) {
.q-badge--rounded { .q-badge--rounded {
border-radius: 50%; border-radius: 50%;
} }
.center-content { .grouping-badge {
display: flex;
max-width: max-content;
margin: auto;
padding: 0 11px; padding: 0 11px;
height: 28px; height: 28px;
} }

View File

@ -58,7 +58,7 @@ const columns = computed(() => [
{ {
label: t('shelvings.item'), label: t('shelvings.item'),
name: 'itemFk', name: 'itemFk',
align: 'left', align: 'right',
columnFilter: false, columnFilter: false,
}, },
{ {
@ -102,19 +102,20 @@ const columns = computed(() => [
name: 'label', name: 'label',
align: 'left', align: 'left',
columnFilter: { inWhere: true }, columnFilter: { inWhere: true },
component: 'number',
format: (row) => (row.stock / row.packing).toFixed(2), format: (row) => (row.stock / row.packing).toFixed(2),
}, },
{ {
label: t('shelvings.packing'), label: t('shelvings.packing'),
name: 'packing', name: 'packing',
attrs: { inWhere: true }, attrs: { inWhere: true },
align: 'left', component: 'number',
}, },
{ {
label: t('globals.visible'), label: t('globals.visible'),
name: 'stock', name: 'stock',
attrs: { inWhere: true }, attrs: { inWhere: true },
align: 'left', component: 'number',
}, },
]); ]);
@ -138,21 +139,12 @@ watchEffect(selectedRows);
<template> <template>
<template v-if="stateStore.isHeaderMounted()"> <template v-if="stateStore.isHeaderMounted()">
<Teleport to="#st-data"> <Teleport to="#st-data">
<div class="q-pa-md q-mr-lg q-ma-xs" style="border: 2px solid #222"> <QCardSection class="column items-center" horizontal>
<QCardSection horizontal> <div>
<span class="text-weight-bold text-subtitle1 text-center full-width"> <span class="details-label">{{ t('shelvings.totalLabels') }} </span>
{{ t('shelvings.total') }} <span>: {{ totalLabels }}</span>
</span> </div>
</QCardSection> </QCardSection>
<QCardSection class="column items-center" horizontal>
<div>
<span class="details-label"
>{{ t('shelvings.totalLabels') }}
</span>
<span>: {{ totalLabels }}</span>
</div></QCardSection
>
</div>
</Teleport> </Teleport>
<Teleport to="#st-actions"> <Teleport to="#st-actions">
<QBtn <QBtn

View File

@ -2,6 +2,7 @@
import { computed } from 'vue'; import { computed } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { dashIfEmpty } from 'src/filters';
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';
@ -48,7 +49,7 @@ const getUrl = (id, param) => `#/Item/${id}/${param}`;
<ItemDescriptorMenu :entity-id="entityId" :warehouse-fk="warehouseFk" /> <ItemDescriptorMenu :entity-id="entityId" :warehouse-fk="warehouseFk" />
</template> </template>
<template #body="{ entity: { item, tags, visible, available, botanical } }"> <template #body="{ entity: { item, tags, visible, available, botanical } }">
<QCard class="vn-one photo"> <QCard class="vn-one photo" v-if="$route.name != 'ItemSummary'">
<ItemDescriptorImage <ItemDescriptorImage
:entity-id="entityId" :entity-id="entityId"
:visible="visible" :visible="visible"
@ -56,84 +57,108 @@ const getUrl = (id, param) => `#/Item/${id}/${param}`;
:show-edit-button="false" :show-edit-button="false"
/> />
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-two">
<VnTitle <VnTitle
:url="getUrl(entityId, 'basic-data')" :url="getUrl(entityId, 'basic-data')"
:text="t('globals.summary.basicData')" :text="t('globals.summary.basicData')"
/> />
<VnLv :label="t('globals.name')" :value="item.name" /> <div class="vn-card-group">
<VnLv :label="t('item.summary.completeName')" :value="item.longName" /> <div class="vn-card-content">
<VnLv :label="t('item.summary.family')" :value="item.itemType.name" /> <VnLv :label="t('globals.name')" :value="item.name" />
<VnLv :label="t('globals.size')" :value="item.size" /> <VnLv
<VnLv :label="t('globals.origin')" :value="item.origin.name" /> :label="t('item.summary.completeName')"
<VnLv :label="t('item.summary.stems')" :value="item.stems" /> :value="item.longName"
<VnLv />
:label="t('item.summary.multiplier')" <VnLv
:value="item.stemMultiplier" :label="t('item.summary.family')"
/> :value="item.itemType.name"
/>
<VnLv :label="t('globals.size')" :value="item.size" />
<VnLv :label="t('globals.origin')" :value="item.origin.name" />
<VnLv :label="t('item.summary.stems')" :value="item.stems" />
<VnLv
:label="t('item.summary.multiplier')"
:value="item.stemMultiplier"
/>
<VnLv :label="t('item.summary.buyer')"> <VnLv :label="t('item.summary.buyer')">
<template #value> <template #value>
<VnUserLink <VnUserLink
:name="item.itemType.worker.user.name" :name="item.itemType.worker.user.name"
:worker-id="item.itemType.worker.id" :worker-id="item.itemType.worker.id"
/>
</template>
</VnLv>
<VnLv :info="t('Este artículo necesita una foto')">
<template #value>
<QCheckbox
:label="t('item.summary.doPhoto')"
v-model="item.isPhotoRequested"
:disable="true"
size="xs"
/>
</template>
</VnLv>
<VnLv :label="t('globals.description')">
<template #value>
<span
style="
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
"
v-text="dashIfEmpty(item.description)"
/>
</template>
</VnLv>
</div>
<div class="vn-card-content">
<VnLv
:label="t('item.summary.intrastatCode')"
:value="item.intrastat.id"
/> />
</template> <VnLv
</VnLv> :label="t('globals.intrastat')"
<VnLv :info="t('Este artículo necesita una foto')"> :value="item.intrastat.description"
<template #value>
<QCheckbox
:label="t('item.summary.doPhoto')"
v-model="item.isPhotoRequested"
:disable="true"
/> />
</template> <VnLv :label="t('item.summary.ref')" :value="item.comment" />
</VnLv> <VnLv
</QCard> :label="t('item.summary.relevance')"
<QCard class="vn-one"> :value="item.relevancy"
<VnTitle />
:url="getUrl(entityId, 'basic-data')" <VnLv
:text="t('item.summary.otherData')" :label="t('item.summary.weight')"
/> :value="item.weightByPiece"
<VnLv />
:label="t('item.summary.intrastatCode')" <VnLv :label="t('item.summary.units')" :value="item.packingOut" />
:value="item.intrastat.id" <VnLv
/> :label="t('item.summary.expense')"
<VnLv :value="item.expense.name"
:label="t('globals.intrastat')" />
:value="item.intrastat.description" <VnLv
/> :label="t('item.summary.generic')"
<VnLv :label="t('item.summary.ref')" :value="item.comment" /> :value="item.genericFk"
<VnLv :label="t('item.summary.relevance')" :value="item.relevancy" /> />
<VnLv :label="t('item.summary.weight')" :value="item.weightByPiece" /> <VnLv
<VnLv :label="t('item.summary.units')" :value="item.packingOut" /> :label="t('item.summary.recycledPlastic')"
<VnLv :label="t('item.summary.expense')" :value="item.expense.name" /> :value="item.recycledPlastic"
<VnLv :label="t('item.summary.generic')" :value="item.genericFk" /> />
<VnLv <VnLv
:label="t('item.summary.recycledPlastic')" :label="t('item.summary.nonRecycledPlastic')"
:value="item.recycledPlastic" :value="item.nonRecycledPlastic"
/> />
<VnLv </div>
:label="t('item.summary.nonRecycledPlastic')" </div>
:value="item.nonRecycledPlastic"
/>
</QCard> </QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle :url="getUrl(entityId, 'tags')" :text="t('globals.tags')" /> <VnTitle :url="getUrl(entityId, 'tags')" :text="t('globals.tags')" />
<VnLv <VnLv
v-for="(tag, index) in tags" v-for="(tag, index) in tags"
:key="index" :key="index"
:label="`${tag.priority} ${tag.tag.name}:`" :label="`${tag.priority} ${tag.tag.name}`"
:value="tag.value" :value="tag.value"
/> />
</QCard> </QCard>
<QCard class="vn-one" v-if="item.description">
<VnTitle
:url="getUrl(entityId, 'basic-data')"
:text="t('globals.description')"
/>
<p v-text="item.description" />
</QCard>
<QCard class="vn-one"> <QCard class="vn-one">
<VnTitle :url="getUrl(entityId, 'tax')" :text="t('item.summary.tax')" /> <VnTitle :url="getUrl(entityId, 'tax')" :text="t('item.summary.tax')" />
<VnLv <VnLv

View File

@ -17,7 +17,7 @@ const itemTagsRef = ref();
const tagOptions = ref([]); const tagOptions = ref([]);
const valueOptionsMap = ref(new Map()); const valueOptionsMap = ref(new Map());
const getSelectedTagValues = async (tag) => { const getSelectedTagValues = async (tag) => {
if (!tag.tagFk && tag.tag.isFree) return; if (!tag.tagFk && tag.tag?.isFree) return;
const filter = { const filter = {
fields: ['value'], fields: ['value'],
order: 'value ASC', order: 'value ASC',
@ -25,6 +25,7 @@ const getSelectedTagValues = async (tag) => {
}; };
const params = { filter: JSON.stringify(filter) }; const params = { filter: JSON.stringify(filter) };
if (!tag.tagFk) return;
const { data } = await axios.get(`Tags/${tag.tagFk}/filterValue`, { const { data } = await axios.get(`Tags/${tag.tagFk}/filterValue`, {
params, params,
}); });
@ -82,7 +83,6 @@ const insertTag = (rows) => {
value: undefined, value: undefined,
name: undefined, name: undefined,
}, },
}" }"
:data-default="{ :data-default="{
tag: { tag: {
@ -113,7 +113,7 @@ const insertTag = (rows) => {
<VnRow <VnRow
v-for="(row, index) in rows" v-for="(row, index) in rows"
:key="index" :key="index"
class="items-center" class="items-center q-py-sm"
> >
<VnSelect <VnSelect
:label="t('itemTags.tag')" :label="t('itemTags.tag')"
@ -139,9 +139,7 @@ const insertTag = (rows) => {
emit-value emit-value
use-input use-input
class="col" class="col"
:is-clearable="false"
:required="false" :required="false"
:rules="validate('itemTag.tagFk')"
:use-like="false" :use-like="false"
sort-by="value" sort-by="value"
/> />
@ -152,7 +150,6 @@ const insertTag = (rows) => {
v-model="row.value" v-model="row.value"
:label="t('itemTags.value')" :label="t('itemTags.value')"
:is-clearable="false" :is-clearable="false"
@keyup.enter.stop="(data) => itemTagsRef.onSubmit(data)"
:data-cy="`tag${row?.tag?.name}Value`" :data-cy="`tag${row?.tag?.name}Value`"
/> />
<VnInput <VnInput
@ -161,7 +158,6 @@ const insertTag = (rows) => {
v-model="row.priority" v-model="row.priority"
:required="true" :required="true"
:rules="validate('itemTag.priority')" :rules="validate('itemTag.priority')"
@keyup.enter.stop="(data) => itemTagsRef.onSubmit(data)"
/> />
<div class="row justify-center" style="flex: 0"> <div class="row justify-center" style="flex: 0">
<QIcon <QIcon
@ -188,11 +184,8 @@ const insertTag = (rows) => {
v-shortcut="'+'" v-shortcut="'+'"
fab fab
data-cy="createNewTag" data-cy="createNewTag"
> :title="t('globals.add')"
<QTooltip> />
{{ t('itemTags.addTag') }}
</QTooltip>
</QBtn>
</QPageSticky> </QPageSticky>
</template> </template>
</CrudModel> </CrudModel>

View File

@ -64,27 +64,29 @@ const submitTaxes = async (data) => {
auto-load auto-load
> >
<template #body="{ rows }"> <template #body="{ rows }">
<QCard class="q-px-lg q-py-md"> <div style="display: flex; justify-content: center">
<VnRow <QCard class="q-px-lg q-py-md">
v-for="(row, index) in rows" <VnRow
:key="index" v-for="(row, index) in rows"
class="row q-gutter-md q-mb-md" :key="index"
> class="row q-gutter-md q-mb-md"
<VnInput >
:label="t('tax.country')" <VnInput
v-model="row.country.name" :label="t('tax.country')"
disable v-model="row.country.name"
/> disable
<VnSelect />
:label="t('tax.class')" <VnSelect
v-model="row.taxClassFk" :label="t('tax.class')"
:options="taxesOptions" v-model="row.taxClassFk"
option-label="description" :options="taxesOptions"
option-value="id" option-label="description"
hide-selected option-value="id"
/> hide-selected
</VnRow> />
</QCard> </VnRow>
</QCard>
</div>
</template> </template>
</CrudModel> </CrudModel>
</template> </template>

View File

@ -1,7 +1,6 @@
<script setup> <script setup>
import { ref, computed, onBeforeMount } from 'vue'; import { ref, computed, onBeforeMount } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import VnImg from 'src/components/ui/VnImg.vue'; import VnImg from 'src/components/ui/VnImg.vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import { toDate } from 'src/filters'; import { toDate } from 'src/filters';
@ -18,16 +17,13 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import axios from 'axios'; import axios from 'axios';
import VnSection from 'src/components/common/VnSection.vue'; import VnSection from 'src/components/common/VnSection.vue';
const entityId = computed(() => route.params.id);
const { openCloneDialog } = cloneItem(); const { openCloneDialog } = cloneItem();
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const { t } = useI18n(); const { t } = useI18n();
const tableRef = ref(); const tableRef = ref();
const route = useRoute();
const dataKey = 'ItemList'; const dataKey = 'ItemList';
const validPriorities = ref([]); const validPriorities = ref([]);
const defaultTag = ref(); const defaultItem = ref(null);
const defaultPriority = ref();
const itemFilter = { const itemFilter = {
include: [ include: [
@ -59,15 +55,14 @@ const itemFilter = {
}; };
const columns = computed(() => [ const columns = computed(() => [
{ {
label: '',
name: 'image', name: 'image',
align: 'left', align: 'left',
columnFilter: false, columnFilter: false,
}, },
{ {
align: 'right',
label: t('item.list.id'), label: t('item.list.id'),
name: 'id', name: 'id',
align: 'left',
isId: true, isId: true,
chip: { chip: {
condition: () => true, condition: () => true,
@ -75,36 +70,36 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
}, },
{ {
align: 'right',
label: t('entry.summary.grouping'), label: t('entry.summary.grouping'),
name: 'grouping', name: 'grouping',
align: 'left',
columnFilter: { columnFilter: {
component: 'number', component: 'number',
inWhere: true, inWhere: true,
}, },
}, },
{ {
align: 'right',
label: t('entry.summary.packing'), label: t('entry.summary.packing'),
name: 'packing', name: 'packing',
align: 'left',
columnFilter: { columnFilter: {
component: 'number', component: 'number',
inWhere: true, inWhere: true,
}, },
}, },
{ {
align: 'left',
label: t('globals.description'), label: t('globals.description'),
name: 'description', name: 'description',
align: 'left',
columnFilter: { columnFilter: {
name: 'search', name: 'search',
}, },
columnClass: 'expand', columnClass: 'expand',
}, },
{ {
align: 'right',
label: t('item.list.stems'), label: t('item.list.stems'),
name: 'stems', name: 'stems',
align: 'left',
columnFilter: { columnFilter: {
component: 'number', component: 'number',
inWhere: true, inWhere: true,
@ -112,19 +107,20 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
}, },
{ {
align: 'right',
label: t('globals.size'), label: t('globals.size'),
name: 'size', name: 'size',
align: 'left',
columnFilter: { columnFilter: {
component: 'number', component: 'number',
inWhere: true, inWhere: true,
}, },
cardVisible: true, cardVisible: true,
columnClass: 'expand',
}, },
{ {
align: 'left',
label: t('item.list.typeName'), label: t('item.list.typeName'),
name: 'typeFk', name: 'typeFk',
align: 'left',
component: 'select', component: 'select',
attrs: { attrs: {
url: 'ItemTypes', url: 'ItemTypes',
@ -173,26 +169,17 @@ const columns = computed(() => [
}, },
{ {
label: t('globals.intrastat'), label: t('globals.intrastat'),
name: 'intrastat', name: 'intrastatFk',
align: 'left', align: 'left',
component: 'select', component: 'select',
attrs: { attrs: {
url: 'Intrastats', url: 'Intrastats',
optionValue: 'description', fields: ['id', 'description'],
optionLabel: 'description', optionLabel: 'description',
}, optionValue: 'id',
columnFilter: {
name: 'intrastat',
attrs: {
url: 'Intrastats',
optionValue: 'description',
optionLabel: 'description',
},
},
columnField: {
component: null,
}, },
cardVisible: true, cardVisible: true,
format: (row, dashIfEmpty) => dashIfEmpty(row.intrastat),
}, },
{ {
label: t('item.list.origin'), label: t('item.list.origin'),
@ -238,21 +225,15 @@ const columns = computed(() => [
}, },
}, },
{ {
label: t('item.list.weight'), label: t('item.list.weightByPiece'),
toolTip: t('item.list.weightByPiece'), toolTip: t('item.list.weightByPiece'),
name: 'weightByPiece', name: 'weightByPiece',
component: 'input', component: 'input',
columnField: {
component: null,
},
columnFilter: {
inWhere: true,
},
}, },
{ {
align: 'right',
label: t('item.list.stemMultiplier'), label: t('item.list.stemMultiplier'),
name: 'stemMultiplier', name: 'stemMultiplier',
align: 'left',
component: 'input', component: 'input',
columnField: { columnField: {
component: null, component: null,
@ -301,7 +282,6 @@ const columns = computed(() => [
actions: [ actions: [
{ {
title: t('globals.clone'), title: t('globals.clone'),
icon: 'vn:clone', icon: 'vn:clone',
action: openCloneDialog, action: openCloneDialog,
isPrimary: true, isPrimary: true,
@ -317,15 +297,10 @@ const columns = computed(() => [
]); ]);
onBeforeMount(async () => { onBeforeMount(async () => {
const { data } = await axios.get('ItemConfigs'); const { data } = await axios.get('ItemConfigs/findOne');
defaultTag.value = data[0].defaultTag; defaultItem.value = data;
defaultPriority.value = data[0].defaultPriority;
data.forEach((priority) => {
validPriorities.value = priority.validPriorities;
});
}); });
</script> </script>
<template> <template>
<VnSection <VnSection
:data-key="dataKey" :data-key="dataKey"
@ -338,27 +313,29 @@ onBeforeMount(async () => {
}" }"
> >
<template #advanced-menu> <template #advanced-menu>
<ItemListFilter data-key="ItemList" /> <ItemListFilter :data-key="dataKey" />
</template> </template>
<template #body> <template #body>
<VnTable <VnTable
v-if="defaultTag" v-if="defaultItem"
ref="tableRef"
:data-key="dataKey" :data-key="dataKey"
:columns="columns" ref="tableRef"
:right-search="false" search-url="ItemList"
redirect="Item" url="Items/filter"
:filter="itemFilter"
:create="{ :create="{
urlCreate: 'Items/new', urlCreate: 'Items/new',
title: t('item.list.newItem'), title: t('item.list.newItem'),
onDataSaved: ({ id }) => tableRef.redirect(`${id}/basic-data`), onDataSaved: ({ id }) => tableRef.redirect(`${id}/basic-data`),
formInitialData: { formInitialData: {
editorFk: entityId, tag: defaultItem?.defaultTag,
tag: defaultTag, priority: defaultItem?.defaultPriority,
priority: defaultPriority,
}, },
}" }"
:is-editable="false" :columns="columns"
redirect="Item"
:right-search="false"
auto-load
> >
<template #column-image="{ row }"> <template #column-image="{ row }">
<VnImg <VnImg
@ -374,10 +351,18 @@ onBeforeMount(async () => {
<ItemDescriptorProxy :id="row.id" /> <ItemDescriptorProxy :id="row.id" />
</span> </span>
</template> </template>
<template #column-description="{ row }">
<span class="row column full-width justify-between items-start">
{{ row?.name }}
<span class="subName">
{{ row?.subName?.toUpperCase() }} &nbsp;
</span>
</span>
<FetchedTags :item="row" :columns="3" />
</template>
<template #column-typeName="{ row }"> <template #column-typeName="{ row }">
<span class="link" @click.stop> <span class="link" @click.stop>
{{ row.typeName }} {{ row.typeName }}
{{ row.typeFk }}
<ItemTypeDescriptorProxy :id="row.typeFk" /> <ItemTypeDescriptorProxy :id="row.typeFk" />
</span> </span>
</template> </template>
@ -387,20 +372,11 @@ onBeforeMount(async () => {
<WorkerDescriptorProxy :id="row.buyerFk" /> <WorkerDescriptorProxy :id="row.buyerFk" />
</span> </span>
</template> </template>
<template #column-description="{ row }">
<div class="row column full-width justify-between items-start">
{{ row?.name }}
<div v-if="row?.subName" class="subName">
{{ row?.subName.toUpperCase() }}
</div>
</div>
<FetchedTags :item="row" :columns="3" />
</template>
<template #more-create-dialog="{ data }"> <template #more-create-dialog="{ data }">
<VnInput <VnInput
v-model="data.provisionalName" v-model="data.provisionalName"
:label="t('globals.description')" :label="t('Provisional name')"
:is-required="true" :required="true"
/> />
<VnSelect <VnSelect
url="Tags" url="Tags"
@ -410,7 +386,7 @@ onBeforeMount(async () => {
option-label="name" option-label="name"
option-value="id" option-value="id"
:is-required="true" :is-required="true"
:sort-by="['name ASC']" :order="['name ASC']"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -427,7 +403,7 @@ onBeforeMount(async () => {
:options="validPriorities" :options="validPriorities"
v-model="data.priority" v-model="data.priority"
:label="t('item.create.priority')" :label="t('item.create.priority')"
:is-required="true" :required="true"
/> />
<VnSelect <VnSelect
url="ItemTypes" url="ItemTypes"
@ -436,7 +412,7 @@ onBeforeMount(async () => {
:fields="['id', 'code', 'name']" :fields="['id', 'code', 'name']"
option-label="name" option-label="name"
option-value="id" option-value="id"
:is-required="true" :required="true"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -456,7 +432,7 @@ onBeforeMount(async () => {
:fields="['id', 'description']" :fields="['id', 'description']"
option-label="description" option-label="description"
option-value="id" option-value="id"
:is-required="true" :required="true"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -476,7 +452,7 @@ onBeforeMount(async () => {
:fields="['id', 'code', 'name']" :fields="['id', 'code', 'name']"
option-label="code" option-label="code"
option-value="id" option-value="id"
:is-required="true" :required="true"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -503,7 +479,5 @@ onBeforeMount(async () => {
</style> </style>
<i18n> <i18n>
es: es:
New item: Nuevo artículo Provisional name: Nombre provisional
Create Item: Crear artículo
You can search by id: Puedes buscar por id
</i18n> </i18n>

View File

@ -8,7 +8,6 @@ import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue'; import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import { useValidator } from 'src/composables/useValidator'; import { useValidator } from 'src/composables/useValidator';
import axios from 'axios'; import axios from 'axios';
@ -85,26 +84,6 @@ const removeTag = (index, params, search) => {
applyTags(params, search); applyTags(params, search);
}; };
const applyFieldFilters = (params) => {
fieldFiltersValues.value.forEach((fieldFilter) => {
if (
fieldFilter.selectedField &&
(fieldFilter.value !== null ||
fieldFilter.value !== '' ||
fieldFilter.value !== undefined)
) {
params[fieldFilter.name] = fieldFilter.value;
}
});
arrayData.applyFilter({ params });
};
const removeFieldFilter = (index, params, search) => {
delete params[fieldFiltersValues.value[index].name];
(fieldFiltersValues.value || []).splice(index, 1);
applyFieldFilters(params, search);
};
onMounted(async () => { onMounted(async () => {
stateStore.rightDrawer = true; stateStore.rightDrawer = true;
if (arrayData.store?.userParams?.categoryFk) if (arrayData.store?.userParams?.categoryFk)
@ -125,7 +104,6 @@ onMounted(async () => {
}); });
}); });
// Fill fieldFiltersValues with existent userParams
if (arrayData.store?.userParams) { if (arrayData.store?.userParams) {
fieldFiltersValues.value = Object.entries(arrayData.store?.userParams) fieldFiltersValues.value = Object.entries(arrayData.store?.userParams)
.filter(([key, value]) => value && _moreFields.includes(key)) .filter(([key, value]) => value && _moreFields.includes(key))
@ -249,6 +227,16 @@ onMounted(async () => {
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.isFloramondo')"
v-model="params.isFloramondo"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<!-- Tags filter --> <!-- Tags filter -->
<QItemLabel header> <QItemLabel header>
{{ t('params.tags') }} {{ t('params.tags') }}
@ -315,74 +303,6 @@ onMounted(async () => {
@click="removeTag(index, params, searchFn)" @click="removeTag(index, params, searchFn)"
/> />
</QItem> </QItem>
<!-- Filter fields -->
<QItemLabel header
>{{ t('More fields') }}
<QIcon
name="add_circle"
class="fill-icon-on-hover q-ml-md"
size="sm"
color="primary"
@click="fieldFiltersValues.push({})"
/></QItemLabel>
<QItem v-for="(fieldFilter, index) in fieldFiltersValues" :key="index">
<QItemSection class="col">
<VnSelect
class="full-width"
:label="t('params.tag')"
:model-value="fieldFilter.selectedField"
:options="moreFields"
option-label="label"
option-value="label"
dense
filled
:emit-value="false"
use-input
:is-clearable="false"
@update:model-value="
($event) => {
fieldFilter.name = $event.name;
fieldFilter.value = null;
fieldFilter.selectedField = $event;
}
"
/>
</QItemSection>
<QItemSection class="col">
<VnCheckbox
v-if="fieldFilter.selectedField?.type === 'boolean'"
v-model="fieldFilter.value"
:label="t('params.value')"
@update:model-value="applyFieldFilters(params, searchFn)"
/>
<VnInput
v-else
v-model="fieldFilter.value"
:label="t('params.value')"
:disable="!fieldFilter.selectedField"
filled
@keydown.enter="applyFieldFilters(params, searchFn)"
/>
</QItemSection>
<QItemSection side
><QIcon
name="delete"
class="fill-icon-on-hover q-ml-xs"
size="sm"
color="primary"
@click="removeFieldFilter(index, params, searchFn)"
/></QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.isFloramondo')"
v-model="params.isFloramondo"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
</template> </template>
</VnFilterPanel> </VnFilterPanel>
</template> </template>
@ -410,6 +330,17 @@ en:
Green: Green Green: Green
Handmade: Handmade Handmade: Handmade
Plant: Plant Plant: Plant
packing: Packing
grouping: Grouping
stems: Stems
size: Size
intrastatFk: Intrastat
ori:
id: Origin
workerFk: Buyer
weightByPiece: Weight/stem
stemMultiplier: Stem multiplier
landed: Landed date
es: es:
More fields: Más campos More fields: Más campos
params: params:
@ -433,4 +364,15 @@ es:
Green: Verde Green: Verde
Handmade: Hecho a mano Handmade: Hecho a mano
Plant: Planta Plant: Planta
packing: Packing
grouping: Grouping
stems: Tallos
size: Altura
intrastatFk: Intrastat
ori:
id: Origen
workerFk: Comprador
weightByPiece: Peso/tallo
stemMultiplier: Multiplicador de tallos
landed: Fecha de entrega
</i18n> </i18n>

View File

@ -1,30 +1,27 @@
<script setup> <script setup>
import { ref, computed, onMounted } from 'vue'; import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'filters/index'; import { toCurrency } from 'filters/index';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import { toDate, dashIfEmpty } from 'src/filters';
import axios from 'axios'; import axios from 'axios';
import ItemRequestDenyForm from './ItemRequestDenyForm.vue';
import { toDate } from 'src/filters';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import TicketDescriptorProxy from 'src/pages/Ticket/Card/TicketDescriptorProxy.vue';
import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import ItemRequestFilter from './ItemRequestFilter.vue'; import ItemRequestFilter from './ItemRequestFilter.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import FormModelPopup from 'src/components/FormModelPopup.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
const stateStore = useStateStore(); const stateStore = useStateStore();
const denyFormRef = ref(null); const denyFormRef = ref(null);
const denyRequestId = ref(null); const denyRequestId = ref(null);
const denyRequestIndex = ref(null);
const itemRequestsOptions = ref([]);
const userParams = { const userParams = {
state: 'pending',
daysOnward: 7, daysOnward: 7,
}; };
const tableRef = ref(); const tableRef = ref();
@ -34,9 +31,13 @@ onMounted(async () => {
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'id',
visible: false,
},
{
align: 'right',
label: t('globals.ticketId'), label: t('globals.ticketId'),
name: 'ticketFk', name: 'ticketFk',
align: 'left',
isId: true, isId: true,
chip: { chip: {
condition: () => true, condition: () => true,
@ -44,15 +45,16 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
}, },
{ {
align: 'center',
label: t('globals.shipped'), label: t('globals.shipped'),
name: 'shipped', name: 'shipped',
align: 'left',
component: 'date', component: 'date',
columnField: { columnField: {
component: null, component: null,
}, },
format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.shipped)), format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.shipped)),
columnClass: 'shrink', columnClass: 'shrink',
isEditable: false,
}, },
{ {
label: t('globals.description'), label: t('globals.description'),
@ -78,6 +80,7 @@ const columns = computed(() => [
component: null, component: null,
}, },
columnClass: 'shrink', columnClass: 'shrink',
isEditable: false,
}, },
{ {
align: 'left', align: 'left',
@ -91,6 +94,7 @@ const columns = computed(() => [
component: null, component: null,
}, },
format: (row, dashIfEmpty) => dashIfEmpty(row.departmentName), format: (row, dashIfEmpty) => dashIfEmpty(row.departmentName),
isEditable: false,
}, },
{ {
label: t('item.buyRequest.requested'), label: t('item.buyRequest.requested'),
@ -121,21 +125,52 @@ const columns = computed(() => [
component: null, component: null,
}, },
columnClass: 'shrink', columnClass: 'shrink',
isEditable: false,
}, },
{ {
label: t('globals.item'), label: t('globals.item'),
name: 'item', name: 'itemFk',
align: 'left', align: 'left',
component: 'input', component: 'input',
columnClass: 'expand', columnClass: 'expand',
isEditable: ({ isOk }) => isOk === null,
}, },
{ {
label: t('item.buyRequest.achieved'), label: t('item.buyRequest.achieved'),
name: 'achieved', name: 'saleQuantity',
align: 'left', align: 'left',
component: 'input', component: 'input',
columnClass: 'shrink', columnClass: 'shrink',
isEditable: ({ itemFk, isOk }) => {
if (itemFk && isOk === null) return true;
},
beforeDestroy: (row) => {
if (!row.saleQuantity) {
return tableRef.value.reload();
}
try {
axios
.post(`TicketRequests/${row.id}/confirm`, {
id: row.id,
itemFk: parseInt(row.itemFk),
quantity: parseInt(row.saleQuantity),
})
.then(() => {
axios
.get(`Items/findOne`, { where: { id: row.itemFk } })
.then((response) => {
row.itemDescription = response.data.name;
row.state = 1;
});
notify(t('globals.dataSaved'), 'positive');
return tableRef.value.reload();
});
} catch (error) {
notify(error.response.data.error.message, 'negative');
return tableRef.value.reload();
}
},
}, },
{ {
label: t('item.buyRequest.concept'), label: t('item.buyRequest.concept'),
@ -144,11 +179,12 @@ const columns = computed(() => [
sortable: true, sortable: true,
component: 'input', component: 'input',
columnClass: 'expand', columnClass: 'expand',
isEditable: false,
}, },
{ {
label: t('globals.state'), label: t('globals.state'),
name: 'state', name: 'state',
format: (row) => getState(row.isOk), format: ({ isOk }) => getState(isOk),
align: 'left', align: 'left',
}, },
{ {
@ -163,7 +199,23 @@ const columns = computed(() => [
{ {
align: 'right', align: 'right',
label: '', label: '',
name: 'denyOptions', name: 'tableActions',
actions: [
{
title: (row) => row.response,
icon: 'insert_drive_file',
isPrimary: true,
show: (row) => row?.response?.length,
},
{
title: t('Discard'),
icon: 'thumb_down',
fill: true,
isPrimary: true,
show: ({ isOk }) => isOk === null,
action: (row) => showDenyRequestForm(row.id),
},
],
}, },
]); ]);
@ -181,54 +233,17 @@ const getBadgeColor = (date) => {
if (difference > 0) return 'alert'; if (difference > 0) return 'alert';
}; };
const changeQuantity = async (request) => {
if (request.saleFk) {
const params = {
quantity: request.saleQuantity,
};
await axios.patch(`Sales/${request.saleFk}`, params);
}
await confirmRequest(request);
notify(t('globals.dataSaved'), 'positive');
};
const confirmRequest = async (request) => {
if (!request.itemFk || !request.saleQuantity) return;
const params = {
itemFk: request.itemFk,
quantity: request.saleQuantity,
attenderFk: request.attenderFk,
};
const { data } = await axios.post(`TicketRequests/${request.id}/confirm`, params);
request.itemDescription = data.concept;
request.isOk = true;
};
const getState = (isOk) => { const getState = (isOk) => {
if (isOk === null) return t('Pending'); if (isOk === null) return t('Pending');
else if (isOk) return t('Accepted'); else if (isOk) return t('Accepted');
else return t('Denied'); else return t('Denied');
}; };
const showDenyRequestForm = (requestId, rowIndex) => { const showDenyRequestForm = (requestId) => {
denyRequestId.value = requestId; denyRequestId.value = requestId;
denyRequestIndex.value = rowIndex;
denyFormRef.value.show(); denyFormRef.value.show();
}; };
const onDenyAccept = (_, responseData) => {
itemRequestsOptions.value[denyRequestIndex.value].isOk = responseData.isOk;
itemRequestsOptions.value[denyRequestIndex.value].attenderFk =
responseData.attenderFk;
itemRequestsOptions.value[denyRequestIndex.value].response = responseData.response;
denyRequestId.value = null;
denyRequestIndex.value = null;
tableRef.value.reload();
};
</script> </script>
<template> <template>
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
@ -240,12 +255,13 @@ const onDenyAccept = (_, responseData) => {
data-key="itemRequest" data-key="itemRequest"
url="ticketRequests/filter" url="ticketRequests/filter"
order="shipped ASC, isOk ASC" order="shipped ASC, isOk ASC"
:columns="columns"
:user-params="userParams" :user-params="userParams"
:right-search="false" :is-editable="true"
auto-load :columns="columns"
:disable-option="{ card: true }" :disable-option="{ card: true }"
chip-locale="item.params" :right-search="false"
:default-remove="false"
auto-load
> >
<template #column-ticketFk="{ row }"> <template #column-ticketFk="{ row }">
<span class="link"> <span class="link">
@ -254,16 +270,14 @@ const onDenyAccept = (_, responseData) => {
</span> </span>
</template> </template>
<template #column-shipped="{ row }"> <template #column-shipped="{ row }">
<QTd> <QBadge
<QBadge :color="getBadgeColor(row.shipped)"
:color="getBadgeColor(row.shipped)" text-color="black"
text-color="black" class="q-pa-xs"
class="q-pa-sm" style="font-size: 14px"
style="font-size: 14px" >
> {{ toDate(row.shipped) }}
{{ toDate(row.shipped) }} </QBadge>
</QBadge>
</QTd>
</template> </template>
<template #column-attenderName="{ row }"> <template #column-attenderName="{ row }">
<span class="link" @click.stop> <span class="link" @click.stop>
@ -284,74 +298,34 @@ const onDenyAccept = (_, responseData) => {
<DepartmentDescriptorProxy :id="row.departmentFk" /> <DepartmentDescriptorProxy :id="row.departmentFk" />
</span> </span>
</template> </template>
<template #column-item="{ row }">
<span>
<VnInput v-model.number="row.itemFk" dense />
</span>
</template>
<template #column-achieved="{ row }">
<span>
<VnInput
ref="achievedRef"
type="number"
v-model.number="row.saleQuantity"
:disable="!row.itemFk || row.isOk != null"
@blur="changeQuantity(row)"
@keyup.enter="$refs.achievedRef.vnInputRef.blur()"
dense
/>
</span>
</template>
<template #column-concept="{ row }"> <template #column-concept="{ row }">
<span @click.stop disabled="row.isOk != null"> <span :class="{ link: row.itemDescription }" @click.stop>
{{ row.itemDescription }} {{ dashIfEmpty(row.itemDescription) }}
<ItemDescriptorProxy v-if="row.itemFk" :id="row.itemFk" />
</span> </span>
</template> </template>
<template #moreFilterPanel="{ params }">
<VnInputNumber
:label="t('params.scopeDays')"
v-model.number="params.daysOnward"
@keyup.enter="(evt) => handleScopeDays(evt.target.value)"
@remove="handleScopeDays()"
class="q-px-xs q-pr-lg"
filled
dense
lazy-rules
is-outlined
/>
</template>
<template #column-denyOptions="{ row, rowIndex }">
<QIcon
v-if="row.response?.length"
name="insert_drive_file"
color="primary"
size="sm"
>
<QTooltip>
{{ row.response }}
</QTooltip>
</QIcon>
<QIcon
v-if="row.isOk == null"
name="thumb_down"
color="primary"
size="sm"
class="fill-icon"
@click="showDenyRequestForm(row.id, rowIndex)"
>
<QTooltip>
{{ t('Discard') }}
</QTooltip>
</QIcon>
</template>
</VnTable> </VnTable>
<QDialog ref="denyFormRef" transition-show="scale" transition-hide="scale"> <QDialog ref="denyFormRef" transition-show="scale" transition-hide="scale">
<ItemRequestDenyForm :request-id="denyRequestId" @on-data-saved="onDenyAccept" /> <FormModelPopup
:url-create="`TicketRequests/${denyRequestId}/deny`"
:title="t('Specify the reasons to deny this request')"
:form-initial-data="{ id: denyRequestId }"
@on-data-saved="tableRef.reload()"
>
<template #form-inputs="{ data }">
<VnInput
ref="textAreaRef"
type="textarea"
v-model="data.observation"
fill-input
:required="true"
auto-grow
data-cy="discardTextArea"
/>
</template>
</FormModelPopup>
</QDialog> </QDialog>
</template> </template>
<i18n> <i18n>
es: es:
Discard: Descartar Discard: Descartar

View File

@ -1,59 +0,0 @@
<script setup>
import { reactive, ref, onMounted, nextTick } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import VnRow from 'components/ui/VnRow.vue';
import FormModelPopup from 'src/components/FormModelPopup.vue';
defineProps({
requestId: {
type: Number,
default: null,
required: true,
},
});
const emit = defineEmits(['onDataSaved']);
const { t } = useI18n();
const textAreaRef = ref(null);
const bankEntityFormData = reactive({});
const onDataSaved = (formData, requestResponse) => {
emit('onDataSaved', formData, requestResponse);
};
onMounted(async () => {
await nextTick();
textAreaRef.value.focus();
});
</script>
<template>
<FormModelPopup
:url-create="`TicketRequests/${$props.requestId}/deny`"
:title="t('Specify the reasons to deny this request')"
:form-initial-data="bankEntityFormData"
@on-data-saved="onDataSaved"
>
<template #form-inputs="{ data }">
<VnRow>
<div class="col">
<VnInput
ref="textAreaRef"
type="textarea"
v-model="data.observation"
fill-input
:required="true"
autogrow
/>
</div>
</VnRow>
</template>
</FormModelPopup>
</template>
<i18n>
es:
Specify the reasons to deny this request: Especifica las razones para descartar la petición
</i18n>

View File

@ -189,7 +189,7 @@ onMounted(async () => {
<QCheckbox <QCheckbox
:label="t('params.mine')" :label="t('params.mine')"
v-model="params.mine" v-model="params.mine"
:toggle-indeterminate="false" :toggle-indeterminate="null"
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
@ -212,6 +212,13 @@ en:
state: State state: State
daysOnward: Days onward daysOnward: Days onward
myTeam: My team myTeam: My team
shipped: Shipped
description: Description
departmentFk: Department
quantity: Quantity
price: Price
item: Item
concept: Concept
dateFiltersTooltip: Cannot choose a range of dates and days onward at the same time dateFiltersTooltip: Cannot choose a range of dates and days onward at the same time
denied: Denied denied: Denied
accepted: Accepted accepted: Accepted
@ -230,6 +237,13 @@ es:
state: Estado state: Estado
daysOnward: Días en adelante daysOnward: Días en adelante
myTeam: Mi equipo myTeam: Mi equipo
shipped: Enviado
description: Descripción
departmentFk: Departamento
quantity: Cantidad
price: Precio
item: Artículo
concept: Concepto
dateFiltersTooltip: No se puede seleccionar un rango de fechas y días en adelante a la vez dateFiltersTooltip: No se puede seleccionar un rango de fechas y días en adelante a la vez
denied: Denegada denied: Denegada
accepted: Aceptada accepted: Aceptada

View File

@ -42,11 +42,11 @@ const itemPackingTypesOptions = ref([]);
/> />
<FormModel :url-update="`ItemTypes/${route.params.id}`" model="ItemType" auto-load> <FormModel :url-update="`ItemTypes/${route.params.id}`" model="ItemType" auto-load>
<template #form="{ data }"> <template #form="{ data }">
<VnRow> <VnRow class="q-py-sm">
<VnInput v-model="data.code" :label="t('itemType.shared.code')" /> <VnInput v-model="data.code" :label="t('itemType.shared.code')" />
<VnInput v-model="data.name" :label="t('itemType.shared.name')" /> <VnInput v-model="data.name" :label="t('itemType.shared.name')" />
</VnRow> </VnRow>
<VnRow> <VnRow class="q-py-sm">
<VnSelect <VnSelect
url="Workers/search" url="Workers/search"
v-model="data.workerFk" v-model="data.workerFk"
@ -58,11 +58,7 @@ const itemPackingTypesOptions = ref([]);
hide-selected hide-selected
> >
<template #prepend> <template #prepend>
<VnAvatar <VnAvatar :worker-id="data.workerFk" color="primary" />
:worker-id="data.workerFk"
color="primary"
:title="title"
/>
</template> </template>
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -85,7 +81,7 @@ const itemPackingTypesOptions = ref([]);
hide-selected hide-selected
/> />
</VnRow> </VnRow>
<VnRow> <VnRow class="q-py-sm">
<VnSelect <VnSelect
v-model="data.temperatureFk" v-model="data.temperatureFk"
:label="t('itemType.shared.temperature')" :label="t('itemType.shared.temperature')"
@ -96,7 +92,7 @@ const itemPackingTypesOptions = ref([]);
/> />
<VnInput v-model="data.life" :label="t('itemType.summary.life')" /> <VnInput v-model="data.life" :label="t('itemType.summary.life')" />
</VnRow> </VnRow>
<VnRow> <VnRow class="q-py-sm">
<VnSelect <VnSelect
v-model="data.itemPackingTypeFk" v-model="data.itemPackingTypeFk"
:label="t('itemType.shared.itemPackingType')" :label="t('itemType.shared.itemPackingType')"
@ -107,7 +103,7 @@ const itemPackingTypesOptions = ref([]);
/> />
<VnInput v-model="data.maxRefs" :label="t('itemType.shared.maxRefs')" /> <VnInput v-model="data.maxRefs" :label="t('itemType.shared.maxRefs')" />
</VnRow> </VnRow>
<VnRow> <VnRow class="q-py-sm">
<QCheckbox <QCheckbox
v-model="data.isFragile" v-model="data.isFragile"
:label="t('itemType.shared.fragile')" :label="t('itemType.shared.fragile')"

View File

@ -8,7 +8,8 @@ import filter from './ItemTypeFilter.js';
<VnCard <VnCard
data-key="ItemType" data-key="ItemType"
url="ItemTypes" url="ItemTypes"
:filter="filter" :filter
:id-in-where="true"
:descriptor="ItemTypeDescriptor" :descriptor="ItemTypeDescriptor"
/> />
</template> </template>

View File

@ -30,7 +30,6 @@ const entityId = computed(() => {
:filter="filter" :filter="filter"
title="code" title="code"
data-key="ItemType" data-key="ItemType"
:to-module="{ name: 'ItemTypeList' }"
> >
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="$t('itemType.shared.code')" :value="entity.code" /> <VnLv :label="$t('itemType.shared.code')" :value="entity.code" />

View File

@ -17,3 +17,10 @@ itemType:
isUnconventionalSize: Is unconventional size isUnconventionalSize: Is unconventional size
search: Search item type search: Search item type
searchInfo: Search item type by id, name or code searchInfo: Search item type by id, name or code
params:
id: Id
code: Code
name: Name
categoryFk: Category
workerFk: Comprador
temperatureFk: Temperature

View File

@ -17,3 +17,10 @@ itemType:
isUnconventionalSize: Es de tamaño poco convencional isUnconventionalSize: Es de tamaño poco convencional
search: Buscar familia search: Buscar familia
searchInfo: Buscar familia por id, nombre o código searchInfo: Buscar familia por id, nombre o código
params:
id: Id
code: Código
name: Nombre
categoryFk: Reino
workerFk: Comprador
temperatureFk: Temperatura

View File

@ -25,6 +25,10 @@ const exprBuilder = (param, value) => {
return { return {
code: { like: `%${value}%` }, code: { like: `%${value}%` },
}; };
case 'temperatureFk':
return {
temperatureFk: value,
};
case 'search': case 'search':
if (value) { if (value) {
if (!isNaN(value)) { if (!isNaN(value)) {
@ -51,16 +55,19 @@ const exprBuilder = (param, value) => {
const columns = computed(() => [ const columns = computed(() => [
{ {
align: 'left', align: 'right',
name: 'id', name: 'id',
label: t('id'), label: 'Id',
isId: true, isId: true,
columnFilter: {
inWhere: true,
},
cardVisible: true, cardVisible: true,
}, },
{ {
align: 'left', align: 'left',
name: 'code', name: 'code',
label: t('code'), label: t('itemType.shared.code'),
isTitle: true, isTitle: true,
cardVisible: true, cardVisible: true,
}, },
@ -71,8 +78,7 @@ const columns = computed(() => [
cardVisible: true, cardVisible: true,
}, },
{ {
align: 'left', label: t('itemType.shared.worker'),
label: t('worker'),
name: 'workerFk', name: 'workerFk',
component: 'select', component: 'select',
attrs: { attrs: {
@ -80,7 +86,6 @@ const columns = computed(() => [
optionLabel: 'nickname', optionLabel: 'nickname',
optionValue: 'id', optionValue: 'id',
}, },
format: (row) => row.worker?.user?.name,
cardVisible: true, cardVisible: true,
columnField: { component: null }, columnField: { component: null },
columnFilter: { columnFilter: {
@ -95,6 +100,7 @@ const columns = computed(() => [
}, },
inWhere: true, inWhere: true,
}, },
format: (row) => row.worker?.user?.name,
}, },
{ {
align: 'left', align: 'left',
@ -104,19 +110,24 @@ const columns = computed(() => [
attrs: { attrs: {
options: itemCategoriesOptions.value, options: itemCategoriesOptions.value,
}, },
columnFilter: {
inWhere: true,
},
cardVisible: false, cardVisible: false,
visible: false, format: (row, dashIfEmpty) => dashIfEmpty(row.category?.name),
}, },
{ {
align: 'left', align: 'left',
name: 'Temperature', name: 'temperatureFk',
label: t('Temperature'), label: t('Temperature'),
component: 'select', component: 'select',
attrs: { attrs: {
options: temperatureOptions.value, options: temperatureOptions.value,
}, },
columnFilter: {
inWhere: true,
},
cardVisible: false, cardVisible: false,
visible: false,
}, },
]); ]);
</script> </script>
@ -141,20 +152,28 @@ const columns = computed(() => [
:array-data-props="{ :array-data-props="{
url: 'ItemTypes', url: 'ItemTypes',
order: 'name ASC', order: 'name ASC',
exprBuilder, exprBuilder: exprBuilder,
userFilter: { userFilter: {
include: { include: [
relation: 'worker', {
scope: { relation: 'worker',
fields: ['id'], scope: {
include: { fields: ['id'],
relation: 'user', include: {
scope: { relation: 'user',
fields: ['id', 'name'], scope: {
fields: ['id', 'name'],
},
}, },
}, },
}, },
}, {
relation: 'category',
scope: {
fields: ['id', 'name'],
},
},
],
}, },
}" }"
> >
@ -169,7 +188,7 @@ const columns = computed(() => [
formInitialData: {}, formInitialData: {},
}" }"
:columns="columns" :columns="columns"
auto-load :right-search="false"
redirect="item/item-type" redirect="item/item-type"
> >
<template #column-workerFk="{ row }"> <template #column-workerFk="{ row }">
@ -208,15 +227,17 @@ const columns = computed(() => [
<i18n> <i18n>
es: es:
id: Id params:
code: Código id: Id
worker: Trabajador code: Código
ItemCategory: Reino worker: Trabajador
Temperature: Temperatura ItemCategory: Reino
Create ItemTypes: Crear familia Temperature: Temperatura
Create ItemTypes: Crear familia
en: en:
code: Code params:
worker: Worker code: Code
ItemCategory: ItemCategory worker: Worker
Temperature: Temperature ItemCategory: ItemCategory
Temperature: Temperature
</i18n> </i18n>

View File

@ -8,10 +8,11 @@ import VnTable from 'src/components/VnTable/VnTable.vue';
import axios from 'axios'; import axios from 'axios';
import { displayResults } from 'src/pages/Ticket/Negative/composables/notifyResults'; import { displayResults } from 'src/pages/Ticket/Negative/composables/notifyResults';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import { useState } from 'src/composables/useState'; import useNotify from 'src/composables/useNotify.js';
const MATCH = 'match'; const MATCH = 'match';
const { notifyResults } = displayResults(); const { notifyResults } = displayResults();
const { notify } = useNotify();
const { t } = useI18n(); const { t } = useI18n();
const $props = defineProps({ const $props = defineProps({
@ -42,7 +43,7 @@ const ticketConfig = ref({});
const proposalTableRef = ref(null); const proposalTableRef = ref(null);
const sale = computed(() => $props.sales[0]); const sale = computed(() => $props.sales[0]);
const saleFk = computed(() => sale.value.saleFk); const saleFk = computed(() => sale.value?.saleFk);
const filter = computed(() => ({ const filter = computed(() => ({
where: $props.filter, where: $props.filter,
@ -56,8 +57,24 @@ const defaultColumnAttrs = {
}; };
const emit = defineEmits(['onDialogClosed', 'itemReplaced']); const emit = defineEmits(['onDialogClosed', 'itemReplaced']);
const conditionalValuePrice = (price) => const priceStatusClass = (proposalPrice) => {
price > 1 + ticketConfig.value.lackAlertPrice / 100 ? 'match' : 'not-match'; 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(() => [ const columns = computed(() => [
{ {
@ -97,7 +114,15 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
sortable: true, 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', name: 'tag5',
field: 'value5', field: 'value5',
columnClass: 'expand', columnClass: 'expand',
@ -105,7 +130,7 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
sortable: true, sortable: true,
label: t('item.list.stems'), label: t('proposal.tag6'),
name: 'tag6', name: 'tag6',
field: 'value6', field: 'value6',
columnClass: 'expand', columnClass: 'expand',
@ -113,12 +138,27 @@ const columns = computed(() => [
{ {
align: 'left', align: 'left',
sortable: true, sortable: true,
label: t('item.list.producer'), label: t('proposal.tag7'),
name: 'tag7', name: 'tag7',
field: 'value7', field: 'value7',
columnClass: 'expand', 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, ...defaultColumnAttrs,
label: t('proposal.price2'), label: t('proposal.price2'),
@ -169,14 +209,14 @@ function extractMatchValues(obj) {
.filter((key) => key.startsWith(MATCH)) .filter((key) => key.startsWith(MATCH))
.map((key) => parseInt(key.replace(MATCH, ''), 10)); .map((key) => parseInt(key.replace(MATCH, ''), 10));
} }
const gradientStyle = (value) => { const gradientStyleClass = (row) => {
let color = 'white'; let color = 'white';
const perc = parseFloat(value); const value = parseFloat(row);
switch (true) { switch (true) {
case perc >= 0 && perc < 33: case value >= 0 && value < 33:
color = 'primary'; color = 'primary';
break; break;
case perc >= 33 && perc < 66: case value >= 33 && value < 66:
color = 'warning'; color = 'warning';
break; break;
@ -193,52 +233,63 @@ const statusConditionalValue = (row) => {
}; };
const isSelectionAvailable = (itemProposal) => { const isSelectionAvailable = (itemProposal) => {
const { price2 } = itemProposal; const { price2, available } = itemProposal;
const salePrice = sale.value.price; const salePrice = sale.value.price;
const byPrice = (100 * price2) / salePrice > ticketConfig.value.lackAlertPrice; const { lackAlertPrice } = ticketConfig.value;
if (byPrice) { const isPriceTooHigh = (100 * price2) / salePrice > lackAlertPrice;
return byPrice; if (isPriceTooHigh) {
return isPriceTooHigh;
} }
const byQuantity = const hasEnoughQuantity =
(100 * itemProposal.available) / Math.abs($props.itemLack.lack) < (100 * available) / Math.abs($props.itemLack.lack) < lackAlertPrice;
ticketConfig.value.lackAlertPrice; return hasEnoughQuantity;
return byQuantity;
}; };
async function change({ itemFk: substitutionFk }) { async function change(itemSubstitution) {
try { if (!isSelectionAvailable(itemSubstitution)) {
const promises = $props.sales.map(({ saleFk, quantity }) => { notify(t('notAvailable'), 'warning');
const params = { return;
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);
} }
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) { async function handleTicketConfig(data) {
ticketConfig.value = data[0]; ticketConfig.value = data[0];
} }
function filterRows(data) {
const filteredRows = data.sort(
(a, b) => isSelectionAvailable(b) - isSelectionAvailable(a),
);
proposalTableRef.value.CrudModelRef.formData = filteredRows;
}
</script> </script>
<template> <template>
<FetchData <FetchData
url="TicketConfigs" url="TicketConfigs"
:filter="{ fields: ['lackAlertPrice'] }" :filter="{ fields: ['lackAlertPrice'] }"
@on-fetch="handleTicketConfig" @on-fetch="handleTicketConfig"
></FetchData> auto-load
/>
<QInnerLoading <QInnerLoading
:showing="isLoading" :showing="isLoading"
:label="t && t('globals.pleaseWait')" :label="t && t('globals.pleaseWait')"
@ -255,13 +306,22 @@ async function handleTicketConfig(data) {
:user-filter="filter" :user-filter="filter"
:columns="columns" :columns="columns"
class="full-width q-mt-md" class="full-width q-mt-md"
@on-fetch="filterRows"
row-key="id" row-key="id"
:row-click="change" :row-click="change"
:is-editable="false" :is-editable="false"
:right-search="false" :right-search="false"
:without-header="true"
:disable-option="{ card: true, table: 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 }"> <template #column-longName="{ row }">
<QTd <QTd
class="flex" class="flex"
@ -269,15 +329,17 @@ async function handleTicketConfig(data) {
> >
<div <div
class="middle full-width" class="middle full-width"
:class="[`proposal-${gradientStyle(statusConditionalValue(row))}`]" :class="[
`proposal-${gradientStyleClass(statusConditionalValue(row))}`,
]"
> >
<QTooltip> {{ statusConditionalValue(row) }}% </QTooltip> <QTooltip> {{ statusConditionalValue(row) }}% </QTooltip>
</div> </div>
<div style="flex: 2 0 100%; align-content: center"> <div style="flex: 2 0 100%; align-content: center">
<div> <span class="link" @click.stop>
<span class="link">{{ row.longName }}</span> {{ row.longName }}
<ItemDescriptorProxy :id="row.id" /> <ItemDescriptorProxy :id="row.id" />
</div> </span>
</div> </div>
</QTd> </QTd>
</template> </template>
@ -290,6 +352,9 @@ async function handleTicketConfig(data) {
<template #column-tag7="{ row }"> <template #column-tag7="{ row }">
<span :class="{ match: !row.match7 }">{{ row.value7 }}</span> <span :class="{ match: !row.match7 }">{{ row.value7 }}</span>
</template> </template>
<template #column-tag8="{ row }">
<span :class="{ match: !row.match8 }">{{ row.value8 }}</span>
</template>
<template #column-counter="{ row }"> <template #column-counter="{ row }">
<span <span
:class="{ :class="{
@ -304,8 +369,17 @@ async function handleTicketConfig(data) {
</template> </template>
<template #column-price2="{ row }"> <template #column-price2="{ row }">
<div class="flex column items-center content-center"> <div class="flex column items-center content-center">
<VnStockValueDisplay :value="(sales[0].price - row.price2) / 100" /> <!-- Use class binding for tooltip background -->
<span :class="[conditionalValuePrice(row.price2)]">{{ <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) toCurrency(row.price2)
}}</span> }}</span>
</div> </div>
@ -319,12 +393,26 @@ async function handleTicketConfig(data) {
margin-right: 2px; margin-right: 2px;
flex: 2 0 5px; 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 { .match {
color: $negative; color: $negative;
} }
.not-match {
color: inherit;
}
.proposal-warning { .proposal-warning {
background-color: $warning; background-color: $warning;
} }
@ -344,3 +432,9 @@ async function handleTicketConfig(data) {
font-size: smaller; font-size: smaller;
} }
</style> </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: () => [], default: () => [],
}, },
}); });
const { dialogRef } = useDialogPluginComponent();
const { dialogRef, onDialogHide, onDialogOK, onDialogCancel } =
useDialogPluginComponent();
const emit = defineEmits([ const emit = defineEmits([
'onDialogClosed', 'onDialogClosed',
'onDialogOk',
'itemReplaced', 'itemReplaced',
...useDialogPluginComponent.emits, ...useDialogPluginComponent.emits,
]); ]);
defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() }); defineExpose({ show: () => dialogRef.value.show(), hide: () => dialogRef.value.hide() });
const itemReplaced = (data) => {
onDialogOK(data);
dialogRef.value.hide();
};
</script> </script>
<template> <template>
<QDialog ref="dialogRef" transition-show="scale" transition-hide="scale"> <QDialog ref="dialogRef" transition-show="scale" transition-hide="scale">
<QCard class="dialog-width"> <QCard class="dialog-width">
<QCardSection class="row items-center q-pb-none"> <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 /> <QSpace />
<QBtn icon="close" flat round dense v-close-popup /> <QBtn icon="close" flat round dense v-close-popup />
</QCardSection> </QCardSection>
<QCardSection> <QCardSection>
<ItemProposal <ItemProposal v-bind="$props" @item-replaced="itemReplaced"
v-bind="$props" /></QCardSection>
@item-replaced="
(data) => {
emit('itemReplaced', data);
dialogRef.hide();
}
"
></ItemProposal
></QCardSection>
</QCard> </QCard>
</QDialog> </QDialog>
</template> </template>

View File

@ -99,9 +99,6 @@ item:
concept: Concept concept: Concept
denyOptions: Deny denyOptions: Deny
scopeDays: Scope days scopeDays: Scope days
searchbar:
label: Search item
info: You can search by id
descriptor: descriptor:
item: Item item: Item
buyer: Buyer buyer: Buyer
@ -158,6 +155,7 @@ item:
isPhotoRequestedTooltip: This item does need a photo isPhotoRequestedTooltip: This item does need a photo
isCustomInspectionRequired: Needs physical inspection (PIF) isCustomInspectionRequired: Needs physical inspection (PIF)
description: Description description: Description
photoMotivation: Comment for the photographer
fixedPrice: fixedPrice:
itemFk: Item ID itemFk: Item ID
groupingPrice: Grouping price groupingPrice: Grouping price
@ -218,7 +216,7 @@ item:
genus: Genus genus: Genus
specie: Specie specie: Specie
search: 'Search item' search: 'Search item'
searchInfo: 'You can search by id' searchInfo: 'You can search by id or barcode'
regularizeStock: Regularize stock regularizeStock: Regularize stock
itemProposal: Items proposal itemProposal: Items proposal
proposal: proposal:
@ -231,6 +229,11 @@ proposal:
value6: value6 value6: value6
value7: value7 value7: value7
value8: value8 value8: value8
tag5: Tag5
tag6: Tag6
tag7: Tag7
tag8: Tag8
advanceable: Advanceable
available: Available available: Available
minQuantity: minQuantity minQuantity: minQuantity
price2: Price price2: Price

View File

@ -73,13 +73,6 @@ itemTags:
addTag: Añadir etiqueta addTag: Añadir etiqueta
tag: Etiqueta tag: Etiqueta
value: Valor value: Valor
itemType:
shared:
code: Código
name: Nombre
worker: Trabajador
category: Reino
temperature: Temperatura
searchbar: searchbar:
label: Buscar artículo label: Buscar artículo
info: Buscar por id de artículo info: Buscar por id de artículo
@ -155,15 +148,16 @@ item:
weightByPiece: Peso (gramos)/tallo weightByPiece: Peso (gramos)/tallo
boxUnits: Unidades/caja boxUnits: Unidades/caja
recycledPlastic: Plastico reciclado recycledPlastic: Plastico reciclado
nonRecycledPlastic: Plático no reciclado nonRecycledPlastic: Plástico no reciclado
isActive: Activo isActive: Activo
hasKgPrice: Precio en kg hasKgPrice: Precio en kg
isFragile: Frágil isFragile: Frágil
isFragileTooltip: Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...) isFragileTooltip: Se muestra en la web, app que este artículo no puede viajar (coronas, palmas, ...)
isPhotoRequested: Hacer foto isPhotoRequested: Hacer foto
isPhotoRequestedTooltip: Este artículo necesita una foto isPhotoRequestedTooltip: Este artículo necesita una foto
isCustomInspectionRequired: Necesita inspección física (PIF) isCustomInspectionRequired: Necesita insp. física (PIF)
description: Descripción description: Descripción
photoMotivation: Comentario para el fotógrafo
fixedPrice: fixedPrice:
itemFk: ID Artículo itemFk: ID Artículo
groupingPrice: Precio grouping groupingPrice: Precio grouping
@ -212,6 +206,8 @@ item:
minSalesQuantity: Cantidad mínima de venta minSalesQuantity: Cantidad mínima de venta
genus: Genus genus: Genus
specie: Specie specie: Specie
search: 'Buscar artículo'
searchInfo: 'Puedes buscar por id de artículo o código de barras'
regularizeStock: Regularizar stock regularizeStock: Regularizar stock
buyRequest: buyRequest:
ticketId: 'ID Ticket' ticketId: 'ID Ticket'
@ -237,11 +233,16 @@ proposal:
value6: value6 value6: value6
value7: value7 value7: value7
value8: value8 value8: value8
tag5: Tag5
tag6: Tag6
tag7: Tag7
tag8: Tag8
available: Disponible available: Disponible
minQuantity: Min. cantidad minQuantity: Min. cantidad
price2: Precio price2: Precio
located: Ubicado located: Ubicado
counter: Contador counter: Contador
advanceable: Adelantable
difference: Diferencial difference: Diferencial
groupingPrice: Precio Grouping groupingPrice: Precio Grouping
itemOldPrice: Precio itemOld itemOldPrice: Precio itemOld

View File

@ -4,7 +4,7 @@ import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore'; import { useStateStore } from 'stores/useStateStore';
import { QIcon } from 'quasar'; import { QIcon } from 'quasar';
import { dashIfEmpty, toCurrency, toDate, toHour } from 'src/filters'; import { dashIfEmpty, toCurrency, toDate, toDateHourMinSec, toHour } from 'src/filters';
import { openBuscaman } from 'src/utils/buscaman'; import { openBuscaman } from 'src/utils/buscaman';
import CardSummary from 'components/ui/CardSummary.vue'; import CardSummary from 'components/ui/CardSummary.vue';
import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue'; import WorkerDescriptorProxy from 'pages/Worker/Card/WorkerDescriptorProxy.vue';
@ -83,14 +83,14 @@ const ticketColumns = ref([
{ {
name: 'delivered', name: 'delivered',
label: t('route.delivered'), label: t('route.delivered'),
field: (row) => dashIfEmpty(toDate(row?.delivered)), field: (row) => dashIfEmpty(toDateHourMinSec(row?.delivered)),
sortable: false, sortable: false,
align: 'center', align: 'center',
}, },
{ {
name: 'forecast', name: 'estimated',
label: t('route.forecast'), label: t('route.estimated'),
field: (row) => dashIfEmpty(toDate(row?.forecast)), field: (row) => dashIfEmpty(toDateHourMinSec(row?.estimated)),
sortable: false, sortable: false,
align: 'center', align: 'center',
}, },
@ -103,7 +103,7 @@ const ticketColumns = ref([
}, },
{ {
name: 'volume', name: 'volume',
label: t('route.summary.m3'), label: 'm³',
field: (row) => row?.volume, field: (row) => row?.volume,
sortable: false, sortable: false,
align: 'center', align: 'center',

View File

@ -2,7 +2,7 @@
import VnPaginate from 'components/ui/VnPaginate.vue'; import VnPaginate from 'components/ui/VnPaginate.vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty, toDateHourMinSec } from 'src/filters';
import VnInputDate from 'components/common/VnInputDate.vue'; import VnInputDate from 'components/common/VnInputDate.vue';
import VnInput from 'components/common/VnInput.vue'; import VnInput from 'components/common/VnInput.vue';
import axios from 'axios'; import axios from 'axios';
@ -66,15 +66,15 @@ const columns = computed(() => [
}, },
{ {
name: 'delivered', name: 'delivered',
label: t('route.ticket.delivered'), label: t('route.delivered'),
field: (row) => dashIfEmpty(row?.delivered), field: (row) => dashIfEmpty(toDateHourMinSec(row?.delivered)),
sortable: false, sortable: false,
align: 'left', align: 'left',
}, },
{ {
name: 'estimated', name: 'estimated',
label: t('route.ticket.estimated'), label: t('route.estimated'),
field: (row) => dashIfEmpty(row?.estimated), field: (row) => dashIfEmpty(toDateHourMinSec(row?.estimated)),
sortable: false, sortable: false,
align: 'left', align: 'left',
}, },
@ -254,7 +254,9 @@ const openSmsDialog = async () => {
<QDialog v-model="confirmationDialog"> <QDialog v-model="confirmationDialog">
<QCard style="min-width: 350px"> <QCard style="min-width: 350px">
<QCardSection> <QCardSection>
<p class="text-h6 q-ma-none">{{ t('route.ticket.selectStartingDate') }}</p> <p class="text-h6 q-ma-none">
{{ t('route.ticket.selectStartingDate') }}
</p>
</QCardSection> </QCardSection>
<QCardSection class="q-pt-none"> <QCardSection class="q-pt-none">
@ -265,7 +267,12 @@ const openSmsDialog = async () => {
/> />
</QCardSection> </QCardSection>
<QCardActions align="right"> <QCardActions align="right">
<QBtn flat :label="t('globals.cancel')" v-close-popup class="text-primary" /> <QBtn
flat
:label="t('globals.cancel')"
v-close-popup
class="text-primary"
/>
<QBtn color="primary" v-close-popup @click="cloneRoutes"> <QBtn color="primary" v-close-popup @click="cloneRoutes">
{{ t('globals.clone') }} {{ t('globals.clone') }}
</QBtn> </QBtn>
@ -302,11 +309,7 @@ const openSmsDialog = async () => {
class="q-mr-sm" class="q-mr-sm"
@click="setOrderedPriority" @click="setOrderedPriority"
> >
<QTooltip <QTooltip>{{ t('route.ticket.renumberAllTickets') }} </QTooltip>
>{{
t('route.ticket.renumberAllTickets')
}}
</QTooltip>
</QBtn> </QBtn>
<QBtn <QBtn
icon="sms" icon="sms"
@ -353,7 +356,11 @@ const openSmsDialog = async () => {
@click="setHighestPriority(row, rows)" @click="setHighestPriority(row, rows)"
> >
<QTooltip> <QTooltip>
{{ t('route.ticket.assignHighestPriority') }} {{
t(
'route.ticket.assignHighestPriority',
)
}}
</QTooltip> </QTooltip>
</QIcon> </QIcon>
<VnInput <VnInput
@ -368,7 +375,9 @@ const openSmsDialog = async () => {
<QTd> <QTd>
<span class="link" @click="goToBuscaman(row)"> <span class="link" @click="goToBuscaman(row)">
{{ value }} {{ value }}
<QTooltip>{{ t('route.ticket.openBuscaman') }}</QTooltip> <QTooltip>{{
t('route.ticket.openBuscaman')
}}</QTooltip>
</span> </span>
</QTd> </QTd>
</template> </template>

View File

@ -2,32 +2,32 @@ route:
filter: filter:
Served: Served Served: Served
summary: summary:
date: Date date: Date
agency: Agency agency: Agency
vehicle: Vehicle vehicle: Vehicle
driver: Driver driver: Driver
cost: Cost cost: Cost
started: Started time started: Started time
finished: Finished time finished: Finished time
kmStart: Km start kmStart: Km start
kmEnd: Km end kmEnd: Km end
volume: Volume volume: Volume
packages: Packages packages: Packages
description: Description description: Description
tickets: Tickets tickets: Tickets
order: Order order: Order
street: Street street: Street
city: City city: City
pc: PC pc: PC
client: Client client: Client
state: State state: State
m3: m3:
packaging: Packaging packaging: Packaging
ticket: Ticket ticket: Ticket
closed: Closed closed: Closed
open: Open open: Open
yes: Yes yes: Yes
no: No no: No
extendedList: extendedList:
selectStartingDate: Select the starting date selectStartingDate: Select the starting date
startingDate: Starting date startingDate: Starting date
@ -105,7 +105,7 @@ route:
dated: Dated dated: Dated
preview: Preview preview: Preview
delivered: Delivered delivered: Delivered
forecast: Forecast estimated: Estimated
cmr: cmr:
search: Search Cmr search: Search Cmr
searchInfo: You can search Cmr by Id searchInfo: You can search Cmr by Id
@ -131,8 +131,6 @@ route:
PC: PC PC: PC
client: Client client: Client
warehouse: Warehouse warehouse: Warehouse
delivered: Delivered
estimated: Estimated
packages: Packages packages: Packages
packaging: Packaging packaging: Packaging
ticket: Ticket ticket: Ticket

View File

@ -2,30 +2,30 @@ route:
filter: filter:
Served: Servida Served: Servida
summary: summary:
date: Fecha date: Fecha
agency: Agencia agency: Agencia
vehicle: Vehículo vehicle: Vehículo
driver: Conductor driver: Conductor
cost: Costo cost: Costo
started: Hora inicio started: Hora inicio
finished: Hora fin finished: Hora fin
kmStart: Km inicio kmStart: Km inicio
kmEnd: Km fin kmEnd: Km fin
volume: Volumen volume: Volumen
packages: Bultos packages: Bultos
description: Descripción description: Descripción
tickets: Tickets tickets: Tickets
order: Orden order: Orden
street: Dirección fiscal street: Dirección fiscal
city: Población city: Población
pc: CP pc: CP
client: Cliente client: Cliente
state: Estado state: Estado
packaging: Encajado packaging: Encajado
closed: Cerrada closed: Cerrada
open: Abierta open: Abierta
yes: yes:
no: No no: No
extendedList: extendedList:
selectStartingDate: Seleccione la fecha de inicio selectStartingDate: Seleccione la fecha de inicio
statingDate: Fecha de inicio statingDate: Fecha de inicio
@ -104,7 +104,7 @@ route:
dated: Fecha dated: Fecha
preview: Vista previa preview: Vista previa
delivered: Entregado delivered: Entregado
forecast: Pronóstico estimated: Pronóstico
cmr: cmr:
list: list:
results: resultados results: resultados
@ -127,8 +127,6 @@ route:
PC: CP PC: CP
client: Cliente client: Cliente
warehouse: Almacén warehouse: Almacén
delivered: Entregado
estimated: Pronóstico
packages: Bultos packages: Bultos
packaging: Encajado packaging: Encajado
ticket: Ticket ticket: Ticket

View File

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

View File

@ -17,7 +17,6 @@ import ItemProposalProxy from 'src/pages/Item/components/ItemProposalProxy.vue';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
const quasar = useQuasar(); const quasar = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
const editableStates = ref([]);
const stateStore = useStateStore(); const stateStore = useStateStore();
const tableRef = ref(); const tableRef = ref();
const changeItemDialogRef = ref(null); const changeItemDialogRef = ref(null);
@ -70,14 +69,11 @@ const showItemProposal = () => {
}) })
.onOk(itemProposalEvt); .onOk(itemProposalEvt);
}; };
const isButtonDisabled = computed(() => selectedRows.value.length !== 1);
</script> </script>
<template> <template>
<FetchData
url="States/editableStates"
@on-fetch="(data) => (editableStates = data)"
auto-load
/>
<FetchData <FetchData
:url="`Items/${entityId}/getCard`" :url="`Items/${entityId}/getCard`"
:fields="['longName']" :fields="['longName']"
@ -99,11 +95,7 @@ const showItemProposal = () => {
> >
<template #top-right> <template #top-right>
<QBtnGroup push class="q-mr-lg" style="column-gap: 1px"> <QBtnGroup push class="q-mr-lg" style="column-gap: 1px">
<QBtn <QBtn data-cy="transferLines" color="primary" :disable="isButtonDisabled">
data-cy="transferLines"
color="primary"
:disable="!(selectedRows.length === 1)"
>
<template #default> <template #default>
<QIcon name="vn:splitline" /> <QIcon name="vn:splitline" />
<QIcon name="vn:ticket" /> <QIcon name="vn:ticket" />
@ -124,7 +116,7 @@ const showItemProposal = () => {
<QBtn <QBtn
color="primary" color="primary"
@click="showItemProposal" @click="showItemProposal"
:disable="!(selectedRows.length === 1)" :disable="isButtonDisabled"
data-cy="itemProposal" data-cy="itemProposal"
> >
<QIcon name="import_export" class="rotate-90" /> <QIcon name="import_export" class="rotate-90" />
@ -135,7 +127,7 @@ const showItemProposal = () => {
<VnPopupProxy <VnPopupProxy
data-cy="changeItem" data-cy="changeItem"
icon="sync" icon="sync"
:disable="!(selectedRows.length === 1)" :disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeItem.title')" :tooltip="t('negative.detail.modal.changeItem.title')"
> >
<template #extraIcon> <QIcon name="vn:item" /> </template> <template #extraIcon> <QIcon name="vn:item" /> </template>
@ -149,7 +141,7 @@ const showItemProposal = () => {
<VnPopupProxy <VnPopupProxy
data-cy="changeState" data-cy="changeState"
icon="sync" icon="sync"
:disable="!(selectedRows.length === 1)" :disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeState.title')" :tooltip="t('negative.detail.modal.changeState.title')"
> >
<template #extraIcon> <QIcon name="vn:eye" /> </template> <template #extraIcon> <QIcon name="vn:eye" /> </template>
@ -163,7 +155,7 @@ const showItemProposal = () => {
<VnPopupProxy <VnPopupProxy
data-cy="changeQuantity" data-cy="changeQuantity"
icon="sync" icon="sync"
:disable="!(selectedRows.length === 1)" :disable="isButtonDisabled"
:tooltip="t('negative.detail.modal.changeQuantity.title')" :tooltip="t('negative.detail.modal.changeQuantity.title')"
@click="showChangeQuantityDialog = true" @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 VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
import VnInputDateTime from 'src/components/common/VnInputDateTime.vue'; import VnInputDateTime from 'src/components/common/VnInputDateTime.vue';
import VnInputDates from 'src/components/common/VnInputDates.vue';
const { t } = useI18n(); const { t } = useI18n();
const props = defineProps({ const props = defineProps({
dataKey: { dataKey: {
@ -73,8 +75,8 @@ const setUserParams = (params) => {
<VnFilterPanel <VnFilterPanel
:data-key="props.dataKey" :data-key="props.dataKey"
:search-button="true" :search-button="true"
:hidden-tags="['excludedDates']"
@set-user-params="setUserParams" @set-user-params="setUserParams"
:unremovable-params="['warehouseFk']"
> >
<template #tags="{ tag, formatFn }"> <template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs"> <div class="q-gutter-x-xs">
@ -92,7 +94,7 @@ const setUserParams = (params) => {
dense dense
filled filled
@update:model-value=" @update:model-value="
(value) => { () => {
setUserParams(params); setUserParams(params);
} }
" "
@ -127,8 +129,19 @@ const setUserParams = (params) => {
dense dense
filled filled
/> />
</QItemSection> </QItem </QItemSection>
><QItem> </QItem>
<QItem>
<QItemSection>
<VnInputDates
v-model="params.excludedDates"
filled
:label="t('negative.excludedDates')"
>
</VnInputDates>
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="categoriesOptions"> <QItemSection v-if="categoriesOptions">
<VnSelect <VnSelect
:label="t('negative.categoryFk')" :label="t('negative.categoryFk')"

View File

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

View File

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

View File

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

View File

@ -19,9 +19,9 @@ const $props = defineProps({
const updateState = async () => { const updateState = async () => {
try { try {
showChangeStateDialog.value = true; showChangeStateDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ id }) => const rowsToUpdate = $props.selectedRows.map(({ ticketFk }) =>
axios.post(`Tickets/state`, { axios.post(`Tickets/state`, {
ticketFk: id, ticketFk,
code: newState.value, code: newState.value,
}), }),
); );
@ -49,8 +49,9 @@ const updateState = async () => {
v-model="newState" v-model="newState"
:options="editableStates" :options="editableStates"
option-label="name" option-label="name"
option-value="code" option-value="id"
autofocus autofocus
data-cy="New state_select"
/> />
</QCardSection> </QCardSection>
<QCardActions align="right"> <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 VnInputDate from 'src/components/common/VnInputDate.vue';
import VnRow from 'src/components/ui/VnRow.vue'; import VnRow from 'src/components/ui/VnRow.vue';
import TicketFilter from './TicketFilter.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 CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue'; import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.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 VnSection from 'src/components/common/VnSection.vue';
import { getAddresses } from 'src/pages/Customer/composables/getAddresses'; import { getAddresses } from 'src/pages/Customer/composables/getAddresses';
import { getAgencies } from 'src/pages/Route/Agency/composables/getAgencies'; import { getAgencies } from 'src/pages/Route/Agency/composables/getAgencies';
import TicketNewPayment from './components/TicketNewPayment.vue';
const route = useRoute(); const route = useRoute();
const router = useRouter(); const router = useRouter();
@ -73,11 +72,6 @@ const initializeFromQuery = () => {
const selectedRows = ref([]); const selectedRows = ref([]);
const hasSelectedRows = computed(() => selectedRows.value.length > 0); 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 dataKey = 'TicketList';
const formInitialData = ref({}); const formInitialData = ref({});
@ -381,87 +375,18 @@ function openBalanceDialog(ticket) {
description.value.push(ticketData.id); description.value.push(ticketData.id);
} }
const balanceCreateDialog = ref({ const dialogData = ref({
amountPaid: amountPaid.value, amountPaid: amountPaid.value,
clientFk: clientFk.value, clientFk: clientFk.value,
description: `Albaran: ${description.value.join(', ')}`, description: `Albaran: ${description.value.join(', ')}`,
}); });
dialogData.value = balanceCreateDialog; quasar.dialog({
showForm.value = true; component: TicketNewPayment,
} componentProps: {
clientId: clientFk.value,
async function onSubmit() { formData: dialogData.value,
const { data: email } = await axios.get('Clients', {
params: {
filter: JSON.stringify({ where: { id: dialogData.value.value.clientFk } }),
}, },
}); });
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) { function exprBuilder(param, value) {
@ -492,16 +417,6 @@ function exprBuilder(param, value) {
</script> </script>
<template> <template>
<FetchData
url="Companies"
@on-fetch="(data) => (companiesOptions = data)"
auto-load
/>
<FetchData
url="Accountings"
@on-fetch="(data) => (accountingOptions = data)"
auto-load
/>
<VnSection <VnSection
:data-key="dataKey" :data-key="dataKey"
:columns="columns" :columns="columns"
@ -742,99 +657,6 @@ function exprBuilder(param, value) {
{{ t('ticketList.accountPayment') }} {{ t('ticketList.accountPayment') }}
</QTooltip> </QTooltip>
</QPageSticky> </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"> <QPageSticky v-if="hasSelectedRows" :offset="[20, 200]" style="z-index: 2">
<QBtn <QBtn
@click="sendDocuware(selectedRows)" @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 toLines: Go to lines
addressNickname: Address nickname addressNickname: Address nickname
ref: Reference ref: Reference
hour: Hour
rounding: Rounding rounding: Rounding
noVerifiedData: No verified data noVerifiedData: No verified data
warehouse: Warehouse warehouse: Warehouse
@ -215,6 +214,8 @@ ticketList:
clientFrozen: Client frozen clientFrozen: Client frozen
componentLack: Component lack componentLack: Component lack
negative: negative:
nextEntryFk: Next entry
nextEntryLanded: Next entry landed
hour: Hour hour: Hour
id: Id Article id: Id Article
longName: Article longName: Article
@ -225,6 +226,7 @@ negative:
value: Negative value: Negative
itemFk: Article itemFk: Article
producer: Producer producer: Producer
excludedDates: Excluded dates
warehouse: Warehouse warehouse: Warehouse
warehouseFk: Warehouse warehouseFk: Warehouse
category: Category category: Category

View File

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

View File

@ -18,6 +18,7 @@ const invoiceInCard = {
'InvoiceInIntrastat', 'InvoiceInIntrastat',
'InvoiceInCorrective', 'InvoiceInCorrective',
'InvoiceInLog', 'InvoiceInLog',
'InvoiceInVehicle',
], ],
}, },
children: [ children: [
@ -75,6 +76,15 @@ const invoiceInCard = {
}, },
component: () => import('src/pages/InvoiceIn/Card/InvoiceInCorrective.vue'), component: () => import('src/pages/InvoiceIn/Card/InvoiceInCorrective.vue'),
}, },
{
name: 'InvoiceInVehicle',
path: 'vehicle',
meta: {
title: 'vehicle',
icon: 'directions_car',
},
component: () => import('src/pages/InvoiceIn/Card/InvoiceInVehicle.vue'),
},
{ {
name: 'InvoiceInLog', name: 'InvoiceInLog',
path: 'log', path: 'log',

View File

@ -20,8 +20,8 @@ const itemCard = {
}, },
children: [ children: [
{ {
name: 'ItemSummary',
path: 'summary', path: 'summary',
name: 'ItemSummary',
meta: { meta: {
title: 'summary', title: 'summary',
icon: 'launch', icon: 'launch',
@ -189,6 +189,7 @@ export default {
title: 'list', title: 'list',
icon: 'view_list', icon: 'view_list',
}, },
component: () => import('src/pages/Item/ItemList.vue'),
}, },
itemCard, itemCard,
], ],
@ -202,19 +203,6 @@ export default {
}, },
component: () => import('src/pages/Item/ItemRequest.vue'), component: () => import('src/pages/Item/ItemRequest.vue'),
}, },
{
path: 'waste-breakdown',
name: 'WasteBreakdown',
meta: {
title: 'wasteBreakdown',
icon: 'vn:claims',
},
beforeEnter: (to, from, next) => {
next({ name: 'ItemList' });
window.location.href =
'https://grafana.verdnatura.es/d/TTNXQAxVk';
},
},
{ {
path: 'fixed-price', path: 'fixed-price',
name: 'ItemFixedPrice', name: 'ItemFixedPrice',
@ -228,6 +216,11 @@ export default {
path: 'item-type', path: 'item-type',
name: 'ItemTypeMain', name: 'ItemTypeMain',
redirect: { name: 'ItemTypeList' }, redirect: { name: 'ItemTypeList' },
meta: {
title: 'itemType',
icon: 'family_restroom',
moduleName: 'itemType',
},
component: () => import('src/pages/Item/ItemTypeList.vue'), component: () => import('src/pages/Item/ItemTypeList.vue'),
children: [ children: [
{ {
@ -235,8 +228,9 @@ export default {
path: 'list', path: 'list',
meta: { meta: {
title: 'family', title: 'family',
icon: 'contact_support', icon: 'family_restroom',
}, },
component: () => import('src/pages/Item/ItemTypeList.vue'),
}, },
itemTypeCard, itemTypeCard,
], ],

View File

@ -119,44 +119,45 @@ const agencyCard = {
], ],
}; };
const roadmapCard = { // Waiting for the roadmap to be implemented refs #8227
path: ':id', // const roadmapCard = {
name: 'RoadmapCard', // path: ':id',
component: () => import('src/pages/Route/Roadmap/RoadmapCard.vue'), // name: 'RoadmapCard',
redirect: { name: 'RoadmapSummary' }, // component: () => import('src/pages/Route/Roadmap/RoadmapCard.vue'),
meta: { // redirect: { name: 'RoadmapSummary' },
menu: ['RoadmapBasicData', 'RoadmapStops'], // meta: {
}, // menu: ['RoadmapBasicData', 'RoadmapStops'],
children: [ // },
{ // children: [
name: 'RoadmapSummary', // {
path: 'summary', // name: 'RoadmapSummary',
meta: { // path: 'summary',
title: 'summary', // meta: {
icon: 'open_in_new', // title: 'summary',
}, // icon: 'open_in_new',
component: () => import('pages/Route/Roadmap/RoadmapSummary.vue'), // },
}, // component: () => import('pages/Route/Roadmap/RoadmapSummary.vue'),
{ // },
name: 'RoadmapBasicData', // {
path: 'basic-data', // name: 'RoadmapBasicData',
meta: { // path: 'basic-data',
title: 'basicData', // meta: {
icon: 'vn:settings', // title: 'basicData',
}, // icon: 'vn:settings',
component: () => import('pages/Route/Roadmap/RoadmapBasicData.vue'), // },
}, // component: () => import('pages/Route/Roadmap/RoadmapBasicData.vue'),
{ // },
name: 'RoadmapStops', // {
path: 'stops', // name: 'RoadmapStops',
meta: { // path: 'stops',
title: 'stops', // meta: {
icon: 'vn:lines', // title: 'stops',
}, // icon: 'vn:lines',
component: () => import('pages/Route/Roadmap/RoadmapStops.vue'), // },
}, // component: () => import('pages/Route/Roadmap/RoadmapStops.vue'),
], // },
}; // ],
// };
const vehicleCard = { const vehicleCard = {
path: ':id', path: ':id',
@ -241,7 +242,7 @@ export default {
'RouteList', 'RouteList',
'RouteExtendedList', 'RouteExtendedList',
'RouteAutonomous', 'RouteAutonomous',
'RouteRoadmap', // 'RouteRoadmap', Waiting for the roadmap to be implemented refs #8227
'CmrList', 'CmrList',
'AgencyList', 'AgencyList',
'VehicleList', 'VehicleList',
@ -301,28 +302,29 @@ export default {
}, },
component: () => import('src/pages/Route/RouteAutonomous.vue'), component: () => import('src/pages/Route/RouteAutonomous.vue'),
}, },
{ // Waiting for the roadmap to be implemented refs #8227
path: 'roadmap', // {
name: 'RouteRoadmap', // path: 'roadmap',
redirect: { name: 'RoadmapList' }, // name: 'RouteRoadmap',
component: () => import('src/pages/Route/RouteRoadmap.vue'), // redirect: { name: 'RoadmapList' },
meta: { // component: () => import('src/pages/Route/RouteRoadmap.vue'),
title: 'RouteRoadmap', // meta: {
icon: 'vn:troncales', // title: 'RouteRoadmap',
}, // icon: 'vn:troncales',
children: [ // },
{ // children: [
name: 'RoadmapList', // {
path: 'list', // name: 'RoadmapList',
meta: { // path: 'list',
title: 'list', // meta: {
icon: 'view_list', // title: 'list',
}, // icon: 'view_list',
component: () => import('src/pages/Route/RouteRoadmap.vue'), // },
}, // component: () => import('src/pages/Route/RouteRoadmap.vue'),
roadmapCard, // },
], // roadmapCard,
}, // ],
// },
{ {
path: 'cmr', path: 'cmr',
name: 'CmrList', name: 'CmrList',

View File

@ -31,7 +31,7 @@ describe('ClaimDevelopment', { testIsolation: true }, () => {
cy.saveCard(); cy.saveCard();
}); });
it('should add and remove new line', () => { it.skip('should add and remove new line', () => {
cy.addCard(); cy.addCard();
cy.waitForElement(thirdRow); cy.waitForElement(thirdRow);

View File

@ -5,8 +5,8 @@ describe('EntryList', () => {
cy.login('buyer'); cy.login('buyer');
cy.visit(`/#/entry/list`); cy.visit(`/#/entry/list`);
}); });
// fix on task https://redmine.verdnatura.es/issues/8638
it('View popup summary', () => { it.skip('View popup summary', () => {
cy.createEntry(); cy.createEntry();
cy.get('.q-notification__message').eq(0).should('have.text', 'Data created'); cy.get('.q-notification__message').eq(0).should('have.text', 'Data created');
cy.waitForElement('[data-cy="entry-buys"]'); cy.waitForElement('[data-cy="entry-buys"]');

View File

@ -37,7 +37,7 @@ describe('InvoiceInBasicData', { testIsolation: true }, () => {
cy.validateForm(mock, { attr: 'data-cy' }); cy.validateForm(mock, { attr: 'data-cy' });
}); });
it('should edit, remove and create the dms data', () => { xit('should edit, remove and create the dms data', () => {
const firtsInput = 'Ticket:65'; const firtsInput = 'Ticket:65';
const secondInput = "I don't know what posting here!"; const secondInput = "I don't know what posting here!";

View File

@ -14,7 +14,7 @@ describe('InvoiceInDueDay', () => {
cy.get('.q-notification__message').should('have.text', 'Data saved'); cy.get('.q-notification__message').should('have.text', 'Data saved');
}); });
it('should remove the first line', () => { xit('should remove the first line', () => {
cy.removeRow(1); cy.removeRow(1);
}); });

View File

@ -30,7 +30,7 @@ describe('InvoiceInList', () => {
}); });
}); });
it('should open the details', () => { xit('should open the details', () => {
cy.get('[data-col-field="id"]').then(($cells) => { cy.get('[data-col-field="id"]').then(($cells) => {
const exactMatch = [...$cells].find( const exactMatch = [...$cells].find(
(cell) => cell.textContent.trim() === invoiceId, (cell) => cell.textContent.trim() === invoiceId,

View File

@ -1,7 +1,100 @@
describe('InvoiceInSummary', () => { describe('InvoiceInSummary', () => {
const url = '/#/invoice-in/1/summary';
const selectors = {
supplierLink: '[data-cy="invoiceInSummary_supplier"]',
vehicleLink: '[data-cy="invoiceInSummary_vehicle"]',
descriptorOpenSummaryBtn: '.q-menu .descriptor [data-cy="openSummaryBtn"]',
descriptorGoToSummaryBtn: '.q-menu .descriptor [data-cy="goToSummaryBtn"]',
summaryGoToSummaryBtn: '.summaryHeader [data-cy="goToSummaryBtn"]',
supplierDescriptorTitle: '[data-cy="vnDescriptor_description"]',
vehicleDescriptorTitle: '[data-cy="vnDescriptor_title"]',
};
const sectionSelectors = {
basicDataIcon: 'InvoiceInBasicData-menu-item',
vatIcon: 'InvoiceInVat-menu-item',
dueDayIcon: 'InvoiceInDueDay-menu-item',
intrastatIcon: 'InvoiceInIntrastat-menu-item',
vehicleIcon: 'InvoiceInVehicle-menu-item',
logIcon: 'InvoiceInLog-menu-item',
};
const titlesLinks = {
basicData: '[data-cy="basicDataTitleLink"].link',
vat: '[data-cy="vatTitleLink"].link',
dueDay: '[data-cy="dueDayTitleLink"].link',
intrastat: '[data-cy="intrastatTitleLink"].link',
vehicle: '[data-cy="vehicleTitleLink"].link',
};
const basePath = 'invoice-in/1/';
const sectionRegex = {
basicData: new RegExp(`${basePath}basic-data`),
vat: new RegExp(`${basePath}vat`),
dueDay: new RegExp(`${basePath}due-day`),
intrastat: new RegExp(`${basePath}intrastat`),
vehicle: new RegExp(`${basePath}vehicle`),
log: new RegExp(`${basePath}log`),
};
const supplierSummaryUrlRegex = /supplier\/\d+\/summary/;
const vehicleSummaryUrlRegex = /vehicle\/\d+\/summary/;
beforeEach(() => { beforeEach(() => {
cy.login('administrative'); cy.login('administrative');
cy.visit('/#/invoice-in/3/summary'); cy.visit(url);
});
it('Should redirect to the corresponding section when clicking on the icons in the left menu', () => {
cy.dataCy(sectionSelectors.basicDataIcon).click();
cy.location().should('match', sectionRegex.basicData);
cy.visit(url);
cy.dataCy(sectionSelectors.vatIcon).click();
cy.location().should('match', sectionRegex.vat);
cy.visit(url);
cy.dataCy(sectionSelectors.dueDayIcon).click();
cy.location().should('match', sectionRegex.dueDay);
cy.visit(url);
cy.dataCy(sectionSelectors.intrastatIcon).click();
cy.location().should('match', sectionRegex.intrastat);
cy.visit(url);
cy.dataCy(sectionSelectors.vehicleIcon).click();
cy.location().should('match', sectionRegex.vehicle);
cy.visit(url);
cy.dataCy(sectionSelectors.logIcon).click();
cy.location().should('match', sectionRegex.log);
});
describe('Title links redirections', () => {
it('Should redirect to invoiceIn basic-data when clicking on basic-data title link', () => {
cy.get(titlesLinks.basicData).click();
cy.location().should('match', sectionRegex.basicData);
});
it('Should redirect to invoiceIn vat when clicking on vat title link', () => {
cy.get(titlesLinks.vat).click();
cy.location().should('match', sectionRegex.vat);
});
it('Should redirect to invoiceIn due-day when clicking on due-day title link', () => {
cy.get(titlesLinks.dueDay).click();
cy.location().should('match', sectionRegex.dueDay);
});
it('Should redirect to invoiceIn intrastat when clicking on intrastat title link', () => {
cy.get(titlesLinks.intrastat).click();
cy.location().should('match', sectionRegex.intrastat);
});
it('Should redirect to invoiceIn vehicle when clicking on vehicle title link', () => {
cy.get(titlesLinks.vehicle).click();
cy.location().should('match', sectionRegex.vehicle);
});
}); });
it('should booking and unbooking the invoice properly', () => { it('should booking and unbooking the invoice properly', () => {
@ -9,16 +102,53 @@ describe('InvoiceInSummary', () => {
cy.dataCy('invoiceInSummary_book').click(); cy.dataCy('invoiceInSummary_book').click();
cy.dataCy('VnConfirm_confirm').click(); cy.dataCy('VnConfirm_confirm').click();
cy.validateCheckbox(checkbox); cy.validateCheckbox(checkbox);
cy.selectDescriptorOption();
cy.dataCy('VnConfirm_confirm').click();
}); });
it('should open the supplier descriptor popup', () => { describe('Supplier pop-ups', () => {
cy.intercept('GET', /Suppliers\/\d+/).as('getSupplier'); it('Should redirect to the supplier summary from the supplier descriptor pop-up', () => {
cy.dataCy('invoiceInSummary_supplier').then(($el) => { cy.checkRedirectionFromPopUp({
const description = $el.text().trim(); selectorToClick: selectors.supplierLink,
$el.click(); steps: [selectors.descriptorGoToSummaryBtn],
cy.wait('@getSupplier').then(() => expectedUrlRegex: supplierSummaryUrlRegex,
cy.validateDescriptor({ description, popup: true }), expectedTextSelector: selectors.supplierDescriptorTitle,
); });
});
it('Should redirect to the supplier summary from summary pop-up from the supplier descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.supplierLink,
steps: [
selectors.descriptorOpenSummaryBtn,
selectors.summaryGoToSummaryBtn,
],
expectedUrlRegex: supplierSummaryUrlRegex,
expectedTextSelector: selectors.supplierDescriptorTitle,
});
});
});
describe('Vehicle pop-ups', () => {
it('Should redirect to the vehicle summary from the vehicle descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.vehicleLink,
steps: [selectors.descriptorGoToSummaryBtn],
expectedUrlRegex: vehicleSummaryUrlRegex,
expectedTextSelector: selectors.vehicleDescriptorTitle,
});
});
it('Should redirect to the vehicle summary from summary pop-up from the vehicle descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.vehicleLink,
steps: [
selectors.descriptorOpenSummaryBtn,
selectors.summaryGoToSummaryBtn,
],
expectedUrlRegex: vehicleSummaryUrlRegex,
expectedTextSelector: selectors.vehicleDescriptorTitle,
});
}); });
}); });
}); });

View File

@ -0,0 +1,54 @@
describe('InvoiceInVehicle', () => {
const selectors = {
tableActionUnlink: 'tableAction-0',
firstVehicleLink:
'tr:first-child > [data-col-field="vehicleFk"] > .no-padding > .link',
descriptorOpenSummaryBtn: '.q-menu .descriptor [data-cy="openSummaryBtn"]',
descriptorGoToSummaryBtn: '.q-menu .descriptor [data-cy="goToSummaryBtn"]',
summaryGoToSummaryBtn: '.summaryHeader [data-cy="goToSummaryBtn"]',
descriptorTitle: '[data-cy="vnDescriptor_title"]',
};
const vehicleSummaryUrlRegex = /vehicle\/\d+\/summary/;
beforeEach(() => {
cy.login('administrative');
cy.visit(`/#/invoice-in/1/vehicle`);
});
it('should link and unlink vehicle to the invoice', () => {
const data = {
Vehicle: { val: '2222-IMK', type: 'select' },
Amount: { val: 125 },
};
cy.addBtnClick();
cy.fillInForm(data);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
cy.dataCy(selectors.tableActionUnlink).last().click();
cy.clickConfirm();
cy.checkNotification('Unlinked vehicle');
});
describe('Vehicle pop-ups', () => {
xit('Should redirect to the vehicle summary from the vehicle descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.firstVehicleLink,
steps: [selectors.descriptorGoToSummaryBtn],
expectedUrlRegex: vehicleSummaryUrlRegex,
expectedTextSelector: selectors.descriptorTitle,
});
});
xit('Should redirect to the vehicle summary from summary pop-up from the vehicle descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.firstVehicleLink,
steps: [
selectors.descriptorOpenSummaryBtn,
selectors.summaryGoToSummaryBtn,
],
expectedUrlRegex: vehicleSummaryUrlRegex,
expectedTextSelector: selectors.descriptorTitle,
});
});
});
});

View File

@ -0,0 +1,21 @@
describe('Item list', () => {
beforeEach(() => {
cy.login('buyer');
cy.visit(`/#/item/1/basic-data`);
});
const mock = {
itemBasicDataItemType: { val: 'Container', type: 'select' },
itemBasicDataReference: '1',
itemBasicDataRelevancy: '1',
itemBasicDataStems: '1',
itemBasicDataMultiplier: '2',
itemBasicDataGeneric: { val: 'Pallet', type: 'select' },
};
it('should edit every field', () => {
cy.fillInForm(mock, { attr: 'data-cy' });
cy.saveCard();
cy.validateForm(mock, { attr: 'data-cy' });
});
});

View File

@ -6,19 +6,16 @@ describe('Item summary', { testIsolation: true }, () => {
}); });
it('should clone the item', () => { it('should clone the item', () => {
cy.dataCy('descriptor-more-opts').click(); cy.selectDescriptorOption(2);
cy.get('.q-menu > .q-list > :nth-child(2) > .q-item__section').click();
cy.dataCy('VnConfirm_confirm').click(); cy.dataCy('VnConfirm_confirm').click();
cy.waitForElement('[data-cy="itemTags"]'); cy.waitForElement('[data-cy="itemTags"]');
cy.dataCy('itemTags').should('be.visible'); cy.dataCy('itemTags').should('be.visible');
}); });
it('should regularize stock', () => { it('should regularize stock', () => {
cy.dataCy('descriptor-more-opts').click(); cy.selectDescriptorOption();
cy.get('.q-menu > .q-list > :nth-child(1) > .q-item__section').click();
cy.dataCy('regularizeStockInput').type('10'); cy.dataCy('regularizeStockInput').type('10');
cy.dataCy('Warehouse_select').type('Warehouse One{enter}'); cy.dataCy('Warehouse_select').type('Warehouse One{enter}');
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created'); cy.checkNotification('Data created');
}); });
}); });

View File

@ -1,22 +1,21 @@
/// <reference types="cypress" />
describe('Item list', () => { describe('Item list', () => {
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.login('buyer');
cy.visit(`/#/item/list`); cy.visit(`/#/item/list`);
cy.typeSearchbar('{enter}');
}); });
it('should filter the items and redirect to the summary', () => { it('should filter the items and redirect to the summary', () => {
cy.dataCy('Category_select').type('Plant'); cy.selectOption('[data-cy="Category_select"]', 'Plant');
cy.get('.q-menu .q-item').contains('Plant').click(); cy.selectOption('[data-cy="Type_select"]', 'Anthurium');
cy.dataCy('Type_select').type('Anthurium'); cy.get('td[data-row-index="0"][data-col-field="description"]')
cy.get('.q-menu .q-item').contains('Anthurium').click(); .should('exist')
cy.get('.q-virtual-scroll__content > :nth-child(4) > :nth-child(4)').click(); .click();
cy.url().should('include', '/summary');
}); });
it('should create an item', () => { it('should create an item', () => {
const data = { const data = {
'Provisional name': { val: `Test item` },
Description: { val: `Test item` }, Description: { val: `Test item` },
Type: { val: `Crisantemo`, type: 'select' }, Type: { val: `Crisantemo`, type: 'select' },
Intrastat: { val: `Coral y materiales similares`, type: 'select' }, Intrastat: { val: `Coral y materiales similares`, type: 'select' },
@ -26,8 +25,6 @@ describe('Item list', () => {
cy.fillInForm(data); cy.fillInForm(data);
cy.dataCy('FormModelPopup_save').click(); cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created'); cy.checkNotification('Data created');
cy.get( cy.url().should('include', '/basic-data');
':nth-child(2) > .q-drawer > .q-drawer__content > .q-scrollarea > .q-scrollarea__container > .q-scrollarea__content',
).should('be.visible');
}); });
}); });

View File

@ -0,0 +1,28 @@
describe('Item Request', () => {
const rowIndex = 0;
const itemFkField = `td[data-row-index="${rowIndex}"][data-col-field="itemFk"]`;
const saleQuantityField = `td[data-row-index="${rowIndex}"][data-col-field="saleQuantity"]`;
const ticketInputFilter = '[data-cy="Ticket id_input"]';
before(() => {
cy.login('buyer');
cy.visit(`/#/item/request`);
});
it('should fill the id and quantity then check the concept was updated', () => {
cy.waitForElement('tbody');
cy.get(ticketInputFilter).should('exist').type('38{enter}');
cy.get(itemFkField).should('exist').click();
cy.get(`${itemFkField} input`).should('exist').type('4');
cy.get(saleQuantityField).should('exist').click();
cy.get(`${saleQuantityField} input`).should('exist').type('10{esc}');
cy.checkNotification('Data saved');
});
it('should now click on the second deny request icon then type the reason', () => {
cy.selectOption('[data-cy="State_select"]', 'Pending');
cy.get('button[title="Discard"]').eq(0).click();
cy.dataCy('discardTextArea').should('exist').type('test{enter}');
cy.checkNotification('Data saved');
});
});

View File

@ -1,23 +1,21 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe('Item type', () => { describe('Item type', { testIsolation: true }, () => {
const workerError = 'employeeNick'; const workerError = 'employeeNick';
const worker = 'buyerNick'; const worker = 'buyerNick';
const category = 'Artificial'; const category = 'Artificial';
const type = 'Flower'; const type = 'Flower';
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.login('buyer');
cy.visit(`/#/item/item-type`); cy.visit(`/#/item/item-type`);
}); });
it('should throw an error if the code already exists', () => { it('should throw an error if the code already exists', () => {
cy.dataCy('vnTableCreateBtn').click(); cy.dataCy('vnTableCreateBtn').should('exist').click();
cy.dataCy('codeInput').type('ALS'); cy.dataCy('codeInput').type('ALS');
cy.dataCy('nameInput').type('Alstroemeria'); cy.dataCy('nameInput').type('Alstroemeria');
cy.dataCy('vnWorkerSelect').type(workerError); cy.selectOption('[data-cy="vnWorkerSelect"]', workerError);
cy.get('.q-menu .q-item').contains(workerError).click(); cy.selectOption('[data-cy="itemCategorySelect"]', category);
cy.dataCy('itemCategorySelect').type(category);
cy.get('.q-menu .q-item').contains(category).click();
cy.dataCy('FormModelPopup_save').click(); cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('An item type with the same code already exists'); cy.checkNotification('An item type with the same code already exists');
}); });
@ -26,10 +24,8 @@ describe('Item type', () => {
cy.dataCy('vnTableCreateBtn').click(); cy.dataCy('vnTableCreateBtn').click();
cy.dataCy('codeInput').type('LIL'); cy.dataCy('codeInput').type('LIL');
cy.dataCy('nameInput').type('Lilium'); cy.dataCy('nameInput').type('Lilium');
cy.dataCy('vnWorkerSelect').type(worker); cy.selectOption('[data-cy="vnWorkerSelect"]', worker);
cy.get('.q-menu .q-item').contains(worker).click(); cy.selectOption('[data-cy="itemCategorySelect"]', type);
cy.dataCy('itemCategorySelect').type(type);
cy.get('.q-menu .q-item').contains(type).click();
cy.dataCy('FormModelPopup_save').click(); cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created'); cy.checkNotification('Data created');
}); });

View File

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

View File

@ -133,7 +133,7 @@ describe('Vehicle DMS', { testIsolation: true }, () => {
); );
}); });
describe('Worker pop-ups', () => { describe.skip('Worker pop-ups', () => {
it('Should redirect to worker summary from worker descriptor pop-up', () => { it('Should redirect to worker summary from worker descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({ cy.checkRedirectionFromPopUp({
selectorToClick: selectors.firstRowWorkerLink, selectorToClick: selectors.firstRowWorkerLink,

View File

@ -1,146 +1,161 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
describe.skip('Ticket Lack detail', () => { const firstRow = 'tr.cursor-pointer > :nth-child(1)';
beforeEach(() => { const ticketId = 1000000;
cy.login('developer'); const clickNotificationAction = () => {
cy.intercept('GET', /\/api\/Tickets\/itemLack\/5.*$/, { const notification = '.q-notification';
statusCode: 200, cy.waitForElement(notification);
body: [ cy.get(notification).should('be.visible');
{ cy.get('.q-notification__actions > .q-btn').click();
saleFk: 33, cy.get('@open').should((openStub) => {
code: 'OK', expect(openStub).to.be.called;
ticketFk: 142, const firstArg = openStub.args[0][0];
nickname: 'Malibu Point', expect(firstArg).to.match(/\/ticket\/\d+\/sale/);
shipped: '2000-12-31T23:00:00.000Z', expect(firstArg).to.include(`/ticket/${ticketId}/sale`);
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');
}); });
describe('Table actions', () => { };
it('should display only one row in the lack list', () => { describe('Ticket Lack detail', { testIsolation: true }, () => {
cy.location('href').should('contain', '#/ticket/negative/5'); beforeEach(() => {
cy.viewport(1980, 1020);
cy.get('[data-cy="changeItem"]').should('be.disabled'); cy.login('developer');
cy.get('[data-cy="changeState"]').should('be.disabled'); cy.intercept('GET', /\/api\/Tickets\/itemLack\/88.*$/).as('getItemLack');
cy.get('[data-cy="changeQuantity"]').should('be.disabled'); cy.visit('/#/ticket/negative/88');
cy.get('[data-cy="itemProposal"]').should('be.disabled'); cy.window().then((win) => {
cy.get('[data-cy="transferLines"]').should('be.disabled'); cy.stub(win, 'open').as('open');
cy.get('tr.cursor-pointer > :nth-child(1)').click(); });
cy.get('[data-cy="changeItem"]').should('be.enabled'); cy.wait('@getItemLack').then((interception) => {
cy.get('[data-cy="changeState"]').should('be.enabled'); const { query } = interception.request;
cy.get('[data-cy="changeQuantity"]').should('be.enabled'); const filter = JSON.parse(query.filter);
cy.get('[data-cy="itemProposal"]').should('be.enabled'); expect(filter).to.have.property('where');
cy.get('[data-cy="transferLines"]').should('be.enabled'); 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', () => { describe('Item proposal', () => {
beforeEach(() => { beforeEach(() => {
cy.get('tr.cursor-pointer > :nth-child(1)').click(); cy.get(firstRow).click();
cy.dataCy('itemProposal').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');
}); });
describe.skip('Replace item if', () => { describe('Replace item if', () => {
it('Quantity is less than available', () => { xit('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');
});
xit('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" /> /// <reference types="cypress" />
describe('Ticket Lack list', () => { describe('Ticket Lack list', () => {
beforeEach(() => { beforeEach(() => {
cy.login('developer'); cy.viewport(1980, 1020);
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.login('developer');
cy.visit('/#/ticket/negative'); cy.visit('/#/ticket/negative');
}); });
describe('Table actions', () => { describe('Table actions', () => {
it('should display only one row in the lack list', () => { 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.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click();
cy.location('href').should('contain', '#/ticket/negative/5'); cy.location('href').should('contain', '#/ticket/negative/5');
}); });

View File

@ -6,7 +6,7 @@ describe('TicketBasicData', () => {
cy.visit('/#/ticket/31/basic-data'); cy.visit('/#/ticket/31/basic-data');
}); });
it('Should redirect to customer basic data', () => { it.skip('Should redirect to customer basic data', () => {
cy.get('.q-page').should('be.visible'); cy.get('.q-page').should('be.visible');
cy.get(':nth-child(2) > div > .text-primary').click(); cy.get(':nth-child(2) > div > .text-primary').click();
cy.dataCy('Address_select').click(); cy.dataCy('Address_select').click();
@ -16,7 +16,7 @@ describe('TicketBasicData', () => {
).click(); ).click();
cy.url().should('include', '/customer/1104/basic-data'); cy.url().should('include', '/customer/1104/basic-data');
}); });
it.only('stepper', () => { it('stepper', () => {
cy.get('.q-stepper__tab--active').should('have.class', 'q-stepper__tab--active'); cy.get('.q-stepper__tab--active').should('have.class', 'q-stepper__tab--active');
cy.get('.q-stepper__nav > .q-btn--standard').click(); cy.get('.q-stepper__nav > .q-btn--standard').click();

View File

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

View File

@ -1,6 +1,6 @@
/// <reference types="cypress" /> /// <reference types="cypress" />
// https://redmine.verdnatura.es/issues/8848 // https://redmine.verdnatura.es/issues/8848
describe.skip('VnShortcuts', () => { describe('VnShortcuts', () => {
const modules = { const modules = {
item: 'a', item: 'a',
customer: 'c', customer: 'c',

View File

@ -1,3 +1,9 @@
Cypress.Commands.add('handleVnConfirm', () => {
cy.confirmVnConfirm();
cy.checkNotification('Data saved');
});
Cypress.Commands.add('confirmVnConfirm', () => Cypress.Commands.add('confirmVnConfirm', () =>
cy.dataCy('VnConfirm_confirm').should('exist').click(), 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`, `: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}"]`);
}
});