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) {
const clickedElement = event.target.closest('td');
if (!clickedElement) return;
event.preventDefault();
target.value = event.target;
qmenuRef.value.show();
colField.value = clickedElement.getAttribute('data-col-field');

View File

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

View File

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

View File

@ -36,8 +36,6 @@ const validate = async () => {
isLoading.value = true;
await props.submitFn(newPassword, oldPassword);
emit('onSubmit');
} catch (e) {
notify('errors.writeRequest', 'negative');
} finally {
changePassDialog.value.hide();
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;
.label {
color: var(--vn-label-color);
width: 9em;
overflow: hidden;
white-space: wrap;
text-overflow: ellipsis;

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -9,8 +9,6 @@ export function getColAlign(col) {
case 'number':
align = 'right';
break;
case 'time':
case 'date':
case 'checkbox':
align = 'center';
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-text-color-contrast: white;
--vn-link-color: #1e90ff;
--vn-input-underline-color: #bdbdbd;
--vn-input-icons-color: #797979;
background-color: var(--vn-page-color);
@ -29,7 +31,7 @@ body.body--light {
body.body--dark {
--vn-header-color: #5d5d5d;
--vn-page-color: #222;
--vn-section-color: #3d3d3d;
--vn-section-color: #3c3b3b;
--vn-section-hover-color: #747474;
--vn-text-color: white;
--vn-label-color: #a8a8a8;
@ -40,6 +42,8 @@ body.body--dark {
--vn-black-text-color: black;
--vn-text-color-contrast: black;
--vn-link-color: #66bfff;
--vn-input-underline-color: #545353;
--vn-input-icons-color: #888787;
background-color: var(--vn-page-color);
@ -155,7 +159,6 @@ select:-webkit-autofill {
cursor: pointer;
}
/* Estilo para el asterisco en campos requeridos */
.q-field.required .q-field__label:after {
content: ' *';
}
@ -290,6 +293,18 @@ input::-webkit-inner-spin-button {
.expand {
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 {

View File

@ -513,26 +513,6 @@ entry:
isRaid: Raid
invoiceNumber: Invoice
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:
params:
ticketFk: Ticket ID
@ -898,6 +878,7 @@ components:
minPrice: Min. Price
itemFk: Item id
dated: Date
date: Date
userPanel:
copyToken: Token copied to clipboard
settings: Settings

View File

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

View File

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

View File

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

View File

@ -52,5 +52,22 @@ export default {
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 VnTitle from 'src/components/common/VnTitle.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 { 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 () => {
invoiceInUrl.value = `${await getUrl('')}invoiceIn/${entityId.value}/`;
});
@ -218,6 +236,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<VnTitle
:url="getLink('basic-data')"
:text="t('globals.pageTitles.basicData')"
data-cy="basicDataTitleLink"
/>
<div class="vn-card-group">
<div class="vn-card-content">
@ -282,7 +301,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
:value="entity.expenseDeductible?.name"
/>
<VnLv
:label="t('invoiceIn.card.company')"
:label="t('globals.company')"
:value="entity.company?.code"
/>
<VnLv
@ -319,8 +338,12 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</div>
</QCard>
<!--Vat-->
<QCard v-if="entity.invoiceInTax.length" class="vat">
<VnTitle :url="getLink('vat')" :text="t('invoiceIn.card.vat')" />
<QCard v-if="entity.invoiceInTax.length" class="col-extend">
<VnTitle
:url="getLink('vat')"
:text="t('globals.pageTitles.vat')"
data-cy="vatTitleLink"
/>
<QTable
:columns="vatColumns"
:rows="entity.invoiceInTax"
@ -372,13 +395,18 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
currency,
)
}}</QTd>
<QTd></QTd>
</QTr>
</template>
</QTable>
</QCard>
<!--Due Day-->
<QCard v-if="entity.invoiceInDueDay.length" class="due-day">
<VnTitle :url="getLink('due-day')" :text="t('invoiceIn.card.dueDay')" />
<QCard v-if="entity.invoiceInDueDay.length" class="col-shrink">
<VnTitle
:url="getLink('due-day')"
:text="t('globals.pageTitles.dueDay')"
data-cy="dueDayTitleLink"
/>
<QTable :columns="dueDayColumns" :rows="entity.invoiceInDueDay" flat>
<template #header="dueDayProps">
<QTr :props="dueDayProps" class="bg">
@ -413,10 +441,11 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</QTable>
</QCard>
<!--Intrastat-->
<QCard v-if="entity.invoiceInIntrastat.length">
<QCard v-if="entity.invoiceInIntrastat.length" class="col-extend">
<VnTitle
:url="getLink('intrastat')"
:text="t('invoiceIn.card.intrastat')"
:text="t('globals.pageTitles.intrastat')"
data-cy="intrastatTitleLink"
/>
<QTable
:columns="intrastatColumns"
@ -450,6 +479,53 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</template>
</QTable>
</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>
</CardSummary>
</template>
@ -463,15 +539,15 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
@media (min-width: $breakpoint-md) {
.summaryBody {
.vat {
.col-extend {
flex: 65%;
}
.due-day {
.col-shrink {
flex: 30%;
}
.vat,
.due-day {
.col-extend,
.col-shrink {
.q-table th {
padding-right: 0;
}

View File

@ -9,7 +9,6 @@ import FetchData from 'src/components/FetchData.vue';
import { getExchange } from 'src/composables/getExchange';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnSelectExpense from 'src/components/common/VnSelectExpense.vue';
import axios from 'axios';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
const { t } = useI18n();
@ -51,7 +50,7 @@ const columns = computed(() => [
create: true,
width: 'max-content',
cellEvent: {
keydown: async (evt, row) => {
keydown: (evt, row) => {
if (evt.key !== 'Tab') return;
const val = evt.target.value;
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
entry: Entry
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:
currency: Currency
issued: Expedition date
@ -71,3 +63,7 @@ invoiceIn:
correctingFk: Rectificative
issued: Issued
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
entry: Entrada
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:
currency: Divisa
docNumber: Número documento
@ -52,7 +45,7 @@ invoiceIn:
expense: Gasto
taxableBase: Base imp.
rate: Tasa
sageTransaction: Sage transación
sageTransaction: Sage transacción
dueDay: Fecha
bank: Caja
foreignValue: Divisa
@ -69,3 +62,7 @@ invoiceIn:
correctingFk: Rectificativa
issued: Fecha de emisión
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"
>
<template #form="{ data }">
<VnRow>
<VnRow class="q-py-sm">
<VnSelect
:label="t('item.basicData.type')"
v-model="data.typeFk"
@ -71,6 +71,7 @@ const onIntrastatCreated = (response, formData) => {
hide-selected
map-options
required
data-cy="itemBasicDataItemType"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
@ -83,23 +84,30 @@ const onIntrastatCreated = (response, formData) => {
</QItem>
</template>
</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
:label="t('item.basicData.relevancy')"
type="number"
v-model="data.relevancy"
data-cy="itemBasicDataRelevancy"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<VnRow class="q-py-sm">
<VnInput
:label="t('item.basicData.stems')"
type="number"
v-model="data.stems"
data-cy="itemBasicDataStems"
/>
<VnInput
:label="t('item.basicData.multiplier')"
type="number"
v-model="data.stemMultiplier"
data-cy="itemBasicDataMultiplier"
/>
<VnSelectDialog
:label="t('item.basicData.generic')"
@ -112,6 +120,7 @@ const onIntrastatCreated = (response, formData) => {
map-options
hide-selected
action-icon="filter_alt"
data-cy="itemBasicDataGeneric"
>
<template #form>
<FilterItemForm
@ -129,7 +138,12 @@ const onIntrastatCreated = (response, formData) => {
</template>
</VnSelectDialog>
</VnRow>
<VnRow>
<VnRow class="q-py-sm">
<VnCheckbox
v-model="data.isCustomInspectionRequired"
:label="t('item.basicData.isCustomInspectionRequired')"
data-cy="itemBasicDataCustomInspection"
/>
<VnSelectDialog
:label="t('item.basicData.intrastat')"
v-model="data.intrastatFk"
@ -138,6 +152,7 @@ const onIntrastatCreated = (response, formData) => {
option-label="description"
map-options
hide-selected
data-cy="itemBasicDataIntrastat"
>
<template #form>
<CreateIntrastatForm
@ -165,78 +180,81 @@ const onIntrastatCreated = (response, formData) => {
option-label="name"
hide-selected
map-options
data-cy="itemBasicDataExpense"
/>
</div>
</VnRow>
<VnRow>
<VnRow class="q-py-sm">
<VnCheckbox
v-model="data.hasKgPrice"
:label="t('item.basicData.hasKgPrice')"
data-cy="itemBasicDataHasKgPrice"
/>
<VnInput
:label="t('item.basicData.weightByPiece')"
v-model.number="data.weightByPiece"
:min="0"
type="number"
data-cy="itemBasicDataWeightByPiece"
/>
<VnInput
:label="t('item.basicData.boxUnits')"
v-model.number="data.packingOut"
:min="0"
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
:label="t('item.basicData.recycledPlastic')"
v-model.number="data.recycledPlastic"
:min="0"
type="number"
data-cy="itemBasicDataRecycledPlastic"
/>
<VnInput
:label="t('item.basicData.nonRecycledPlastic')"
v-model.number="data.nonRecycledPlastic"
:min="0"
type="number"
data-cy="itemBasicDataNonRecycledPlastic"
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<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>
<VnRow class="q-py-sm">
<VnInput
:label="t('item.basicData.description')"
type="textarea"
v-model="data.description"
fill-input
data-cy="itemBasicDataDescription"
/>
<VnInput
v-show="data.isPhotoRequested"
type="textarea"
:label="t('globals.comment')"
:label="t('item.basicData.photoMotivation')"
v-model="data.photoMotivation"
fill-input
data-cy="itemBasicDataPhotoMotivation"
/>
</VnRow>
</template>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -8,7 +8,6 @@ import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import { useArrayData } from 'composables/useArrayData';
import { useValidator } from 'src/composables/useValidator';
import axios from 'axios';
@ -85,26 +84,6 @@ const removeTag = (index, 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 () => {
stateStore.rightDrawer = true;
if (arrayData.store?.userParams?.categoryFk)
@ -125,7 +104,6 @@ onMounted(async () => {
});
});
// Fill fieldFiltersValues with existent userParams
if (arrayData.store?.userParams) {
fieldFiltersValues.value = Object.entries(arrayData.store?.userParams)
.filter(([key, value]) => value && _moreFields.includes(key))
@ -249,6 +227,16 @@ onMounted(async () => {
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
:label="t('params.isFloramondo')"
v-model="params.isFloramondo"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<!-- Tags filter -->
<QItemLabel header>
{{ t('params.tags') }}
@ -315,74 +303,6 @@ onMounted(async () => {
@click="removeTag(index, params, searchFn)"
/>
</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>
</VnFilterPanel>
</template>
@ -410,6 +330,17 @@ en:
Green: Green
Handmade: Handmade
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:
More fields: Más campos
params:
@ -433,4 +364,15 @@ es:
Green: Verde
Handmade: Hecho a mano
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>

View File

@ -1,30 +1,27 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
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 { toCurrency } from 'filters/index';
import useNotify from 'src/composables/useNotify.js';
import { toDate, dashIfEmpty } from 'src/filters';
import axios from 'axios';
import ItemRequestDenyForm from './ItemRequestDenyForm.vue';
import { toDate } from 'src/filters';
import VnTable from 'components/VnTable/VnTable.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 RightMenu from 'src/components/common/RightMenu.vue';
import FormModelPopup from 'src/components/FormModelPopup.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
const { t } = useI18n();
const { notify } = useNotify();
const stateStore = useStateStore();
const denyFormRef = ref(null);
const denyRequestId = ref(null);
const denyRequestIndex = ref(null);
const itemRequestsOptions = ref([]);
const userParams = {
state: 'pending',
daysOnward: 7,
};
const tableRef = ref();
@ -34,9 +31,13 @@ onMounted(async () => {
const columns = computed(() => [
{
name: 'id',
visible: false,
},
{
align: 'right',
label: t('globals.ticketId'),
name: 'ticketFk',
align: 'left',
isId: true,
chip: {
condition: () => true,
@ -44,15 +45,16 @@ const columns = computed(() => [
cardVisible: true,
},
{
align: 'center',
label: t('globals.shipped'),
name: 'shipped',
align: 'left',
component: 'date',
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(toDate(row.shipped)),
columnClass: 'shrink',
isEditable: false,
},
{
label: t('globals.description'),
@ -78,6 +80,7 @@ const columns = computed(() => [
component: null,
},
columnClass: 'shrink',
isEditable: false,
},
{
align: 'left',
@ -91,6 +94,7 @@ const columns = computed(() => [
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.departmentName),
isEditable: false,
},
{
label: t('item.buyRequest.requested'),
@ -121,21 +125,52 @@ const columns = computed(() => [
component: null,
},
columnClass: 'shrink',
isEditable: false,
},
{
label: t('globals.item'),
name: 'item',
name: 'itemFk',
align: 'left',
component: 'input',
columnClass: 'expand',
isEditable: ({ isOk }) => isOk === null,
},
{
label: t('item.buyRequest.achieved'),
name: 'achieved',
name: 'saleQuantity',
align: 'left',
component: 'input',
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'),
@ -144,11 +179,12 @@ const columns = computed(() => [
sortable: true,
component: 'input',
columnClass: 'expand',
isEditable: false,
},
{
label: t('globals.state'),
name: 'state',
format: (row) => getState(row.isOk),
format: ({ isOk }) => getState(isOk),
align: 'left',
},
{
@ -163,7 +199,23 @@ const columns = computed(() => [
{
align: 'right',
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';
};
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) => {
if (isOk === null) return t('Pending');
else if (isOk) return t('Accepted');
else return t('Denied');
};
const showDenyRequestForm = (requestId, rowIndex) => {
const showDenyRequestForm = (requestId) => {
denyRequestId.value = requestId;
denyRequestIndex.value = rowIndex;
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>
<template>
<RightMenu>
<template #right-panel>
@ -240,12 +255,13 @@ const onDenyAccept = (_, responseData) => {
data-key="itemRequest"
url="ticketRequests/filter"
order="shipped ASC, isOk ASC"
:columns="columns"
:user-params="userParams"
:right-search="false"
auto-load
:is-editable="true"
:columns="columns"
:disable-option="{ card: true }"
chip-locale="item.params"
:right-search="false"
:default-remove="false"
auto-load
>
<template #column-ticketFk="{ row }">
<span class="link">
@ -254,16 +270,14 @@ const onDenyAccept = (_, responseData) => {
</span>
</template>
<template #column-shipped="{ row }">
<QTd>
<QBadge
:color="getBadgeColor(row.shipped)"
text-color="black"
class="q-pa-sm"
style="font-size: 14px"
>
{{ toDate(row.shipped) }}
</QBadge>
</QTd>
<QBadge
:color="getBadgeColor(row.shipped)"
text-color="black"
class="q-pa-xs"
style="font-size: 14px"
>
{{ toDate(row.shipped) }}
</QBadge>
</template>
<template #column-attenderName="{ row }">
<span class="link" @click.stop>
@ -284,74 +298,34 @@ const onDenyAccept = (_, responseData) => {
<DepartmentDescriptorProxy :id="row.departmentFk" />
</span>
</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 }">
<span @click.stop disabled="row.isOk != null">
{{ row.itemDescription }}
<span :class="{ link: row.itemDescription }" @click.stop>
{{ dashIfEmpty(row.itemDescription) }}
<ItemDescriptorProxy v-if="row.itemFk" :id="row.itemFk" />
</span>
</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>
<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>
</template>
<i18n>
es:
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
:label="t('params.mine')"
v-model="params.mine"
:toggle-indeterminate="false"
:toggle-indeterminate="null"
/>
</QItemSection>
</QItem>
@ -212,6 +212,13 @@ en:
state: State
daysOnward: Days onward
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
denied: Denied
accepted: Accepted
@ -230,6 +237,13 @@ es:
state: Estado
daysOnward: Días en adelante
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
denied: Denegada
accepted: Aceptada

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,6 +18,7 @@ const invoiceInCard = {
'InvoiceInIntrastat',
'InvoiceInCorrective',
'InvoiceInLog',
'InvoiceInVehicle',
],
},
children: [
@ -75,6 +76,15 @@ const invoiceInCard = {
},
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',
path: 'log',

View File

@ -20,8 +20,8 @@ const itemCard = {
},
children: [
{
name: 'ItemSummary',
path: 'summary',
name: 'ItemSummary',
meta: {
title: 'summary',
icon: 'launch',
@ -189,6 +189,7 @@ export default {
title: 'list',
icon: 'view_list',
},
component: () => import('src/pages/Item/ItemList.vue'),
},
itemCard,
],
@ -202,19 +203,6 @@ export default {
},
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',
name: 'ItemFixedPrice',
@ -228,6 +216,11 @@ export default {
path: 'item-type',
name: 'ItemTypeMain',
redirect: { name: 'ItemTypeList' },
meta: {
title: 'itemType',
icon: 'family_restroom',
moduleName: 'itemType',
},
component: () => import('src/pages/Item/ItemTypeList.vue'),
children: [
{
@ -235,8 +228,9 @@ export default {
path: 'list',
meta: {
title: 'family',
icon: 'contact_support',
icon: 'family_restroom',
},
component: () => import('src/pages/Item/ItemTypeList.vue'),
},
itemTypeCard,
],

View File

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

View File

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

View File

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

View File

@ -37,7 +37,7 @@ describe('InvoiceInBasicData', { testIsolation: true }, () => {
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 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');
});
it('should remove the first line', () => {
xit('should remove the first line', () => {
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) => {
const exactMatch = [...$cells].find(
(cell) => cell.textContent.trim() === invoiceId,

View File

@ -1,7 +1,100 @@
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(() => {
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', () => {
@ -9,16 +102,53 @@ describe('InvoiceInSummary', () => {
cy.dataCy('invoiceInSummary_book').click();
cy.dataCy('VnConfirm_confirm').click();
cy.validateCheckbox(checkbox);
cy.selectDescriptorOption();
cy.dataCy('VnConfirm_confirm').click();
});
it('should open the supplier descriptor popup', () => {
cy.intercept('GET', /Suppliers\/\d+/).as('getSupplier');
cy.dataCy('invoiceInSummary_supplier').then(($el) => {
const description = $el.text().trim();
$el.click();
cy.wait('@getSupplier').then(() =>
cy.validateDescriptor({ description, popup: true }),
);
describe('Supplier pop-ups', () => {
it('Should redirect to the supplier summary from the supplier descriptor pop-up', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.supplierLink,
steps: [selectors.descriptorGoToSummaryBtn],
expectedUrlRegex: supplierSummaryUrlRegex,
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', () => {
cy.dataCy('descriptor-more-opts').click();
cy.get('.q-menu > .q-list > :nth-child(2) > .q-item__section').click();
cy.selectDescriptorOption(2);
cy.dataCy('VnConfirm_confirm').click();
cy.waitForElement('[data-cy="itemTags"]');
cy.dataCy('itemTags').should('be.visible');
});
it('should regularize stock', () => {
cy.dataCy('descriptor-more-opts').click();
cy.get('.q-menu > .q-list > :nth-child(1) > .q-item__section').click();
cy.selectDescriptorOption();
cy.dataCy('regularizeStockInput').type('10');
cy.dataCy('Warehouse_select').type('Warehouse One{enter}');
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
});
});

View File

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

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" />
describe('Item type', () => {
describe('Item type', { testIsolation: true }, () => {
const workerError = 'employeeNick';
const worker = 'buyerNick';
const category = 'Artificial';
const type = 'Flower';
beforeEach(() => {
cy.login('developer');
cy.login('buyer');
cy.visit(`/#/item/item-type`);
});
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('nameInput').type('Alstroemeria');
cy.dataCy('vnWorkerSelect').type(workerError);
cy.get('.q-menu .q-item').contains(workerError).click();
cy.dataCy('itemCategorySelect').type(category);
cy.get('.q-menu .q-item').contains(category).click();
cy.selectOption('[data-cy="vnWorkerSelect"]', workerError);
cy.selectOption('[data-cy="itemCategorySelect"]', category);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('An item type with the same code already exists');
});
@ -26,10 +24,8 @@ describe('Item type', () => {
cy.dataCy('vnTableCreateBtn').click();
cy.dataCy('codeInput').type('LIL');
cy.dataCy('nameInput').type('Lilium');
cy.dataCy('vnWorkerSelect').type(worker);
cy.get('.q-menu .q-item').contains(worker).click();
cy.dataCy('itemCategorySelect').type(type);
cy.get('.q-menu .q-item').contains(type).click();
cy.selectOption('[data-cy="vnWorkerSelect"]', worker);
cy.selectOption('[data-cy="itemCategorySelect"]', type);
cy.dataCy('FormModelPopup_save').click();
cy.checkNotification('Data created');
});

View File

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

View File

@ -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', () => {
cy.checkRedirectionFromPopUp({
selectorToClick: selectors.firstRowWorkerLink,

View File

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

View File

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

View File

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

View File

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

View File

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