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

This commit is contained in:
Carlos Satorres 2025-02-11 15:26:23 +00:00
commit a0b7f0083d
60 changed files with 2636 additions and 231 deletions

View File

@ -14,8 +14,8 @@ export default defineConfig({
downloadsFolder: 'test/cypress/downloads',
video: false,
specPattern: 'test/cypress/integration/**/*.spec.js',
experimentalRunAllSpecs: true,
watchForFileChanges: true,
experimentalRunAllSpecs: false,
watchForFileChanges: false,
reporter: 'cypress-mochawesome-reporter',
reporterOptions: {
charts: true,

View File

@ -30,7 +30,6 @@ export default configure(function (/* ctx */) {
// --> boot files are part of "main.js"
// https://v2.quasar.dev/quasar-cli/boot-files
boot: ['i18n', 'axios', 'vnDate', 'validations', 'quasar', 'quasar.defaults'],
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-js#css
css: ['app.scss'],

View File

@ -328,7 +328,6 @@ en:
active: Is active
visible: Is visible
floramondo: Is floramondo
salesPersonFk: Buyer
categoryFk: Category
es:
@ -339,7 +338,6 @@ es:
active: Activo
visible: Visible
floramondo: Floramondo
salesPersonFk: Comprador
categoryFk: Categoría
Plant: Planta natural
Flower: Flor fresca

View File

@ -9,6 +9,7 @@ import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const $props = defineProps({
invoiceOutData: {
@ -131,15 +132,11 @@ const refund = async () => {
:required="true"
/> </VnRow
><VnRow>
<div>
<QCheckbox
:label="t('Inherit warehouse')"
v-model="invoiceParams.inheritWarehouse"
/>
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('Inherit warehouse tooltip') }}</QTooltip>
</QIcon>
</div>
<VnCheckbox
v-model="invoiceParams.inheritWarehouse"
:label="t('Inherit warehouse')"
:info="t('Inherit warehouse tooltip')"
/>
</VnRow>
</template>
</FormPopup>

View File

@ -10,6 +10,7 @@ import VnSelect from 'components/common/VnSelect.vue';
import FormPopup from './FormPopup.vue';
import axios from 'axios';
import useNotify from 'src/composables/useNotify.js';
import VnCheckbox from './common/VnCheckbox.vue';
const $props = defineProps({
invoiceOutData: {
@ -186,15 +187,11 @@ const makeInvoice = async () => {
/>
</VnRow>
<VnRow>
<div>
<QCheckbox
:label="t('Bill destination client')"
v-model="checked"
/>
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>{{ t('transferInvoiceInfo') }}</QTooltip>
</QIcon>
</div>
<VnCheckbox
v-model="checked"
:label="t('Bill destination client')"
:info="t('transferInvoiceInfo')"
/>
</VnRow>
</template>
</FormPopup>

View File

@ -368,6 +368,7 @@ function handleSelection({ evt, added, rows: selectedRows }, rows) {
<slot name="top-left"></slot>
</template>
<template #top-right v-if="!$props.withoutHeader">
<slot name="top-right"></slot>
<VnVisibleColumn
v-if="isTableMode"
v-model="splittedColumns.columns"

View File

@ -0,0 +1,33 @@
<script setup>
import { defineModel } from 'vue';
const modelValue = defineModel({ type: Boolean, default: false });
const $props = defineProps({
info: {
type: String,
default: null,
},
});
</script>
<template>
<div>
<QCheckbox
v-bind="$attrs"
v-on="$attrs"
v-model="modelValue"
/>
<QIcon
v-if="info"
v-bind="$attrs"
class="cursor-info q-ml-sm"
name="info"
size="sm"
>
<QTooltip>
{{ info }}
</QTooltip>
</QIcon>
</div>
</template>

View File

@ -0,0 +1,38 @@
<script setup>
import { ref } from 'vue';
defineProps({
label: {
type: String,
default: '',
},
icon: {
type: String,
required: true,
default: null,
},
color: {
type: String,
default: 'primary',
},
tooltip: {
type: String,
default: null,
},
});
const popupProxyRef = ref(null);
</script>
<template>
<QBtn :color="$props.color" :icon="$props.icon" :label="$t($props.label)">
<template #default>
<slot name="extraIcon"></slot>
<QPopupProxy ref="popupProxyRef" style="max-width: none">
<QCard>
<slot :popup="popupProxyRef"></slot>
</QCard>
</QPopupProxy>
<QTooltip>{{ $t($props.tooltip) }}</QTooltip>
</template>
</QBtn>
</template>

View File

@ -15,6 +15,7 @@ const model = defineModel({ type: [String, Number, Object] });
:fields="['id', 'name', 'nickname', 'nif']"
:filter-options="['id', 'name', 'nickname', 'nif']"
sort-by="name ASC"
data-cy="vnSupplierSelect"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">

View File

@ -57,7 +57,7 @@ defineExpose({ getData });
onBeforeMount(async () => {
arrayData = useArrayData($props.dataKey, {
url: $props.url,
filter: $props.filter,
userFilter: $props.filter,
skip: 0,
oneRecord: true,
});

View File

@ -0,0 +1,41 @@
<script setup>
import { toPercentage } from 'filters/index';
import { computed } from 'vue';
const props = defineProps({
value: {
type: Number,
required: true,
},
});
const valueClass = computed(() =>
props.value === 0 ? 'neutral' : props.value > 0 ? 'positive' : 'negative',
);
const iconName = computed(() =>
props.value === 0 ? 'equal' : props.value > 0 ? 'arrow_upward' : 'arrow_downward',
);
const formattedValue = computed(() => props.value);
</script>
<template>
<span :class="valueClass">
<QIcon :name="iconName" size="sm" class="value-icon" />
{{ toPercentage(formattedValue) }}
</span>
</template>
<style lang="scss" scoped>
.positive {
color: $secondary;
}
.negative {
color: $negative;
}
.neutral {
color: $primary;
}
.value-icon {
margin-right: 4px;
}
</style>

View File

@ -27,6 +27,15 @@ export function useRole() {
return false;
}
function likeAny(roles) {
const roleStore = state.getRoles();
for (const role of roles) {
if (!roleStore.value.findIndex((rs) => rs.startsWith(role)) !== -1)
return true;
}
return false;
}
function isEmployee() {
return hasAny(['employee']);
}
@ -35,6 +44,7 @@ export function useRole() {
isEmployee,
fetch,
hasAny,
likeAny,
state,
};
}

View File

@ -230,10 +230,12 @@ input::-webkit-inner-spin-button {
max-width: 100%;
}
.q-table__container {
/* ===== Scrollbar CSS ===== /
/ Firefox */
.remove-bg {
filter: brightness(1.1);
mix-blend-mode: multiply;
}
.q-table__container {
* {
scrollbar-width: auto;
scrollbar-color: var(--vn-label-color) transparent;

View File

@ -13,7 +13,7 @@
// Tip: Use the "Theme Builder" on Quasar's documentation website.
// Tip: to add new colors https://quasar.dev/style/color-palette/#adding-your-own-colors
$primary: #ec8916;
$secondary: $primary;
$secondary: #89be34;
$positive: #c8e484;
$negative: #fb5252;
$info: #84d0e2;

View File

@ -167,6 +167,7 @@ globals:
workCenters: Work centers
modes: Modes
zones: Zones
negative: Negative
zonesList: List
deliveryDays: Delivery days
upcomingDeliveries: Upcoming deliveries
@ -174,6 +175,7 @@ globals:
alias: Alias
aliasUsers: Users
subRoles: Subroles
myAccount: Mi cuenta
inheritedRoles: Inherited Roles
customers: Customers
customerCreate: New customer

View File

@ -36,6 +36,7 @@ globals:
clone: Clonar
confirm: Confirmar
assign: Asignar
replace: Sustituir
back: Volver
yes: Si
no: No
@ -48,6 +49,7 @@ globals:
rowRemoved: Fila eliminada
pleaseWait: Por favor espera...
noPinnedModules: No has fijado ningún módulo
split: Split
summary:
basicData: Datos básicos
daysOnward: Días adelante
@ -76,8 +78,10 @@ globals:
requiredField: Campo obligatorio
class: clase
type: Tipo
reason: motivo
reason: Motivo
removeSelection: Eliminar selección
noResults: Sin resultados
results: resultados
system: Sistema
notificationSent: Notificación enviada
warehouse: Almacén
@ -166,6 +170,7 @@ globals:
agency: Agencia
workCenters: Centros de trabajo
modes: Modos
negative: Tickets negativos
zones: Zonas
zonesList: Listado
deliveryDays: Días de entrega
@ -286,9 +291,9 @@ globals:
buyRequest: Peticiones de compra
wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo
tax: 'IVA'
botanical: 'Botánico'
barcode: 'Código de barras'
tax: IVA
botanical: Botánico
barcode: Código de barras
itemTypeCreate: Nueva familia
family: Familia
lastEntries: Últimas entradas
@ -410,6 +415,38 @@ ticket:
freightItemName: Nombre
packageItemName: Embalaje
longName: Descripción
pageTitles:
tickets: Tickets
list: Listado
ticketCreate: Nuevo ticket
summary: Resumen
basicData: Datos básicos
boxing: Encajado
sms: Sms
notes: Notas
sale: Lineas del pedido
dms: Gestión documental
negative: Tickets negativos
volume: Volumen
observation: Notas
ticketAdvance: Adelantar tickets
futureTickets: Tickets a futuro
expedition: Expedición
purchaseRequest: Petición de compra
weeklyTickets: Tickets programados
saleTracking: Líneas preparadas
services: Servicios
tracking: Estados
components: Componentes
pictures: Fotos
packages: Bultos
list:
nickname: Alias
state: Estado
shipped: Enviado
landed: Entregado
salesPerson: Comercial
total: Total
card:
customerId: ID cliente
customerCard: Ficha del cliente
@ -635,8 +672,8 @@ wagon:
volumeNotEmpty: El volumen no puede estar vacío
typeNotEmpty: El tipo no puede estar vacío
maxTrays: Has alcanzado el número máximo de bandejas
minHeightBetweenTrays: 'La distancia mínima entre bandejas es '
maxWagonHeight: 'La altura máxima del vagón es '
minHeightBetweenTrays: La distancia mínima entre bandejas es
maxWagonHeight: La altura máxima del vagón es
uncompleteTrays: Hay bandejas sin completar
params:
label: Etiqueta
@ -653,7 +690,6 @@ supplier:
tableVisibleColumns:
nif: NIF/CIF
account: Cuenta
summary:
responsible: Responsable
verified: Verificado
@ -784,7 +820,7 @@ components:
cardDescriptor:
mainList: Listado principal
summary: Resumen
moreOptions: 'Más opciones'
moreOptions: Más opciones
leftMenu:
addToPinned: Añadir a fijados
removeFromPinned: Eliminar de fijados

View File

@ -12,6 +12,7 @@ import VnInputPassword from 'src/components/common/VnInputPassword.vue';
import VnChangePassword from 'src/components/common/VnChangePassword.vue';
import { useQuasar } from 'quasar';
import { useRouter } from 'vue-router';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const $props = defineProps({
hasAccount: {
@ -121,18 +122,14 @@ onMounted(() => {
:promise="sync"
>
<template #customHTML>
{{ shouldSyncPassword }}
<QCheckbox
:label="t('account.card.actions.sync.checkbox')"
<VnCheckbox
v-model="shouldSyncPassword"
class="full-width"
:label="t('account.card.actions.sync.checkbox')"
:info="t('account.card.actions.sync.tooltip')"
clearable
clear-icon="close"
>
<QIcon style="padding-left: 10px" color="primary" name="info" size="sm">
<QTooltip>{{ t('account.card.actions.sync.tooltip') }}</QTooltip>
</QIcon></QCheckbox
>
color="primary"
/>
<VnInputPassword
v-if="shouldSyncPassword"
:label="t('login.password')"

View File

@ -117,7 +117,7 @@ const toCustomerAddressEdit = (addressId) => {
data-key="CustomerAddresses"
order="id DESC"
ref="vnPaginateRef"
:user-filter="addressFilter"
:filter="addressFilter"
:url="`Clients/${route.params.id}/addresses`"
/>
<div class="full-width flex justify-center">
@ -189,11 +189,11 @@ const toCustomerAddressEdit = (addressId) => {
<QSeparator
class="q-mx-lg"
v-if="item.observations.length"
v-if="item?.observations?.length"
vertical
/>
<div v-if="item.observations.length">
<div v-if="item?.observations?.length">
<div
:key="obIndex"
class="flex q-mb-sm"

View File

@ -61,6 +61,23 @@ const columns = computed(() => [
columnFilter: false,
cardVisible: true,
},
{
align: 'left',
name: 'buyerId',
label: t('customer.params.buyerId'),
component: 'select',
attrs: {
url: 'TicketRequests/getItemTypeWorker',
optionLabel: 'nickname',
optionValue: 'id',
fields: ['id', 'nickname'],
sortBy: ['nickname ASC'],
optionFilter: 'firstName',
},
cardVisible: false,
visible: false,
},
{
name: 'description',
align: 'left',
@ -74,6 +91,7 @@ const columns = computed(() => [
name: 'quantity',
label: t('globals.quantity'),
cardVisible: true,
visible: true,
columnFilter: {
inWhere: true,
},
@ -138,11 +156,11 @@ const updateDateParams = (value, params) => {
const campaign = campaignList.value.find((c) => c.id === value);
if (!campaign) return;
const { dated, previousDays, scopeDays } = campaign;
const _date = new Date(dated);
const [from, to] = dateRange(_date);
params.from = new Date(from.setDate(from.getDate() - previousDays)).toISOString();
params.to = new Date(to.setDate(to.getDate() + scopeDays)).toISOString();
const { dated, scopeDays } = campaign;
const from = new Date(dated);
from.setDate(from.getDate() - scopeDays);
params.from = from;
params.to = dated;
return params;
};
</script>
@ -205,24 +223,57 @@ const updateDateParams = (value, params) => {
<template #moreFilterPanel="{ params }">
<div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl">
<VnSelect
v-model="params.campaign"
:options="campaignList"
:label="t('globals.campaign')"
:filled="true"
class="q-px-sm q-pt-none fit"
url="ItemTypes"
v-model="params.typeId"
:label="t('item.list.typeName')"
:fields="['id', 'name', 'categoryFk']"
:include="'category'"
:sortBy="'name ASC'"
dense
option-label="code"
@update:model-value="(data) => updateDateParams(data, params)"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>
{{ scope.opt?.code }}
{{
new Date(scope.opt?.dated).getFullYear()
}}</QItemLabel
>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>{{
scope.opt?.category?.name
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
<VnSelect
:filled="true"
class="q-px-sm q-pt-none fit"
url="ItemCategories"
v-model="params.categoryId"
:label="t('item.list.category')"
:fields="['id', 'name']"
:sortBy="'name ASC'"
dense
@update:model-value="(data) => updateDateParams(data, params)"
/>
<VnSelect
v-model="params.campaign"
:options="campaignList"
:label="t('globals.campaign')"
:filled="true"
class="q-px-sm q-pt-none fit"
:option-label="(opt) => t(opt.code)"
:fields="['id', 'code', 'dated', 'scopeDays']"
@update:model-value="(data) => updateDateParams(data, params)"
dense
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel> {{ t(scope.opt?.code) }} </QItemLabel>
<QItemLabel caption>
{{ new Date(scope.opt?.dated).getFullYear() }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
@ -247,7 +298,19 @@ const updateDateParams = (value, params) => {
</template>
<i18n>
en:
valentinesDay: Valentine's Day
mothersDay: Mother's Day
allSaints: All Saints' Day
es:
Enter a new search: Introduce una nueva búsqueda
Group by items: Agrupar por artículos
valentinesDay: Día de San Valentín
mothersDay: Día de la Madre
allSaints: Día de Todos los Santos
Campaign consumption: Consumo campaña
Campaign: Campaña
From: Desde
To: Hasta
</i18n>

View File

@ -110,7 +110,21 @@ const debtWarning = computed(() => {
>
<QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip>
</QIcon>
<QIcon v-if="entity.isFreezed" name="vn:frozen" size="xs" color="primary">
<QIcon
v-if="entity?.substitutionAllowed"
name="help"
size="xs"
color="primary"
>
<QTooltip>{{ t('Allowed substitution') }}</QTooltip>
</QIcon>
<QIcon
v-if="customer?.isFreezed"
name="vn:frozen"
size="xs"
color="primary"
>
<QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip>
</QIcon>
<QIcon

View File

@ -61,6 +61,16 @@ const openCreateForm = (type) => {
.join('&');
useOpenURL(`/#/${type}/list?${params}`);
};
const updateSubstitutionAllowed = async () => {
try {
await axios.patch(`Clients/${route.params.id}`, {
substitutionAllowed: !$props.customer.substitutionAllowed,
});
notify('globals.notificationSent', 'positive');
} catch (error) {
notify(error.message, 'positive');
}
};
</script>
<template>
@ -69,6 +79,13 @@ const openCreateForm = (type) => {
{{ t('globals.pageTitles.createTicket') }}
</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection @click="updateSubstitutionAllowed()">{{
$props.customer.substitutionAllowed
? t('Disable substitution')
: t('Allow substitution')
}}</QItemSection>
</QItem>
<QItem v-ripple clickable>
<QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection>
</QItem>

View File

@ -9,6 +9,7 @@ import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const { t } = useI18n();
const route = useRoute();
@ -110,14 +111,11 @@ function handleLocation(data, location) {
</VnRow>
<VnRow>
<QCheckbox :label="t('Has to invoice')" v-model="data.hasToInvoice" />
<div>
<QCheckbox :label="t('globals.isVies')" v-model="data.isVies" />
<QIcon name="info" class="cursor-info q-ml-sm" size="sm">
<QTooltip>
{{ t('whenActivatingIt') }}
</QTooltip>
</QIcon>
</div>
<VnCheckbox
v-model="data.isVies"
:label="t('globals.isVies')"
:info="t('whenActivatingIt')"
/>
</VnRow>
<VnRow>
@ -129,17 +127,11 @@ function handleLocation(data, location) {
</VnRow>
<VnRow>
<div>
<QCheckbox
:label="t('Is equalizated')"
v-model="data.isEqualizated"
/>
<QIcon class="cursor-info q-ml-sm" name="info" size="sm">
<QTooltip>
{{ t('inOrderToInvoice') }}
</QTooltip>
</QIcon>
</div>
<VnCheckbox
v-model="data.isEqualizated"
:label="t('Is equalizated')"
:info="t('inOrderToInvoice')"
/>
<QCheckbox :label="t('Daily invoice')" v-model="data.hasDailyInvoice" />
</VnRow>

View File

@ -1,4 +1,3 @@
<script setup>
import { useI18n } from 'vue-i18n';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
@ -52,11 +51,7 @@ const exprBuilder = (param, value) => {
</QItem>
<QItem class="q-mb-sm">
<QItemSection>
<VnInput
:label="t('globals.name')"
v-model="params.name"
is-outlined
/>
<VnInput :label="t('Name')" v-model="params.name" is-outlined />
</QItemSection>
</QItem>
<QItem class="q-mb-sm">

View File

@ -419,7 +419,7 @@ function handleLocation(data, location) {
<VnTable
ref="tableRef"
:data-key="dataKey"
url="Clients/filter"
url="Clients/extendedListFilter"
:create="{
urlCreate: 'Clients/createWithUser',
title: t('globals.pageTitles.customerCreate'),

View File

@ -107,6 +107,9 @@ customer:
defaulterSinced: Defaulted Since
hasRecovery: Has Recovery
socialName: Social name
typeId: Type
buyerId: Buyer
categoryId: Category
city: City
phone: Phone
postcode: Postcode

View File

@ -108,6 +108,9 @@ customer:
hasRecovery: Tiene recobro
socialName: Razón social
campaign: Campaña
typeId: Familia
buyerId: Comprador
categoryId: Reino
city: Ciudad
phone: Teléfono
postcode: Código postal

View File

@ -54,8 +54,8 @@ const transferEntry = async () => {
<i18n>
en:
transferEntryDialog: The entries will be transferred to the next day
transferEntry: Transfer Entry
transferEntry: Partial delay
es:
transferEntryDialog: Se van a transferir las compras al dia siguiente
transferEntry: Transferir Entrada
transferEntry: Retraso parcial
</i18n>

View File

@ -149,6 +149,7 @@ function deleteFile(dmsFk) {
option-value="id"
option-label="id"
:filter-options="['id', 'name']"
data-cy="UnDeductibleVatSelect"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">

View File

@ -97,12 +97,19 @@ const columns = computed(() => [
},
{
align: 'left',
name: 'companyCode',
name: 'companyFk',
label: t('globals.company'),
cardVisible: true,
component: 'select',
attrs: { url: 'Companies', optionLabel: 'code', optionValue: 'id' },
columnField: { component: null },
attrs: {
url: 'Companies',
optionLabel: 'code',
optionValue: 'id',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.companyCode),
},
{
align: 'left',

View File

@ -11,6 +11,7 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import FilterItemForm from 'src/components/FilterItemForm.vue';
import CreateIntrastatForm from './CreateIntrastatForm.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const route = useRoute();
const { t } = useI18n();
@ -208,30 +209,20 @@ const onIntrastatCreated = (response, formData) => {
/>
</VnRow>
<VnRow class="row q-gutter-md q-mb-md">
<div>
<QCheckbox
v-model="data.isFragile"
:label="t('item.basicData.isFragile')"
class="q-mr-sm"
/>
<QIcon name="info" class="cursor-pointer" size="xs">
<QTooltip max-width="300px">
{{ t('item.basicData.isFragileTooltip') }}
</QTooltip>
</QIcon>
</div>
<div>
<QCheckbox
v-model="data.isPhotoRequested"
:label="t('item.basicData.isPhotoRequested')"
class="q-mr-sm"
/>
<QIcon name="info" class="cursor-pointer" size="xs">
<QTooltip>
{{ t('item.basicData.isPhotoRequestedTooltip') }}
</QTooltip>
</QIcon>
</div>
<VnCheckbox
v-model="data.isFragile"
:label="t('item.basicData.isFragile')"
:info="t('item.basicData.isFragileTooltip')"
class="q-mr-sm"
size="xs"
/>
<VnCheckbox
v-model="data.isPhotoRequested"
:label="t('item.basicData.isPhotoRequested')"
:info="t('item.basicData.isPhotoRequestedTooltip')"
class="q-mr-sm"
size="xs"
/>
</VnRow>
<VnRow>
<VnInput

View File

@ -7,8 +7,8 @@ import FetchData from 'components/FetchData.vue';
import FormModel from 'components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CreateGenusForm from './CreateGenusForm.vue';
import CreateSpecieForm from './CreateSpecieForm.vue';
import CreateGenusForm from '../components/CreateGenusForm.vue';
import CreateSpecieForm from '../components/CreateSpecieForm.vue';
const route = useRoute();
const { t } = useI18n();

View File

@ -0,0 +1,332 @@
<script setup>
import { ref, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import { toCurrency } from 'filters/index';
import VnStockValueDisplay from 'src/components/ui/VnStockValueDisplay.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import axios from 'axios';
import notifyResults from 'src/utils/notifyResults';
import FetchData from 'components/FetchData.vue';
const MATCH = 'match';
const { t } = useI18n();
const $props = defineProps({
itemLack: {
type: Object,
required: true,
default: () => {},
},
replaceAction: {
type: Boolean,
required: false,
default: false,
},
sales: {
type: Array,
required: false,
default: () => [],
},
});
const proposalSelected = ref([]);
const ticketConfig = ref({});
const proposalTableRef = ref(null);
const sale = computed(() => $props.sales[0]);
const saleFk = computed(() => sale.value.saleFk);
const filter = computed(() => ({
itemFk: $props.itemLack.itemFk,
sales: saleFk.value,
}));
const defaultColumnAttrs = {
align: 'center',
sortable: false,
};
const emit = defineEmits(['onDialogClosed', 'itemReplaced']);
const conditionalValuePrice = (price) =>
price > 1 + ticketConfig.value.lackAlertPrice / 100 ? 'match' : 'not-match';
const columns = computed(() => [
{
...defaultColumnAttrs,
label: t('proposal.available'),
name: 'available',
field: 'available',
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
columnClass: 'shrink',
},
{
...defaultColumnAttrs,
label: t('proposal.counter'),
name: 'counter',
field: 'counter',
columnClass: 'shrink',
style: 'max-width: 75px',
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
},
{
align: 'left',
sortable: true,
label: t('proposal.longName'),
name: 'longName',
field: 'longName',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('item.list.color'),
name: 'tag5',
field: 'value5',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('item.list.stems'),
name: 'tag6',
field: 'value6',
columnClass: 'expand',
},
{
align: 'left',
sortable: true,
label: t('item.list.producer'),
name: 'tag7',
field: 'value7',
columnClass: 'expand',
},
{
...defaultColumnAttrs,
label: t('proposal.price2'),
name: 'price2',
style: 'max-width: 75px',
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
},
{
...defaultColumnAttrs,
label: t('proposal.minQuantity'),
name: 'minQuantity',
field: 'minQuantity',
style: 'max-width: 75px',
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
},
{
...defaultColumnAttrs,
label: t('proposal.located'),
name: 'located',
field: 'located',
},
{
align: 'right',
label: '',
name: 'tableActions',
actions: [
{
title: t('Replace'),
icon: 'change_circle',
show: (row) => isSelectionAvailable(row),
action: change,
isPrimary: true,
},
],
},
]);
function extractMatchValues(obj) {
return Object.keys(obj)
.filter((key) => key.startsWith(MATCH))
.map((key) => parseInt(key.replace(MATCH, ''), 10));
}
const gradientStyle = (value) => {
let color = 'white';
const perc = parseFloat(value);
switch (true) {
case perc >= 0 && perc < 33:
color = 'primary';
break;
case perc >= 33 && perc < 66:
color = 'warning';
break;
default:
color = 'secondary';
break;
}
return color;
};
const statusConditionalValue = (row) => {
const matches = extractMatchValues(row);
const value = matches.reduce((acc, i) => acc + row[`${MATCH}${i}`], 0);
return 100 * (value / matches.length);
};
const isSelectionAvailable = (itemProposal) => {
const { price2 } = itemProposal;
const salePrice = sale.value.price;
const byPrice = (100 * price2) / salePrice > ticketConfig.value.lackAlertPrice;
if (byPrice) {
return byPrice;
}
const byQuantity =
(100 * itemProposal.available) / Math.abs($props.itemLack.lack) <
ticketConfig.value.lackAlertPrice;
return byQuantity;
};
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 handleTicketConfig(data) {
ticketConfig.value = data[0];
}
</script>
<template>
<FetchData
url="TicketConfigs"
:filter="{ fields: ['lackAlertPrice'] }"
@on-fetch="handleTicketConfig"
auto-load
/>
<VnTable
v-if="ticketConfig"
auto-load
data-cy="proposalTable"
ref="proposalTableRef"
data-key="ItemsGetSimilar"
url="Items/getSimilar"
:user-filter="filter"
:columns="columns"
class="full-width q-mt-md"
row-key="id"
:row-click="change"
:is-editable="false"
:right-search="false"
:without-header="true"
:disable-option="{ card: true, table: true }"
>
<template #column-longName="{ row }">
<QTd
class="flex"
style="max-width: 100%; flex-shrink: 50px; flex-wrap: nowrap"
>
<div
class="middle full-width"
:class="[`proposal-${gradientStyle(statusConditionalValue(row))}`]"
>
<QTooltip> {{ statusConditionalValue(row) }}% </QTooltip>
</div>
<div style="flex: 2 0 100%; align-content: center">
<div>
<span class="link">{{ row.longName }}</span>
<ItemDescriptorProxy :id="row.id" />
</div>
</div>
</QTd>
</template>
<template #column-tag5="{ row }">
<span :class="{ match: !row.match5 }">{{ row.value5 }}</span>
</template>
<template #column-tag6="{ row }">
<span :class="{ match: !row.match6 }">{{ row.value6 }}</span>
</template>
<template #column-tag7="{ row }">
<span :class="{ match: !row.match7 }">{{ row.value7 }}</span>
</template>
<template #column-counter="{ row }">
<span
:class="{
match: row.counter === 1,
'not-match': row.counter !== 1,
}"
>{{ row.counter }}</span
>
</template>
<template #column-minQuantity="{ row }">
{{ row.minQuantity }}
</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)]">{{
toCurrency(row.price2)
}}</span>
</div>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
@import 'src/css/quasar.variables.scss';
.middle {
float: left;
margin-right: 2px;
flex: 2 0 5px;
}
.match {
color: $negative;
}
.not-match {
color: inherit;
}
.proposal-warning {
background-color: $warning;
}
.proposal-secondary {
background-color: $secondary;
}
.proposal-primary {
background-color: $primary;
}
.text {
margin: 0.05rem;
padding: 1px;
border: 1px solid var(--vn-label-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: smaller;
}
</style>

View File

@ -0,0 +1,56 @@
<script setup>
import ItemProposal from './ItemProposal.vue';
import { useDialogPluginComponent } from 'quasar';
const $props = defineProps({
itemLack: {
type: Object,
required: true,
default: () => {},
},
replaceAction: {
type: Boolean,
required: false,
default: false,
},
sales: {
type: Array,
required: false,
default: () => [],
},
});
const { dialogRef } = useDialogPluginComponent();
const emit = defineEmits([
'onDialogClosed',
'itemReplaced',
...useDialogPluginComponent.emits,
]);
defineExpose({ show: () => dialogRef.value.show(), hide: () => 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('Item proposal') }}</span>
<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>
</QCard>
</QDialog>
</template>
<style lang="scss" scoped>
.dialog-width {
max-width: $width-lg;
}
</style>

View File

@ -130,6 +130,7 @@ item:
origin: Orig.
userName: Buyer
weight: Weight
color: Color
weightByPiece: Weight/stem
stemMultiplier: Multiplier
producer: Producer
@ -215,4 +216,24 @@ item:
specie: Specie
search: 'Search item'
searchInfo: 'You can search by id'
regularizeStock: Regularize stock
regularizeStock: Regularize stock
itemProposal: Items proposal
proposal:
difference: Difference
title: Items proposal
itemFk: Item
longName: Name
subName: Producer
value5: value5
value6: value6
value7: value7
value8: value8
available: Available
minQuantity: minQuantity
price2: Price
located: Located
counter: Counter
groupingPrice: Grouping Price
itemOldPrice: itemOld Price
status: State
quantityToReplace: Quanity to replace

View File

@ -135,6 +135,7 @@ item:
size: Medida
origin: Orig.
weight: Peso
color: Color
weightByPiece: Peso/tallo
userName: Comprador
stemMultiplier: Multiplicador
@ -220,5 +221,30 @@ item:
achieved: 'Conseguido'
concept: 'Concepto'
state: 'Estado'
search: 'Buscar artículo'
searchInfo: 'Puedes buscar por id'
itemProposal: Artículos similares
proposal:
substitutionAvailable: Sustitución disponible
notSubstitutionAvailableByPrice: Sustitución no disponible, 30% de diferencia por precio o cantidad
compatibility: Compatibilidad
title: Items de sustitución para los tickets seleccionados
itemFk: Item
longName: Nombre
subName: Productor
value5: value5
value6: value6
value7: value7
value8: value8
available: Disponible
minQuantity: Min. cantidad
price2: Precio
located: Ubicado
counter: Contador
difference: Diferencial
groupingPrice: Precio Grouping
itemOldPrice: Precio itemOld
status: Estado
quantityToReplace: Cantidad a reemplazar
replace: Sustituir
replaceAndConfirm: Sustituir y confirmar precio
search: 'Buscar artículo'
searchInfo: 'Puedes buscar por id'

View File

@ -238,7 +238,7 @@ watch(
lineFilter.value.where.orderFk = router.currentRoute.value.params.id;
tableLinesRef.value.reload();
}
},
);
</script>

View File

@ -10,6 +10,7 @@ import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import VnLocation from 'src/components/common/VnLocation.vue';
import VnAccountNumber from 'src/components/common/VnAccountNumber.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const route = useRoute();
const { t } = useI18n();
@ -182,18 +183,11 @@ function handleLocation(data, location) {
v-model="data.isTrucker"
:label="t('supplier.fiscalData.isTrucker')"
/>
<div class="row items-center">
<QCheckbox v-model="data.isVies" :label="t('globals.isVies')" />
<QIcon name="info" size="xs" class="cursor-pointer q-ml-sm">
<QTooltip>
{{
t(
'When activating it, do not enter the country code in the ID field.'
)
}}
</QTooltip>
</QIcon>
</div>
<VnCheckbox
v-model="data.isVies"
:label="t('globals.isVies')"
:info="t('whenActivatingIt')"
/>
</div>
</VnRow>
</template>
@ -201,6 +195,8 @@ function handleLocation(data, location) {
</template>
<i18n>
en:
whenActivatingIt: When activating it, do not enter the country code in the ID field.
es:
When activating it, do not enter the country code in the ID field.: Al activarlo, no informar el código del país en el campo nif
whenActivatingIt: Al activarlo, no informar el código del país en el campo nif.
</i18n>

View File

@ -9,6 +9,7 @@ import FetchData from 'components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore';
import { toCurrency } from 'filters/index';
import { useRole } from 'src/composables/useRole';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const haveNegatives = defineModel('have-negatives', { type: Boolean, required: true });
const formData = defineModel({ type: Object, required: true });
@ -182,22 +183,19 @@ onMounted(async () => {
</QCard>
<QCard
v-if="haveNegatives"
class="q-pa-md q-mb-md q-ma-md color-vn-text"
class="q-pa-xs q-mb-md q-ma-md color-vn-text"
bordered
flat
style="border-color: black"
>
<QCardSection horizontal class="flex row items-center">
<QCheckbox
:label="t('basicData.withoutNegatives')"
<VnCheckbox
v-model="formData.withoutNegatives"
:label="t('basicData.withoutNegatives')"
:info="t('basicData.withoutNegativesInfo')"
:toggle-indeterminate="false"
size="xs"
/>
<QIcon name="info" size="xs" class="q-ml-sm">
<QTooltip max-width="350px">
{{ t('basicData.withoutNegativesInfo') }}
</QTooltip>
</QIcon>
</QCardSection>
</QCard>
</QDrawer>

View File

@ -14,7 +14,7 @@ import VnImg from 'src/components/ui/VnImg.vue';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import TicketSaleMoreActions from './TicketSaleMoreActions.vue';
import TicketTransfer from './TicketTransfer.vue';
import TicketTransferProxy from './TicketTransferProxy.vue';
import { toCurrency, toPercentage } from 'src/filters';
import { useArrayData } from 'composables/useArrayData';
@ -609,8 +609,9 @@ watch(
@click="setTransferParams()"
data-cy="ticketSaleTransferBtn"
>
<QTooltip>{{ t('Transfer lines') }}</QTooltip>
<TicketTransfer
<QTooltip>{{ t('ticketSale.transferLines') }}</QTooltip>
<TicketTransferProxy
class="full-width"
:transfer="transfer"
:ticket="store.data"
@refresh-data="resetChanges()"

View File

@ -0,0 +1,37 @@
<script setup>
import { ref } from 'vue';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import split from './components/split';
const emit = defineEmits(['ticketTransfered']);
const $props = defineProps({
ticket: {
type: [Array, Object],
default: () => {},
},
});
const splitDate = ref(Date.vnNew());
const splitSelectedRows = async () => {
const tickets = Array.isArray($props.ticket) ? $props.ticket : [$props.ticket];
await split(tickets, splitDate.value);
emit('ticketTransfered', tickets);
};
</script>
<template>
<VnInputDate class="q-mr-sm" :label="$t('New date')" v-model="splitDate" clearable />
<QBtn class="q-mr-sm" color="primary" label="Split" @click="splitSelectedRows"></QBtn>
</template>
<style lang="scss">
.q-table__bottom.row.items-center.q-table__bottom--nodata {
border-top: none;
}
</style>
<i18n>
es:
Sales to transfer: Líneas a transferir
Destination ticket: Ticket destinatario
</i18n>

View File

@ -1,11 +1,11 @@
<script setup>
import { ref, computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n';
import VnInput from 'src/components/common/VnInput.vue';
import TicketTransferForm from './TicketTransferForm.vue';
import { toDateFormat } from 'src/filters/date.js';
const emit = defineEmits(['ticketTransfered']);
const $props = defineProps({
mana: {
@ -21,16 +21,15 @@ const $props = defineProps({
default: () => {},
},
ticket: {
type: Object,
type: [Array, Object],
default: () => {},
},
});
onMounted(() => (_transfer.value = $props.transfer));
const { t } = useI18n();
const QPopupProxyRef = ref(null);
const transferFormRef = ref(null);
const _transfer = ref();
const transferLinesColumns = computed(() => [
{
label: t('ticketList.id'),
@ -86,76 +85,74 @@ const handleRowClick = (row) => {
transferFormRef.value.transferSales(ticketId);
}
};
onMounted(() => (_transfer.value = $props.transfer));
</script>
<template>
<QPopupProxy ref="QPopupProxyRef" data-cy="ticketTransferPopup">
<QCard class="q-px-md" style="display: flex; width: 80vw">
<QTable
:rows="transfer.sales"
:columns="transferLinesColumns"
:title="t('Sales to transfer')"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-quantity="{ row }">
<QTd @click.stop>
<VnInput
v-model.number="row.quantity"
:clearable="false"
style="max-width: 60px"
/>
</QTd>
</template>
</QTable>
<QSeparator vertical spaced />
<QTable
v-if="transfer.lastActiveTickets"
:rows="transfer.lastActiveTickets"
:columns="destinationTicketColumns"
:title="t('Destination ticket')"
row-key="id"
class="full-width q-mt-md"
@row-click="(_, row) => handleRowClick(row)"
>
<template #body-cell-address="{ row }">
<QTd @click.stop>
<span>
{{ row.nickname }}
{{ row.name }}
{{ row.street }}
{{ row.postalCode }}
{{ row.city }}
</span>
<QTooltip>
{{ row.nickname }}
{{ row.name }}
{{ row.street }}
{{ row.postalCode }}
{{ row.city }}
</QTooltip>
</QTd>
</template>
<QTable
:rows="transfer.sales"
:columns="transferLinesColumns"
:title="t('Sales to transfer')"
row-key="id"
:pagination="{ rowsPerPage: 0 }"
class="full-width q-mt-md"
:no-data-label="t('globals.noResults')"
>
<template #body-cell-quantity="{ row }">
<QTd @click.stop>
<VnInput
v-model.number="row.quantity"
:clearable="false"
style="max-width: 60px"
/>
</QTd>
</template>
</QTable>
<QSeparator vertical spaced />
<QTable
v-if="transfer.lastActiveTickets"
:rows="transfer.lastActiveTickets"
:columns="destinationTicketColumns"
:title="t('Destination ticket')"
row-key="id"
class="full-width q-mt-md"
@row-click="(_, row) => handleRowClick(row)"
:no-data-label="t('globals.noResults')"
:pagination="{ rowsPerPage: 0 }"
>
<template #body-cell-address="{ row }">
<QTd @click.stop>
<span>
{{ row.nickname }}
{{ row.name }}
{{ row.street }}
{{ row.postalCode }}
{{ row.city }}
</span>
<QTooltip>
{{ row.nickname }}
{{ row.name }}
{{ row.street }}
{{ row.postalCode }}
{{ row.city }}
</QTooltip>
</QTd>
</template>
<template #no-data>
<TicketTransferForm ref="transferFormRef" v-bind="$props" />
</template>
<template #bottom>
<TicketTransferForm ref="transferFormRef" v-bind="$props" />
</template>
</QTable>
</QCard>
</QPopupProxy>
<template #no-data>
<TicketTransferForm ref="transferFormRef" v-bind="$props" />
</template>
<template #bottom>
<TicketTransferForm ref="transferFormRef" v-bind="$props" />
</template>
</QTable>
</template>
<style lang="scss">
.q-table__bottom.row.items-center.q-table__bottom--nodata {
border-top: none;
}
</style>
<i18n>
es:
Sales to transfer: Líneas a transferir
Destination ticket: Ticket destinatario
Transfer to ticket: Transferir a ticket
New ticket: Nuevo ticket
</i18n>

View File

@ -0,0 +1,54 @@
<script setup>
import { ref } from 'vue';
import TicketTransfer from './TicketTransfer.vue';
import Split from './TicketSplit.vue';
const emit = defineEmits(['ticketTransfered']);
const $props = defineProps({
mana: {
type: Number,
default: null,
},
newPrice: {
type: Number,
default: 0,
},
transfer: {
type: Object,
default: () => {},
},
ticket: {
type: [Array, Object],
default: () => {},
},
split: {
type: Boolean,
default: false,
},
});
const popupProxyRef = ref(null);
const splitRef = ref(null);
const transferRef = ref(null);
</script>
<template>
<QPopupProxy ref="popupProxyRef" data-cy="ticketTransferPopup">
<div class="flex row items-center q-ma-lg" v-if="$props.split">
<Split
ref="splitRef"
@splitSelectedRows="splitSelectedRows"
:ticket="$props.ticket"
/>
</div>
<div v-else>
<TicketTransfer
ref="transferRef"
:ticket="$props.ticket"
:sales="$props.sales"
:transfer="$props.transfer"
/>
</div>
</QPopupProxy>
</template>

View File

@ -0,0 +1,22 @@
import axios from 'axios';
import notifyResults from 'src/utils/notifyResults';
export default async function (data, date) {
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.id, sales: [item.saleFk], date });
}
return acc;
}, []);
const promises = reducedData.map((params) => axios.post(`Tickets/split`, params));
const results = await Promise.allSettled(promises);
notifyResults(results, 'ticketFk');
return results;
}

View File

@ -0,0 +1,198 @@
<script setup>
import { computed, onMounted, onUnmounted, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import ChangeQuantityDialog from './components/ChangeQuantityDialog.vue';
import ChangeStateDialog from './components/ChangeStateDialog.vue';
import ChangeItemDialog from './components/ChangeItemDialog.vue';
import TicketTransferProxy from '../Card/TicketTransferProxy.vue';
import FetchData from 'src/components/FetchData.vue';
import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState';
import { useRoute } from 'vue-router';
import TicketLackTable from './TicketLackTable.vue';
import VnPopupProxy from 'src/components/common/VnPopupProxy.vue';
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);
const changeStateDialogRef = ref(null);
const changeQuantityDialogRef = ref(null);
const showProposalDialog = ref(false);
const showChangeQuantityDialog = ref(false);
const selectedRows = ref([]);
const route = useRoute();
onMounted(() => {
stateStore.rightDrawer = false;
});
onUnmounted(() => {
stateStore.rightDrawer = true;
});
const entityId = computed(() => route.params.id);
const item = ref({});
const itemProposalSelected = ref(null);
const reload = async () => {
tableRef.value.tableRef.reload();
};
defineExpose({ reload });
const filter = computed(() => ({
scopeDays: route.query.days,
showType: true,
alertLevelCode: 'FREE',
date: Date.vnNew(),
warehouseFk: useState().getUser().value.warehouseFk,
}));
const itemProposalEvt = (data) => {
const { itemProposal } = data;
itemProposalSelected.value = itemProposal;
reload();
};
function onBuysFetched(data) {
Object.assign(item.value, data[0]);
}
const showItemProposal = () => {
quasar
.dialog({
component: ItemProposalProxy,
componentProps: {
itemLack: tableRef.value.itemLack,
replaceAction: true,
sales: selectedRows.value,
},
})
.onOk(itemProposalEvt);
};
</script>
<template>
<FetchData
url="States/editableStates"
@on-fetch="(data) => (editableStates = data)"
auto-load
/>
<FetchData
:url="`Items/${entityId}/getCard`"
:fields="['longName']"
@on-fetch="(data) => (item = data)"
auto-load
/>
<FetchData
:url="`Buys/latestBuysFilter`"
:fields="['longName']"
:filter="{ where: { 'i.id': entityId } }"
@on-fetch="onBuysFetched"
auto-load
/>
<TicketLackTable
ref="tableRef"
:filter="filter"
@update:selection="({ value }, _) => (selectedRows = value)"
>
<template #top-right>
<QBtnGroup push class="q-mr-lg" style="column-gap: 1px">
<QBtn
data-cy="transferLines"
color="primary"
:disable="!(selectedRows.length === 1)"
>
<template #default>
<QIcon name="vn:splitline" />
<QIcon name="vn:ticket" />
<QTooltip>{{ t('ticketSale.transferLines') }} </QTooltip>
<TicketTransferProxy
ref="transferFormRef"
split="true"
:ticket="selectedRows"
:transfer="{
sales: selectedRows,
lastActiveTickets: selectedRows.map((row) => row.id),
}"
@ticket-transfered="reload"
></TicketTransferProxy>
</template>
</QBtn>
<QBtn
color="primary"
@click="showProposalDialog = true"
:disable="selectedRows.length < 1"
data-cy="itemProposal"
>
<QIcon
name="import_export"
class="rotate-90"
@click="showItemProposal"
></QIcon>
<QTooltip bottom anchor="bottom right">
{{ t('itemProposal') }}
</QTooltip>
</QBtn>
<VnPopupProxy
data-cy="changeItem"
icon="sync"
:disable="selectedRows.length < 1"
:tooltip="t('negative.detail.modal.changeItem.title')"
>
<template #extraIcon> <QIcon name="vn:item" /> </template>
<template v-slot="{ popup }">
<ChangeItemDialog
ref="changeItemDialogRef"
@update-item="popup.hide()"
:selected-rows="selectedRows"
/></template>
</VnPopupProxy>
<VnPopupProxy
data-cy="changeState"
icon="sync"
:disable="selectedRows.length < 1"
:tooltip="t('negative.detail.modal.changeState.title')"
>
<template #extraIcon> <QIcon name="vn:eye" /> </template>
<template v-slot="{ popup }">
<ChangeStateDialog
ref="changeStateDialogRef"
@update-state="popup.hide()"
:selected-rows="selectedRows"
/></template>
</VnPopupProxy>
<VnPopupProxy
data-cy="changeQuantity"
icon="sync"
:disable="selectedRows.length < 1"
:tooltip="t('negative.detail.modal.changeQuantity.title')"
@click="showChangeQuantityDialog = true"
>
<template #extraIcon> <QIcon name="exposure" /> </template>
<template v-slot="{ popup }">
<ChangeQuantityDialog
ref="changeQuantityDialogRef"
@update-quantity="popup.hide()"
:selected-rows="selectedRows"
/></template>
</VnPopupProxy> </QBtnGroup
></template>
</TicketLackTable>
</template>
<style lang="scss" scoped>
.list-enter-active,
.list-leave-active {
transition: all 1s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
background-color: $primary;
}
.q-table.q-table__container > div:first-child {
border-radius: unset;
}
</style>

View File

@ -0,0 +1,175 @@
<script setup>
import { ref } from 'vue';
import { useI18n } from 'vue-i18n';
import FetchData from 'components/FetchData.vue';
import VnFilterPanel from 'src/components/ui/VnFilterPanel.vue';
import VnInput from 'src/components/common/VnInput.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n();
const props = defineProps({
dataKey: {
type: String,
required: true,
},
});
const to = Date.vnNew();
to.setDate(to.getDate() + 1);
const warehouses = ref();
const categoriesOptions = ref([]);
const itemTypesRef = ref(null);
const itemTypesOptions = ref([]);
const itemTypesFilter = {
fields: ['id', 'name', 'categoryFk'],
include: 'category',
order: 'name ASC',
where: {},
};
const onCategoryChange = async (categoryFk, search) => {
if (!categoryFk) {
itemTypesFilter.where.categoryFk = null;
delete itemTypesFilter.where.categoryFk;
} else {
itemTypesFilter.where.categoryFk = categoryFk;
}
search();
await itemTypesRef.value.fetch();
};
const emit = defineEmits(['set-user-params']);
const setUserParams = (params) => {
emit('set-user-params', params);
};
</script>
<template>
<FetchData url="Warehouses" @on-fetch="(data) => (warehouses = data)" auto-load />
<FetchData
url="ItemCategories"
:filter="{ fields: ['id', 'name'], order: 'name ASC' }"
@on-fetch="(data) => (categoriesOptions = data)"
auto-load
/>
<FetchData
ref="itemTypesRef"
url="ItemTypes"
:filter="itemTypesFilter"
@on-fetch="(data) => (itemTypesOptions = data)"
auto-load
/>
<VnFilterPanel
:data-key="props.dataKey"
:search-button="true"
@set-user-params="setUserParams"
>
<template #tags="{ tag, formatFn }">
<div class="q-gutter-x-xs">
<strong>{{ t(`negative.${tag.label}`) }}</strong>
<span>{{ formatFn(tag.value) }}</span>
</div>
</template>
<template #body="{ params, searchFn }">
<QList dense class="q-gutter-y-sm q-mt-sm">
<QItem>
<QItemSection>
<VnInput
v-model="params.days"
:label="t('negative.days')"
dense
is-outlined
type="number"
@update:model-value="
(value) => {
setUserParams(params);
}
"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.id"
:label="t('negative.id')"
dense
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.producer"
:label="t('negative.producer')"
dense
is-outlined
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnInput
v-model="params.origen"
:label="t('negative.origen')"
dense
is-outlined
/>
</QItemSection> </QItem
><QItem>
<QItemSection v-if="categoriesOptions">
<VnSelect
:label="t('negative.categoryFk')"
v-model="params.categoryFk"
@update:model-value="
($event) => onCategoryChange($event, searchFn)
"
:options="categoriesOptions"
option-value="id"
option-label="name"
hide-selected
dense
outlined
rounded
/> </QItemSection
><QItemSection v-else>
<QSkeleton class="full-width" type="QSelect" />
</QItemSection>
</QItem>
<QItem>
<QItemSection v-if="itemTypesOptions">
<VnSelect
:label="t('negative.type')"
v-model="params.typeFk"
@update:model-value="searchFn()"
:options="itemTypesOptions"
option-value="id"
option-label="name"
hide-selected
dense
outlined
rounded
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel>
<QItemLabel caption>{{
scope.opt?.category?.name
}}</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect> </QItemSection
><QItemSection v-else>
<QSkeleton class="full-width" type="QSelect" />
</QItemSection>
</QItem>
</QList>
</template>
</VnFilterPanel>
</template>

View File

@ -0,0 +1,227 @@
<script setup>
import { computed, ref, reactive } from 'vue';
import { useI18n } from 'vue-i18n';
import { useStateStore } from 'stores/useStateStore';
import VnTable from 'components/VnTable/VnTable.vue';
import { onBeforeMount } from 'vue';
import { dashIfEmpty, toDate, toHour } from 'src/filters';
import { useRouter } from 'vue-router';
import { useState } from 'src/composables/useState';
import { useRole } from 'src/composables/useRole';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import RightMenu from 'src/components/common/RightMenu.vue';
import TicketLackFilter from './TicketLackFilter.vue';
onBeforeMount(() => {
stateStore.$state.rightDrawer = true;
});
const router = useRouter();
const stateStore = useStateStore();
const { t } = useI18n();
const selectedRows = ref([]);
const tableRef = ref();
const filterParams = ref({});
const negativeParams = reactive({
days: useRole().likeAny('buyer') ? 2 : 0,
warehouseFk: useState().getUser().value.warehouseFk,
});
const redirectToCreateView = ({ itemFk }) => {
router.push({
name: 'NegativeDetail',
params: { id: itemFk },
query: { days: filterParams.value.days ?? negativeParams.days },
});
};
const columns = computed(() => [
{
name: 'date',
align: 'left',
label: t('negative.date'),
format: ({ timed }) => toDate(timed),
sortable: true,
cardVisible: true,
isId: true,
columnFilter: {
component: 'date',
},
},
{
columnClass: 'shrink',
name: 'timed',
align: 'left',
label: t('negative.timed'),
format: ({ timed }) => toHour(timed),
sortable: true,
cardVisible: true,
columnFilter: {
component: 'time',
},
},
{
name: 'itemFk',
align: 'left',
label: t('negative.id'),
format: ({ itemFk }) => itemFk,
sortable: true,
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
},
{
name: 'longName',
align: 'left',
label: t('negative.longName'),
field: ({ longName }) => longName,
sortable: true,
headerStyle: 'width: 350px',
cardVisible: true,
columnClass: 'expand',
},
{
name: 'producer',
align: 'left',
label: t('negative.supplier'),
field: ({ producer }) => dashIfEmpty(producer),
sortable: true,
columnClass: 'shrink',
},
{
name: 'inkFk',
align: 'left',
label: t('negative.colour'),
field: ({ inkFk }) => inkFk,
sortable: true,
cardVisible: true,
},
{
name: 'size',
align: 'left',
label: t('negative.size'),
field: ({ size }) => size,
sortable: true,
cardVisible: true,
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
},
{
name: 'category',
align: 'left',
label: t('negative.origen'),
field: ({ category }) => dashIfEmpty(category),
sortable: true,
cardVisible: true,
},
{
name: 'lack',
align: 'left',
label: t('negative.lack'),
field: ({ lack }) => lack,
columnFilter: {
component: 'input',
type: 'number',
columnClass: 'shrink',
},
sortable: true,
headerStyle: 'padding-left: 33px',
cardVisible: true,
},
{
name: 'tableActions',
align: 'left',
actions: [
{
title: t('Open details'),
icon: 'edit',
action: redirectToCreateView,
isPrimary: true,
},
],
},
]);
const setUserParams = (params) => {
filterParams.value = params;
};
</script>
<template>
<RightMenu>
<template #right-panel>
<TicketLackFilter data-key="NegativeList" @set-user-params="setUserParams" />
</template>
</RightMenu>
{{ filterRef }}
<VnTable
ref="tableRef"
data-key="NegativeList"
:url="`Tickets/itemLack`"
:order="['itemFk DESC, date DESC, timed DESC']"
:user-params="negativeParams"
auto-load
:columns="columns"
default-mode="table"
:right-search="false"
:is-editable="false"
:use-model="true"
:map-key="false"
:row-click="redirectToCreateView"
v-model:selected="selectedRows"
:create="false"
:crud-model="{
disableInfiniteScroll: true,
}"
:table="{
'row-key': 'itemFk',
selection: 'multiple',
}"
>
<template #column-itemFk="{ row }">
<div
style="display: flex; justify-content: space-around; align-items: center"
>
<span @click.stop>{{ row.itemFk }}</span>
</div>
</template>
<template #column-longName="{ row }">
<span class="link" @click.stop>
{{ row.longName }}
<ItemDescriptorProxy :id="row.itemFk" />
</span>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
.list {
max-height: 100%;
padding: 15px;
width: 100%;
}
.grid-style-transition {
transition:
transform 0.28s,
background-color 0.28s;
}
#true {
background-color: $positive;
}
#false {
background-color: $negative;
}
div.q-dialog__inner > div {
max-width: fit-content !important;
}
.q-btn-group > .q-btn-item:not(:first-child) {
border-top-left-radius: 0;
border-bottom-left-radius: 0;
}
</style>

View File

@ -0,0 +1,362 @@
<script setup>
import FetchedTags from 'components/ui/FetchedTags.vue';
import ItemDescriptorProxy from 'src/pages/Item/Card/ItemDescriptorProxy.vue';
import { computed, ref, watch } from 'vue';
import { useI18n } from 'vue-i18n';
import axios from 'axios';
import FetchData from 'src/components/FetchData.vue';
import { toDate, toHour } from 'src/filters';
import useNotify from 'src/composables/useNotify.js';
import ZoneDescriptorProxy from 'pages/Zone/Card/ZoneDescriptorProxy.vue';
import { useRoute } from 'vue-router';
import VnTable from 'src/components/VnTable/VnTable.vue';
import TicketDescriptorProxy from '../Card/TicketDescriptorProxy.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CustomerDescriptorProxy from 'src/pages/Customer/Card/CustomerDescriptorProxy.vue';
const $props = defineProps({
filter: {
type: Object,
default: () => ({}),
},
});
watch(
() => $props.filter,
(v) => {
filterLack.value.where = v;
tableRef.value.reload(filterLack);
},
);
const filterLack = ref({
include: [
{
relation: 'workers',
scope: {
fields: ['id', 'firstName'],
},
},
],
where: { ...$props.filter },
order: 'ts.alertLevelCode ASC',
});
const selectedRows = ref([]);
const { t } = useI18n();
const { notify } = useNotify();
const entityId = computed(() => route.params.id);
const item = ref({});
const route = useRoute();
const columns = computed(() => [
{
name: 'status',
align: 'left',
sortable: false,
columnClass: 'expand',
columnFilter: false,
},
{
name: 'ticketFk',
label: t('negative.detail.ticketFk'),
align: 'left',
sortable: true,
columnFilter: {
component: 'input',
type: 'number',
},
columnClass: 'shrink',
},
{
name: 'shipped',
label: t('negative.detail.shipped'),
field: 'shipped',
align: 'left',
format: ({ shipped }) => toDate(shipped),
sortable: true,
columnFilter: {
component: 'date',
columnClass: 'shrink',
},
},
{
name: 'minTimed',
label: t('negative.detail.theoreticalhour'),
field: 'minTimed',
align: 'left',
format: ({ minTimed }) => toHour(minTimed),
sortable: true,
component: 'time',
columnClass: 'shrink',
columnFilter: {},
},
{
name: 'alertLevelCode',
label: t('negative.detail.state'),
columnFilter: {
name: 'alertLevelCode',
component: 'select',
attrs: {
url: 'AlertLevels',
fields: ['name', 'code'],
optionLabel: 'code',
optionValue: 'code',
},
},
columnClass: 'expand',
align: 'left',
sortable: true,
},
{
name: 'zoneName',
label: t('negative.detail.zoneName'),
field: 'zoneName',
align: 'left',
sortable: true,
},
{
name: 'nickname',
label: t('negative.detail.nickname'),
field: 'nickname',
align: 'left',
sortable: true,
},
{
name: 'quantity',
label: t('negative.detail.quantity'),
field: 'quantity',
align: 'left',
sortable: true,
component: 'input',
type: 'number',
},
]);
const emit = defineEmits(['update:selection']);
const itemLack = ref(null);
const fetchItemLack = ref(null);
const tableRef = ref(null);
defineExpose({ tableRef, itemLack });
watch(selectedRows, () => emit('update:selection', selectedRows));
const getInputEvents = ({ col, ...rows }) => ({
'update:modelValue': () => saveChange(col.name, rows),
'keyup.enter': () => saveChange(col.name, rows),
});
const saveChange = async (field, { row }) => {
try {
switch (field) {
case 'alertLevelCode':
await axios.post(`Tickets/state`, {
ticketFk: row.ticketFk,
code: row[field],
});
break;
case 'quantity':
await axios.post(`Sales/${row.saleFk}/updateQuantity`, {
quantity: +row.quantity,
});
break;
}
notify('globals.dataSaved', 'positive');
fetchItemLack.value.fetch();
} catch (err) {
console.error('Error saving changes', err);
f;
}
};
const hasToIgnore = (row) => row.hasToIgnore === 1;
function onBuysFetched(data) {
Object.assign(item.value, data[0]);
}
</script>
<template>
<FetchData
ref="fetchItemLack"
:url="`Tickets/itemLack`"
:params="{ id: entityId }"
@on-fetch="(data) => (itemLack = data[0])"
auto-load
/>
<FetchData
:url="`Items/${entityId}/getCard`"
:fields="['longName']"
@on-fetch="(data) => (item = data)"
auto-load
/>
<FetchData
:url="`Buys/latestBuysFilter`"
:fields="['longName']"
:filter="{ where: { 'i.id': entityId } }"
@on-fetch="onBuysFetched"
auto-load
/>
<VnTable
ref="tableRef"
data-key="NegativeItem"
:map-key="false"
:url="`Tickets/itemLack/${entityId}`"
:columns="columns"
auto-load
:create="false"
:create-as-dialog="false"
:use-model="true"
:filter="filterLack"
:order="['ts.alertLevelCode ASC']"
:table="{
'row-key': 'id',
selection: 'multiple',
}"
dense
:is-editable="true"
:row-click="false"
:right-search="false"
:right-search-icon="false"
v-model:selected="selectedRows"
:disable-option="{ card: true }"
>
<template #top-left>
<div style="display: flex; align-items: center" v-if="itemLack">
<!-- <VnImg :id="itemLack.itemFk" class="rounded image-wrapper"></VnImg> -->
<div class="flex column" style="align-items: center">
<QBadge
ref="badgeLackRef"
class="q-ml-xs"
text-color="white"
:color="itemLack.lack === 0 ? 'positive' : 'negative'"
:label="itemLack.lack"
/>
</div>
<div class="flex column left" style="align-items: flex-start">
<QBtn flat class="link text-blue">
{{ item?.longName ?? item.name }}
<ItemDescriptorProxy :id="entityId" />
<FetchedTags class="q-ml-md" :item="item" :columns="7" />
</QBtn>
</div>
</div>
</template>
<template #top-right>
<slot name="top-right" />
</template>
<template #column-status="{ row }">
<QTd style="width: 150px">
<div class="icon-container">
<QIcon
v-if="row.isBasket"
name="vn:basket"
color="primary"
class="cursor-pointer"
size="xs"
>
<QTooltip>{{ t('negative.detail.isBasket') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.hasToIgnore"
name="star"
color="primary"
class="cursor-pointer fill-icon"
size="xs"
>
<QTooltip>{{ t('negative.detail.hasToIgnore') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.hasObservation"
name="change_circle"
color="primary"
class="cursor-pointer"
size="xs"
>
<QTooltip>{{
t('negative.detail.hasObservation')
}}</QTooltip> </QIcon
><QIcon
v-if="row.isRookie"
name="vn:Person"
size="xs"
color="primary"
class="cursor-pointer"
>
<QTooltip>{{ t('negative.detail.isRookie') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.peticionCompra"
name="vn:buyrequest"
size="xs"
color="primary"
class="cursor-pointer"
>
<QTooltip>{{ t('negative.detail.peticionCompra') }}</QTooltip>
</QIcon>
<QIcon
v-if="row.turno"
name="vn:calendar"
size="xs"
color="primary"
class="cursor-pointer"
>
<QTooltip>{{ t('negative.detail.turno') }}</QTooltip>
</QIcon>
</div></QTd
>
</template>
<template #column-nickname="{ row }">
<span class="link" @click.stop>
{{ row.nickname }}
<CustomerDescriptorProxy :id="row.customerId" />
</span>
</template>
<template #column-ticketFk="{ row }">
<span class="q-pa-sm link">
{{ row.id }}
<TicketDescriptorProxy :id="row.id" />
</span>
</template>
<template #column-alertLevelCode="props">
<VnSelect
url="States/editableStates"
auto-load
hide-selected
option-value="id"
option-label="name"
v-model="props.row.alertLevelCode"
v-on="getInputEvents(props)"
/>
</template>
<template #column-zoneName="{ row }">
<span class="link">{{ row.zoneName }}</span>
<ZoneDescriptorProxy :id="row.zoneFk" />
</template>
<template #column-quantity="props">
<VnInputNumber
v-model.number="props.row.quantity"
v-on="getInputEvents(props)"
></VnInputNumber>
</template>
</VnTable>
</template>
<style lang="scss" scoped>
.icon-container {
display: grid;
grid-template-columns: repeat(3, 0.2fr);
row-gap: 5px; /* Ajusta el espacio entre los iconos según sea necesario */
}
.icon-container > * {
width: 100%;
height: auto;
}
.list-enter-active,
.list-leave-active {
transition: all 1s ease;
}
.list-enter-from,
.list-leave-to {
opacity: 0;
background-color: $primary;
}
</style>

View File

@ -0,0 +1,90 @@
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import VnSelect from 'src/components/common/VnSelect.vue';
import notifyResults from 'src/utils/notifyResults';
const emit = defineEmits(['update-item']);
const showChangeItemDialog = ref(false);
const newItem = ref(null);
const $props = defineProps({
selectedRows: {
type: Array,
default: () => [],
},
});
const updateItem = async () => {
try {
showChangeItemDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ saleFk, quantity }) =>
axios.post(`Sales/replaceItem`, {
saleFk,
substitutionFk: newItem.value,
quantity,
}),
);
const result = await Promise.allSettled(rowsToUpdate);
notifyResults(result, 'saleFk');
emit('update-item', newItem.value);
} catch (err) {
console.error('Error updating item:', err);
return err;
}
};
</script>
<template>
<QCard class="q-pa-sm">
<QCardSection class="row items-center justify-center column items-stretch">
{{ showChangeItemDialog }}
<span>{{ $t('negative.detail.modal.changeItem.title') }}</span>
<VnSelect
url="Items/WithName"
:fields="['id', 'name']"
:sort-by="['id DESC']"
:options="items"
option-label="name"
option-value="id"
v-model="newItem"
>
</VnSelect>
</QCardSection>
<QCardActions align="right">
<QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup />
<QBtn
:label="$t('globals.confirm')"
color="primary"
:disable="!newItem"
@click="updateItem"
unelevated
autofocus
/> </QCardActions
></QCard>
</template>
<style lang="scss" scoped>
.list {
max-height: 100%;
padding: 15px;
width: 100%;
}
.grid-style-transition {
transition:
transform 0.28s,
background-color 0.28s;
}
#true {
background-color: $positive;
}
#false {
background-color: $negative;
}
div.q-dialog__inner > div {
max-width: fit-content !important;
}
</style>

View File

@ -0,0 +1,84 @@
<script setup>
import { ref, defineEmits } from 'vue';
import axios from 'axios';
import VnInput from 'src/components/common/VnInput.vue';
import notifyResults from 'src/utils/notifyResults';
const showChangeQuantityDialog = ref(false);
const newQuantity = ref(null);
const $props = defineProps({
selectedRows: {
type: Array,
default: () => [],
},
});
const emit = defineEmits(['update-quantity']);
const updateQuantity = async () => {
try {
showChangeQuantityDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ saleFk }) =>
axios.post(`Sales/${saleFk}/updateQuantity`, {
saleFk,
quantity: +newQuantity.value,
}),
);
const result = await Promise.allSettled(rowsToUpdate);
notifyResults(result, 'saleFk');
emit('update-quantity', newQuantity.value);
} catch (err) {
return err;
}
};
</script>
<template>
<QCard class="q-pa-sm">
<QCardSection class="row items-center justify-center column items-stretch">
<span>{{ $t('negative.detail.modal.changeQuantity.title') }}</span>
<VnInput
type="number"
:min="0"
:label="$t('negative.detail.modal.changeQuantity.placeholder')"
v-model="newQuantity"
/>
</QCardSection>
<QCardActions align="right">
<QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup />
<QBtn
:label="$t('globals.confirm')"
color="primary"
:disable="!newQuantity || newQuantity < 0"
@click="updateQuantity"
unelevated
autofocus
/> </QCardActions
></QCard>
</template>
<style lang="scss" scoped>
.list {
max-height: 100%;
padding: 15px;
width: 100%;
}
.grid-style-transition {
transition:
transform 0.28s,
background-color 0.28s;
}
#true {
background-color: $positive;
}
#false {
background-color: $negative;
}
div.q-dialog__inner > div {
max-width: fit-content !important;
}
</style>

View File

@ -0,0 +1,91 @@
<script setup>
import { ref } from 'vue';
import axios from 'axios';
import VnSelect from 'src/components/common/VnSelect.vue';
import FetchData from 'components/FetchData.vue';
import notifyResults from 'src/utils/notifyResults';
const emit = defineEmits(['update-state']);
const editableStates = ref([]);
const showChangeStateDialog = ref(false);
const newState = ref(null);
const $props = defineProps({
selectedRows: {
type: Array,
default: () => [],
},
});
const updateState = async () => {
try {
showChangeStateDialog.value = true;
const rowsToUpdate = $props.selectedRows.map(({ id }) =>
axios.post(`Tickets/state`, {
ticketFk: id,
code: newState.value,
}),
);
const result = await Promise.allSettled(rowsToUpdate);
notifyResults(result, 'ticketFk');
emit('update-state', newState.value);
} catch (err) {
return err;
}
};
</script>
<template>
<FetchData
url="States/editableStates"
@on-fetch="(data) => (editableStates = data)"
auto-load
/>
<QCard class="q-pa-sm">
<QCardSection class="row items-center justify-center column items-stretch">
<span>{{ $t('negative.detail.modal.changeState.title') }}</span>
<VnSelect
:label="$t('negative.detail.modal.changeState.placeholder')"
v-model="newState"
:options="editableStates"
option-label="name"
option-value="code"
/>
</QCardSection>
<QCardActions align="right">
<QBtn :label="$t('globals.cancel')" color="primary" flat v-close-popup />
<QBtn
:label="$t('globals.confirm')"
color="primary"
:disable="!newState"
@click="updateState"
unelevated
autofocus
/> </QCardActions
></QCard>
</template>
<style lang="scss" scoped>
.list {
max-height: 100%;
padding: 15px;
width: 100%;
}
.grid-style-transition {
transition:
transform 0.28s,
background-color 0.28s;
}
#true {
background-color: $positive;
}
#false {
background-color: $negative;
}
div.q-dialog__inner > div {
max-width: fit-content !important;
}
</style>

View File

@ -23,6 +23,8 @@ ticketSale:
hasComponentLack: Component lack
ok: Ok
more: More
transferLines: Transfer lines(no basket)/ Split
transferBasket: Some row selected is basket
advanceTickets:
preparation: Preparation
origin: Origin
@ -188,7 +190,6 @@ ticketList:
accountPayment: Account payment
sendDocuware: Set delivered and send delivery note(s) to the tablet
addPayment: Add payment
date: Date
company: Company
amount: Amount
reference: Reference
@ -202,8 +203,6 @@ ticketList:
creditCard: Credit card
transfers: Transfers
province: Province
warehouse: Warehouse
hour: Hour
closure: Closure
toLines: Go to lines
addressNickname: Address nickname
@ -214,3 +213,79 @@ ticketList:
notVisible: Not visible
clientFrozen: Client frozen
componentLack: Component lack
negative:
hour: Hour
id: Id Article
longName: Article
supplier: Supplier
colour: Colour
size: Size
origen: Origin
value: Negative
itemFk: Article
producer: Producer
warehouse: Warehouse
warehouseFk: Warehouse
category: Category
categoryFk: Family
type: Type
typeFk: Type
lack: Negative
inkFk: inkFk
timed: timed
date: Date
minTimed: minTimed
negativeAction: Negative
totalNegative: Total negatives
days: Days
buttonsUpdate:
item: Item
state: State
quantity: Quantity
modalOrigin:
title: Update negatives
question: Select a state to update
modalSplit:
title: Confirm split selected
question: Select a state to update
detail:
saleFk: Sale
itemFk: Article
ticketFk: Ticket
code: Code
nickname: Alias
name: Name
zoneName: Agency name
shipped: Date
theoreticalhour: Theoretical hour
agName: Agency
quantity: Quantity
alertLevelCode: Group state
state: State
peticionCompra: Ticket request
isRookie: Is rookie
turno: Turn line
isBasket: Basket
hasObservation: Has substitution
hasToIgnore: VIP
modal:
changeItem:
title: Update item reference
placeholder: New item
changeState:
title: Update tickets state
placeholder: New state
changeQuantity:
title: Update tickets quantity
placeholder: New quantity
split:
title: Are you sure you want to split selected tickets?
subTitle: Confirm split action
handleSplited:
title: Handle splited tickets
subTitle: Confirm date and agency
split:
ticket: Old ticket
newTicket: New ticket
status: Result
message: Message

View File

@ -127,6 +127,8 @@ ticketSale:
ok: Ok
more: Más
address: Consignatario
transferLines: Transferir líneas(no cesta)/ Separar
transferBasket: No disponible para una cesta
size: Medida
ticketComponents:
serie: Serie
@ -213,6 +215,81 @@ ticketList:
toLines: Ir a lineas
addressNickname: Alias consignatario
ref: Referencia
negative:
hour: Hora
id: Id Articulo
longName: Articulo
supplier: Productor
colour: Color
size: Medida
origen: Origen
value: Negativo
warehouseFk: Almacen
producer: Producer
category: Categoría
categoryFk: Familia
typeFk: Familia
warehouse: Almacen
lack: Negativo
inkFk: Color
timed: Hora
date: Fecha
minTimed: Hora
type: Tipo
negativeAction: Negativo
totalNegative: Total negativos
days: Rango de dias
buttonsUpdate:
item: artículo
state: Estado
quantity: Cantidad
modalOrigin:
title: Actualizar negativos
question: Seleccione un estado para guardar
modalSplit:
title: Confirmar acción de split
question: Selecciona un estado
detail:
saleFk: Línea
itemFk: Artículo
ticketFk: Ticket
code: code
nickname: Alias
name: Nombre
zoneName: Agencia
shipped: F. envío
theoreticalhour: Hora teórica
agName: Agencia
quantity: Cantidad
alertLevelCode: Estado agrupado
state: Estado
peticionCompra: Petición compra
isRookie: Cliente nuevo
turno: Linea turno
isBasket: Cesta
hasObservation: Tiene sustitución
hasToIgnore: VIP
modal:
changeItem:
title: Actualizar referencia artículo
placeholder: Nuevo articulo
changeState:
title: Actualizar estado
placeholder: Nuevo estado
changeQuantity:
title: Actualizar cantidad
placeholder: Nueva cantidad
split:
title: ¿Seguro de separar los tickets seleccionados?
subTitle: Confirma separar tickets seleccionados
handleSplited:
title: Gestionar tickets spliteados
subTitle: Confir fecha y agencia
split:
ticket: Ticket viejo
newTicket: Ticket nuevo
status: Estado
message: Mensaje
rounding: Redondeo
noVerifiedData: Sin datos comprobados
purchaseRequest: Petición de compra

View File

@ -192,7 +192,13 @@ export default {
icon: 'vn:ticket',
moduleName: 'Ticket',
keyBinding: 't',
menu: ['TicketList', 'TicketAdvance', 'TicketWeekly', 'TicketFuture'],
menu: [
'TicketList',
'TicketAdvance',
'TicketWeekly',
'TicketFuture',
'TicketNegative',
],
},
component: RouterView,
redirect: { name: 'TicketMain' },
@ -229,6 +235,32 @@ export default {
},
component: () => import('src/pages/Ticket/TicketCreate.vue'),
},
{
path: 'negative',
redirect: { name: 'TicketNegative' },
children: [
{
name: 'TicketNegative',
meta: {
title: 'negative',
icon: 'exposure',
},
component: () =>
import('src/pages/Ticket/Negative/TicketLackList.vue'),
path: '',
},
{
name: 'NegativeDetail',
path: ':id',
meta: {
title: 'summary',
icon: 'launch',
},
component: () =>
import('src/pages/Ticket/Negative/TicketLackDetail.vue'),
},
],
},
{
path: 'weekly',
name: 'TicketWeekly',

View File

@ -0,0 +1,19 @@
import { Notify } from 'quasar';
export default function (results, key) {
results.forEach((result, index) => {
if (result.status === 'fulfilled') {
const data = JSON.parse(result.value.config.data);
Notify.create({
type: 'positive',
message: `Operación (${index + 1}) ${data[key]} completada con éxito.`,
});
} else {
const data = JSON.parse(result.reason.config.data);
Notify.create({
type: 'negative',
message: `Operación (${index + 1}) ${data[key]} fallida: ${result.reason.message}`,
});
}
});
}

View File

@ -1,9 +1,9 @@
/// <reference types="cypress" />
describe('InvoiceInBasicData', () => {
const formInputs = '.q-form > .q-card input';
const firstFormSelect = '.q-card > .vn-row:nth-child(1) > .q-select';
const documentBtns = '[data-cy="dms-buttons"] button';
const dialogInputs = '.q-dialog input';
const resetBtn = '.q-btn-group--push > .q-btn--flat';
const getDocumentBtns = (opt) => `[data-cy="dms-buttons"] > :nth-child(${opt})`;
beforeEach(() => {
cy.login('developer');
@ -11,13 +11,16 @@ describe('InvoiceInBasicData', () => {
});
it('should edit the provideer and supplier ref', () => {
cy.selectOption(firstFormSelect, 'Bros');
cy.get('[title="Reset"]').click();
cy.get(formInputs).eq(1).type('{selectall}4739');
cy.saveCard();
cy.dataCy('UnDeductibleVatSelect').type('4751000000');
cy.get('.q-menu .q-item').contains('4751000000').click();
cy.get(resetBtn).click();
cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Plants nick');
cy.get(formInputs).eq(1).invoke('val').should('eq', '4739');
cy.waitForElement('#formModel').within(() => {
cy.dataCy('vnSupplierSelect').type('Bros nick');
})
cy.get('.q-menu .q-item').contains('Bros nick').click();
cy.saveCard();
cy.get(`${firstFormSelect} input`).invoke('val').should('eq', 'Bros nick');
});
it('should edit, remove and create the dms data', () => {
@ -25,18 +28,18 @@ describe('InvoiceInBasicData', () => {
const secondInput = "I don't know what posting here!";
//edit
cy.get(documentBtns).eq(1).click();
cy.get(getDocumentBtns(2)).click();
cy.get(dialogInputs).eq(0).type(`{selectall}${firtsInput}`);
cy.get('textarea').type(`{selectall}${secondInput}`);
cy.get('[data-cy="FormModelPopup_save"]').click();
cy.get(documentBtns).eq(1).click();
cy.get(getDocumentBtns(2)).click();
cy.get(dialogInputs).eq(0).invoke('val').should('eq', firtsInput);
cy.get('textarea').invoke('val').should('eq', secondInput);
cy.get('[data-cy="FormModelPopup_save"]').click();
cy.checkNotification('Data saved');
//remove
cy.get(documentBtns).eq(2).click();
cy.get(getDocumentBtns(3)).click();
cy.get('[data-cy="VnConfirm_confirm"]').click();
cy.checkNotification('Data saved');

View File

@ -0,0 +1,11 @@
/// <reference types="cypress" />
describe('ItemProposal', () => {
beforeEach(() => {
const ticketId = 1;
cy.login('developer');
cy.visit(`/#/ticket/${ticketId}/summary`);
});
describe('Handle item proposal selected', () => {});
});

View File

@ -0,0 +1,147 @@
/// <reference types="cypress" />
describe('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');
cy.wait('@getItemLack');
});
describe('Table actions', () => {
it.skip('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('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');
});
describe('Replace item if', () => {
it.only('Quantity is less than available', () => {
cy.get(':nth-child(1) > .text-right > .q-btn').click();
});
});
});
});

View File

@ -0,0 +1,36 @@
/// <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.visit('/#/ticket/negative');
});
describe('Table actions', () => {
it('should display only one row in the lack list', () => {
cy.wait('@getLack', { timeout: 10000 });
cy.get('.q-virtual-scroll__content > :nth-child(1) > .sticky').click();
cy.location('href').should('contain', '#/ticket/negative/5');
});
});
});