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

This commit is contained in:
Alex Moreno 2025-02-12 06:37:12 +00:00
commit 653c9f4d3e
108 changed files with 2916 additions and 365 deletions

View File

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

View File

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

View File

@ -0,0 +1,2 @@
export const langs = ['en', 'es'];
export const decimalPlaces = 2;

View File

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

View File

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

View File

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

View File

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

@ -171,7 +171,8 @@ onMounted(() => {
}); });
const arrayDataKey = const arrayDataKey =
$props.dataKey ?? ($props.url?.length > 0 ? $props.url : $attrs.name ?? $attrs.label); $props.dataKey ??
($props.url?.length > 0 ? $props.url : ($attrs.name ?? $attrs.label));
const arrayData = useArrayData(arrayDataKey, { const arrayData = useArrayData(arrayDataKey, {
url: $props.url, url: $props.url,
@ -220,7 +221,7 @@ async function fetchFilter(val) {
optionFilterValue.value ?? optionFilterValue.value ??
(new RegExp(/\d/g).test(val) (new RegExp(/\d/g).test(val)
? optionValue.value ? optionValue.value
: optionFilter.value ?? optionLabel.value); : (optionFilter.value ?? optionLabel.value));
let defaultWhere = {}; let defaultWhere = {};
if ($props.filterOptions.length) { if ($props.filterOptions.length) {
@ -239,7 +240,7 @@ async function fetchFilter(val) {
const { data } = await arrayData.applyFilter( const { data } = await arrayData.applyFilter(
{ filter: filterOptions }, { filter: filterOptions },
{ updateRouter: false } { updateRouter: false },
); );
setOptions(data); setOptions(data);
return data; return data;
@ -272,7 +273,7 @@ async function filterHandler(val, update) {
ref.setOptionIndex(-1); ref.setOptionIndex(-1);
ref.moveOptionSelection(1, true); ref.moveOptionSelection(1, true);
} }
} },
); );
} }
@ -308,7 +309,7 @@ function handleKeyDown(event) {
if (inputValue) { if (inputValue) {
const matchingOption = myOptions.value.find( const matchingOption = myOptions.value.find(
(option) => (option) =>
option[optionLabel.value].toLowerCase() === inputValue.toLowerCase() option[optionLabel.value].toLowerCase() === inputValue.toLowerCase(),
); );
if (matchingOption) { if (matchingOption) {
@ -320,11 +321,11 @@ function handleKeyDown(event) {
} }
const focusableElements = document.querySelectorAll( const focusableElements = document.querySelectorAll(
'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])' 'a:not([disabled]), button:not([disabled]), input:not([disabled]), textarea:not([disabled]), select:not([disabled]), details:not([disabled]), [tabindex]:not([tabindex="-1"]):not([disabled])',
); );
const currentIndex = Array.prototype.indexOf.call( const currentIndex = Array.prototype.indexOf.call(
focusableElements, focusableElements,
event.target event.target,
); );
if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) { if (currentIndex >= 0 && currentIndex < focusableElements.length - 1) {
focusableElements[currentIndex + 1].focus(); focusableElements[currentIndex + 1].focus();

View File

@ -1,9 +1,7 @@
<script setup> <script setup>
import { computed } from 'vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
const model = defineModel({ type: [String, Number, Object] }); const model = defineModel({ type: [String, Number, Object] });
const url = 'Suppliers';
</script> </script>
<template> <template>
@ -11,11 +9,13 @@ const url = 'Suppliers';
:label="$t('globals.supplier')" :label="$t('globals.supplier')"
v-bind="$attrs" v-bind="$attrs"
v-model="model" v-model="model"
:url="url" url="Suppliers"
option-value="id" option-value="id"
option-label="nickname" option-label="nickname"
:fields="['id', 'name', 'nickname', 'nif']" :fields="['id', 'name', 'nickname', 'nif']"
:filter-options="['id', 'name', 'nickname', 'nif']"
sort-by="name ASC" sort-by="name ASC"
data-cy="vnSupplierSelect"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">

View File

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

View File

@ -18,7 +18,12 @@ import VnInput from 'components/common/VnInput.vue';
const emit = defineEmits(['onFetch']); const emit = defineEmits(['onFetch']);
const $attrs = useAttrs(); const originalAttrs = useAttrs();
const $attrs = computed(() => {
const { style, ...rest } = originalAttrs;
return rest;
});
const isRequired = computed(() => { const isRequired = computed(() => {
return Object.keys($attrs).includes('required') return Object.keys($attrs).includes('required')

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; 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() { function isEmployee() {
return hasAny(['employee']); return hasAny(['employee']);
} }
@ -35,6 +44,7 @@ export function useRole() {
isEmployee, isEmployee,
fetch, fetch,
hasAny, hasAny,
likeAny,
state, state,
}; };
} }

View File

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

View File

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

View File

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

View File

@ -36,6 +36,7 @@ globals:
clone: Clonar clone: Clonar
confirm: Confirmar confirm: Confirmar
assign: Asignar assign: Asignar
replace: Sustituir
back: Volver back: Volver
yes: Si yes: Si
no: No no: No
@ -48,6 +49,7 @@ globals:
rowRemoved: Fila eliminada rowRemoved: Fila eliminada
pleaseWait: Por favor espera... pleaseWait: Por favor espera...
noPinnedModules: No has fijado ningún módulo noPinnedModules: No has fijado ningún módulo
split: Split
summary: summary:
basicData: Datos básicos basicData: Datos básicos
daysOnward: Días adelante daysOnward: Días adelante
@ -76,8 +78,10 @@ globals:
requiredField: Campo obligatorio requiredField: Campo obligatorio
class: clase class: clase
type: Tipo type: Tipo
reason: motivo reason: Motivo
removeSelection: Eliminar selección
noResults: Sin resultados noResults: Sin resultados
results: resultados
system: Sistema system: Sistema
notificationSent: Notificación enviada notificationSent: Notificación enviada
warehouse: Almacén warehouse: Almacén
@ -166,6 +170,7 @@ globals:
agency: Agencia agency: Agencia
workCenters: Centros de trabajo workCenters: Centros de trabajo
modes: Modos modes: Modos
negative: Tickets negativos
zones: Zonas zones: Zonas
zonesList: Listado zonesList: Listado
deliveryDays: Días de entrega deliveryDays: Días de entrega
@ -286,9 +291,9 @@ globals:
buyRequest: Peticiones de compra buyRequest: Peticiones de compra
wasteBreakdown: Deglose de mermas wasteBreakdown: Deglose de mermas
itemCreate: Nuevo artículo itemCreate: Nuevo artículo
tax: 'IVA' tax: IVA
botanical: 'Botánico' botanical: Botánico
barcode: 'Código de barras' barcode: Código de barras
itemTypeCreate: Nueva familia itemTypeCreate: Nueva familia
family: Familia family: Familia
lastEntries: Últimas entradas lastEntries: Últimas entradas
@ -352,7 +357,7 @@ globals:
from: Desde from: Desde
to: Hasta to: Hasta
supplierFk: Proveedor supplierFk: Proveedor
supplierRef: Ref. proveedor supplierRef: Nº factura
serial: Serie serial: Serie
amount: Importe amount: Importe
awbCode: AWB awbCode: AWB
@ -410,6 +415,38 @@ ticket:
freightItemName: Nombre freightItemName: Nombre
packageItemName: Embalaje packageItemName: Embalaje
longName: Descripción 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: card:
customerId: ID cliente customerId: ID cliente
customerCard: Ficha del cliente customerCard: Ficha del cliente
@ -635,8 +672,8 @@ wagon:
volumeNotEmpty: El volumen no puede estar vacío volumeNotEmpty: El volumen no puede estar vacío
typeNotEmpty: El tipo no puede estar vacío typeNotEmpty: El tipo no puede estar vacío
maxTrays: Has alcanzado el número máximo de bandejas maxTrays: Has alcanzado el número máximo de bandejas
minHeightBetweenTrays: 'La distancia mínima entre bandejas es ' minHeightBetweenTrays: La distancia mínima entre bandejas es
maxWagonHeight: 'La altura máxima del vagón es ' maxWagonHeight: La altura máxima del vagón es
uncompleteTrays: Hay bandejas sin completar uncompleteTrays: Hay bandejas sin completar
params: params:
label: Etiqueta label: Etiqueta
@ -653,7 +690,6 @@ supplier:
tableVisibleColumns: tableVisibleColumns:
nif: NIF/CIF nif: NIF/CIF
account: Cuenta account: Cuenta
summary: summary:
responsible: Responsable responsible: Responsable
verified: Verificado verified: Verificado
@ -784,7 +820,7 @@ components:
cardDescriptor: cardDescriptor:
mainList: Listado principal mainList: Listado principal
summary: Resumen summary: Resumen
moreOptions: 'Más opciones' moreOptions: Más opciones
leftMenu: leftMenu:
addToPinned: Añadir a fijados addToPinned: Añadir a fijados
removeFromPinned: Eliminar de fijados removeFromPinned: Eliminar de fijados

View File

@ -1,12 +1,12 @@
<script setup> <script setup>
import { Dark, Quasar } from 'quasar'; import { Dark, Quasar } from 'quasar';
import { computed } from 'vue'; import { computed, onMounted } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { localeEquivalence } from 'src/i18n/index'; import { localeEquivalence } from 'src/i18n/index';
import quasarLang from 'src/utils/quasarLang'; import quasarLang from 'src/utils/quasarLang';
import { langs } from 'src/boot/defaults/constants.js';
const { t, locale } = useI18n(); const { t, locale } = useI18n();
const userLocale = computed({ const userLocale = computed({
get() { get() {
return locale.value; return locale.value;
@ -28,7 +28,6 @@ const darkMode = computed({
Dark.set(value); Dark.set(value);
}, },
}); });
const langs = ['en', 'es'];
</script> </script>
<template> <template>

View File

@ -23,7 +23,7 @@ onMounted(async () => {
<CardDescriptor <CardDescriptor
ref="descriptor" ref="descriptor"
:url="`VnUsers/preview`" :url="`VnUsers/preview`"
:filter="filter" :filter="{ ...filter, where: { id: entityId } }"
module="Account" module="Account"
data-key="Account" data-key="Account"
title="nickname" title="nickname"

View File

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

View File

@ -86,7 +86,7 @@ onMounted(async () => {
/> />
</template> </template>
</VnLv> </VnLv>
<VnLv :label="t('claim.zone')"> <VnLv v-if="entity.ticket?.zone?.id" :label="t('claim.zone')">
<template #value> <template #value>
<span class="link"> <span class="link">
{{ entity.ticket?.zone?.name }} {{ entity.ticket?.zone?.name }}
@ -98,11 +98,10 @@ onMounted(async () => {
:label="t('claim.province')" :label="t('claim.province')"
:value="entity.ticket?.address?.province?.name" :value="entity.ticket?.address?.province?.name"
/> />
<VnLv :label="t('claim.ticketId')"> <VnLv v-if="entity.ticketFk" :label="t('claim.ticketId')">
<template #value> <template #value>
<span class="link"> <span class="link">
{{ entity.ticketFk }} {{ entity.ticketFk }}
<TicketDescriptorProxy :id="entity.ticketFk" /> <TicketDescriptorProxy :id="entity.ticketFk" />
</span> </span>
</template> </template>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { computed } from 'vue'; import { computed, useAttrs } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import VnNotes from 'src/components/ui/VnNotes.vue'; import VnNotes from 'src/components/ui/VnNotes.vue';
@ -7,6 +7,7 @@ import VnNotes from 'src/components/ui/VnNotes.vue';
const route = useRoute(); const route = useRoute();
const state = useState(); const state = useState();
const user = state.getUser(); const user = state.getUser();
const $attrs = useAttrs();
const $props = defineProps({ const $props = defineProps({
id: { type: [Number, String], default: null }, id: { type: [Number, String], default: null },

View File

@ -131,7 +131,7 @@ const STATE_COLOR = {
prefix="claim" prefix="claim"
:array-data-props="{ :array-data-props="{
url: 'Claims/filter', url: 'Claims/filter',
order: ['cs.priority ASC', 'created ASC'], order: 'cs.priority ASC, created ASC',
}" }"
> >
<template #advanced-menu> <template #advanced-menu>

View File

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

View File

@ -61,6 +61,23 @@ const columns = computed(() => [
columnFilter: false, columnFilter: false,
cardVisible: true, 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', name: 'description',
align: 'left', align: 'left',
@ -74,6 +91,7 @@ const columns = computed(() => [
name: 'quantity', name: 'quantity',
label: t('globals.quantity'), label: t('globals.quantity'),
cardVisible: true, cardVisible: true,
visible: true,
columnFilter: { columnFilter: {
inWhere: true, inWhere: true,
}, },
@ -138,11 +156,11 @@ const updateDateParams = (value, params) => {
const campaign = campaignList.value.find((c) => c.id === value); const campaign = campaignList.value.find((c) => c.id === value);
if (!campaign) return; if (!campaign) return;
const { dated, previousDays, scopeDays } = campaign; const { dated, scopeDays } = campaign;
const _date = new Date(dated); const from = new Date(dated);
const [from, to] = dateRange(_date); from.setDate(from.getDate() - scopeDays);
params.from = new Date(from.setDate(from.getDate() - previousDays)).toISOString(); params.from = from;
params.to = new Date(to.setDate(to.getDate() + scopeDays)).toISOString(); params.to = dated;
return params; return params;
}; };
</script> </script>
@ -205,24 +223,57 @@ const updateDateParams = (value, params) => {
<template #moreFilterPanel="{ params }"> <template #moreFilterPanel="{ params }">
<div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl"> <div class="column no-wrap flex-center q-gutter-y-md q-mt-xs q-pr-xl">
<VnSelect <VnSelect
v-model="params.campaign"
:options="campaignList"
:label="t('globals.campaign')"
:filled="true" :filled="true"
class="q-px-sm q-pt-none fit" 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 dense
option-label="code"
@update:model-value="(data) => updateDateParams(data, params)" @update:model-value="(data) => updateDateParams(data, params)"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel> <QItemLabel>{{ scope.opt?.name }}</QItemLabel>
{{ scope.opt?.code }} <QItemLabel caption>{{
{{ scope.opt?.category?.name
new Date(scope.opt?.dated).getFullYear() }}</QItemLabel>
}}</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> </QItemSection>
</QItem> </QItem>
</template> </template>
@ -247,7 +298,19 @@ const updateDateParams = (value, params) => {
</template> </template>
<i18n> <i18n>
en:
valentinesDay: Valentine's Day
mothersDay: Mother's Day
allSaints: All Saints' Day
es: es:
Enter a new search: Introduce una nueva búsqueda Enter a new search: Introduce una nueva búsqueda
Group by items: Agrupar por artículos 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> </i18n>

View File

@ -110,7 +110,21 @@ const debtWarning = computed(() => {
> >
<QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip> <QTooltip>{{ t('customer.card.isDisabled') }}</QTooltip>
</QIcon> </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> <QTooltip>{{ t('customer.card.isFrozen') }}</QTooltip>
</QIcon> </QIcon>
<QIcon <QIcon

View File

@ -61,6 +61,16 @@ const openCreateForm = (type) => {
.join('&'); .join('&');
useOpenURL(`/#/${type}/list?${params}`); 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> </script>
<template> <template>
@ -69,6 +79,13 @@ const openCreateForm = (type) => {
{{ t('globals.pageTitles.createTicket') }} {{ t('globals.pageTitles.createTicket') }}
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem v-ripple clickable>
<QItemSection @click="updateSubstitutionAllowed()">{{
$props.customer.substitutionAllowed
? t('Disable substitution')
: t('Allow substitution')
}}</QItemSection>
</QItem>
<QItem v-ripple clickable> <QItem v-ripple clickable>
<QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection> <QItemSection @click="showSmsDialog()">{{ t('Send SMS') }}</QItemSection>
</QItem> </QItem>

View File

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

View File

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

View File

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

View File

@ -9,7 +9,7 @@ import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.v
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue'; import CustomerDefaulterAddObservation from './CustomerDefaulterAddObservation.vue';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
import VnTable from 'src/components/VnTable/VnTable.vue'; import VnTable from 'src/components/VnTable/VnTable.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';

View File

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

View File

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

View File

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

View File

@ -125,7 +125,7 @@ function deleteFile(dmsFk) {
<VnInput <VnInput
clearable clearable
clear-icon="close" clear-icon="close"
:label="t('Supplier ref')" :label="t('invoiceIn.supplierRef')"
v-model="data.supplierRef" v-model="data.supplierRef"
/> />
</VnRow> </VnRow>
@ -149,6 +149,7 @@ function deleteFile(dmsFk) {
option-value="id" option-value="id"
option-label="id" option-label="id"
:filter-options="['id', 'name']" :filter-options="['id', 'name']"
data-cy="UnDeductibleVatSelect"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -310,7 +311,6 @@ function deleteFile(dmsFk) {
supplierFk: Supplier supplierFk: Supplier
es: es:
supplierFk: Proveedor supplierFk: Proveedor
Supplier ref: Ref. proveedor
Expedition date: Fecha expedición Expedition date: Fecha expedición
Operation date: Fecha operación Operation date: Fecha operación
Undeductible VAT: Iva no deducible Undeductible VAT: Iva no deducible

View File

@ -186,7 +186,7 @@ const createInvoiceInCorrection = async () => {
clickable clickable
@click="book(entityId)" @click="book(entityId)"
> >
<QItemSection>{{ t('invoiceIn.descriptorMenu.toBook') }}</QItemSection> <QItemSection>{{ t('invoiceIn.descriptorMenu.book') }}</QItemSection>
</QItem> </QItem>
</template> </template>
</InvoiceInToBook> </InvoiceInToBook>
@ -197,7 +197,7 @@ const createInvoiceInCorrection = async () => {
@click="triggerMenu('unbook')" @click="triggerMenu('unbook')"
> >
<QItemSection> <QItemSection>
{{ t('invoiceIn.descriptorMenu.toUnbook') }} {{ t('invoiceIn.descriptorMenu.unbook') }}
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem <QItem

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, onBeforeMount } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
@ -12,6 +12,7 @@ import VnSelect from 'src/components/common/VnSelect.vue';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { toCurrency } from 'filters/index';
const route = useRoute(); const route = useRoute();
const { notify } = useNotify(); const { notify } = useNotify();
@ -26,7 +27,7 @@ const invoiceInFormRef = ref();
const invoiceId = +route.params.id; const invoiceId = +route.params.id;
const filter = { where: { invoiceInFk: invoiceId } }; const filter = { where: { invoiceInFk: invoiceId } };
const areRows = ref(false); const areRows = ref(false);
const totals = ref();
const columns = computed(() => [ const columns = computed(() => [
{ {
name: 'duedate', name: 'duedate',
@ -66,6 +67,8 @@ const columns = computed(() => [
}, },
]); ]);
const totalAmount = computed(() => getTotal(invoiceInFormRef.value.formData, 'amount'));
const isNotEuro = (code) => code != 'EUR'; const isNotEuro = (code) => code != 'EUR';
async function insert() { async function insert() {
@ -73,6 +76,10 @@ async function insert() {
await invoiceInFormRef.value.reload(); await invoiceInFormRef.value.reload();
notify(t('globals.dataSaved'), 'positive'); notify(t('globals.dataSaved'), 'positive');
} }
onBeforeMount(async () => {
totals.value = (await axios.get(`InvoiceIns/${invoiceId}/getTotals`)).data;
});
</script> </script>
<template> <template>
<FetchData <FetchData
@ -153,7 +160,7 @@ async function insert() {
<QTd /> <QTd />
<QTd /> <QTd />
<QTd> <QTd>
{{ getTotal(rows, 'amount', { currency: 'default' }) }} {{ toCurrency(totalAmount) }}
</QTd> </QTd>
<QTd> <QTd>
<template v-if="isNotEuro(invoiceIn.currency.code)"> <template v-if="isNotEuro(invoiceIn.currency.code)">
@ -235,7 +242,16 @@ async function insert() {
v-shortcut="'+'" v-shortcut="'+'"
size="lg" size="lg"
round round
@click="!areRows ? insert() : invoiceInFormRef.insert()" @click="
() => {
if (!areRows) insert();
else
invoiceInFormRef.insert({
amount: (totals.totalTaxableBase - totalAmount).toFixed(2),
invoiceInFk: invoiceId,
});
}
"
/> />
</QPageSticky> </QPageSticky>
</template> </template>

View File

@ -193,7 +193,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
<InvoiceIntoBook> <InvoiceIntoBook>
<template #content="{ book }"> <template #content="{ book }">
<QBtn <QBtn
:label="t('To book')" :label="t('Book')"
color="orange-11" color="orange-11"
text-color="black" text-color="black"
@click="book(entityId)" @click="book(entityId)"
@ -224,10 +224,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
</span> </span>
</template> </template>
</VnLv> </VnLv>
<VnLv <VnLv :label="t('invoiceIn.supplierRef')" :value="entity.supplierRef" />
:label="t('invoiceIn.list.supplierRef')"
:value="entity.supplierRef"
/>
<VnLv <VnLv
:label="t('invoiceIn.summary.currency')" :label="t('invoiceIn.summary.currency')"
:value="entity.currency?.code" :value="entity.currency?.code"
@ -357,7 +354,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
entity.totals.totalTaxableBaseForeignValue && entity.totals.totalTaxableBaseForeignValue &&
toCurrency( toCurrency(
entity.totals.totalTaxableBaseForeignValue, entity.totals.totalTaxableBaseForeignValue,
currency currency,
) )
}}</QTd> }}</QTd>
</QTr> </QTr>
@ -392,7 +389,7 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
entity.totals.totalDueDayForeignValue && entity.totals.totalDueDayForeignValue &&
toCurrency( toCurrency(
entity.totals.totalDueDayForeignValue, entity.totals.totalDueDayForeignValue,
currency currency,
) )
}} }}
</QTd> </QTd>
@ -472,5 +469,5 @@ const getLink = (param) => `#/invoice-in/${entityId.value}/${param}`;
Search invoice: Buscar factura recibida Search invoice: Buscar factura recibida
You can search by invoice reference: Puedes buscar por referencia de la factura You can search by invoice reference: Puedes buscar por referencia de la factura
Totals: Totales Totals: Totales
To book: Contabilizar Book: Contabilizar
</i18n> </i18n>

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref, computed } from 'vue'; import { ref, computed, nextTick } from 'vue';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
@ -25,7 +25,6 @@ const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]); const sageTransactionTypes = ref([]);
const rowsSelected = ref([]); const rowsSelected = ref([]);
const invoiceInFormRef = ref(); const invoiceInFormRef = ref();
const expenseRef = ref();
defineProps({ defineProps({
actionIcon: { actionIcon: {
@ -97,6 +96,20 @@ const columns = computed(() => [
}, },
]); ]);
const taxableBaseTotal = computed(() => {
return getTotal(invoiceInFormRef.value.formData, 'taxableBase');
});
const taxRateTotal = computed(() => {
return getTotal(invoiceInFormRef.value.formData, null, {
cb: taxRate,
});
});
const combinedTotal = computed(() => {
return +taxableBaseTotal.value + +taxRateTotal.value;
});
const filter = { const filter = {
fields: [ fields: [
'id', 'id',
@ -125,7 +138,7 @@ function taxRate(invoiceInTax) {
return ((taxTypeSage / 100) * taxableBase).toFixed(2); return ((taxTypeSage / 100) * taxableBase).toFixed(2);
} }
function autocompleteExpense(evt, row, col) { function autocompleteExpense(evt, row, col, ref) {
const val = evt.target.value; const val = evt.target.value;
if (!val) return; if (!val) return;
@ -134,22 +147,17 @@ function autocompleteExpense(evt, row, col) {
({ id }) => id == useAccountShortToStandard(param), ({ id }) => id == useAccountShortToStandard(param),
); );
expenseRef.value.vnSelectDialogRef.vnSelectRef.toggleOption(lookup); ref.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
} }
const taxableBaseTotal = computed(() => { function setCursor(ref) {
return getTotal(invoiceInFormRef.value.formData, 'taxableBase'); nextTick(() => {
}); const select = ref.vnSelectDialogRef
? ref.vnSelectDialogRef.vnSelectRef
const taxRateTotal = computed(() => { : ref.vnSelectRef;
return getTotal(invoiceInFormRef.value.formData, null, { select.$el.querySelector('input').setSelectionRange(0, 0);
cb: taxRate,
}); });
}); }
const combinedTotal = computed(() => {
return +taxableBaseTotal.value + +taxRateTotal.value;
});
</script> </script>
<template> <template>
<FetchData <FetchData
@ -187,14 +195,24 @@ const combinedTotal = computed(() => {
<template #body-cell-expense="{ row, col }"> <template #body-cell-expense="{ row, col }">
<QTd> <QTd>
<VnSelectDialog <VnSelectDialog
ref="expenseRef" :ref="`expenseRef-${row.$index}`"
v-model="row[col.model]" v-model="row[col.model]"
:options="col.options" :options="col.options"
:option-value="col.optionValue" :option-value="col.optionValue"
:option-label="col.optionLabel" :option-label="col.optionLabel"
:filter-options="['id', 'name']" :filter-options="['id', 'name']"
:tooltip="t('Create a new expense')" :tooltip="t('Create a new expense')"
@keydown.tab="autocompleteExpense($event, row, col)" @keydown.tab="
autocompleteExpense(
$event,
row,
col,
$refs[`expenseRef-${row.$index}`],
)
"
@update:model-value="
setCursor($refs[`expenseRef-${row.$index}`])
"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -210,7 +228,7 @@ const combinedTotal = computed(() => {
</QTd> </QTd>
</template> </template>
<template #body-cell-taxablebase="{ row }"> <template #body-cell-taxablebase="{ row }">
<QTd> <QTd shrink>
<VnInputNumber <VnInputNumber
clear-icon="close" clear-icon="close"
v-model="row.taxableBase" v-model="row.taxableBase"
@ -221,12 +239,16 @@ const combinedTotal = computed(() => {
<template #body-cell-sageiva="{ row, col }"> <template #body-cell-sageiva="{ row, col }">
<QTd> <QTd>
<VnSelect <VnSelect
:ref="`sageivaRef-${row.$index}`"
v-model="row[col.model]" v-model="row[col.model]"
:options="col.options" :options="col.options"
:option-value="col.optionValue" :option-value="col.optionValue"
:option-label="col.optionLabel" :option-label="col.optionLabel"
:filter-options="['id', 'vat']" :filter-options="['id', 'vat']"
data-cy="vat-sageiva" data-cy="vat-sageiva"
@update:model-value="
setCursor($refs[`sageivaRef-${row.$index}`])
"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -244,11 +266,15 @@ const combinedTotal = computed(() => {
<template #body-cell-sagetransaction="{ row, col }"> <template #body-cell-sagetransaction="{ row, col }">
<QTd> <QTd>
<VnSelect <VnSelect
:ref="`sagetransactionRef-${row.$index}`"
v-model="row[col.model]" v-model="row[col.model]"
:options="col.options" :options="col.options"
:option-value="col.optionValue" :option-value="col.optionValue"
:option-label="col.optionLabel" :option-label="col.optionLabel"
:filter-options="['id', 'transaction']" :filter-options="['id', 'transaction']"
@update:model-value="
setCursor($refs[`sagetransactionRef-${row.$index}`])
"
> >
<template #option="scope"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
@ -266,7 +292,7 @@ const combinedTotal = computed(() => {
</QTd> </QTd>
</template> </template>
<template #body-cell-foreignvalue="{ row }"> <template #body-cell-foreignvalue="{ row }">
<QTd> <QTd shrink>
<VnInputNumber <VnInputNumber
:class="{ :class="{
'no-pointer-events': !isNotEuro(currency), 'no-pointer-events': !isNotEuro(currency),

View File

@ -56,7 +56,7 @@ const cols = computed(() => [
{ {
align: 'left', align: 'left',
name: 'supplierRef', name: 'supplierRef',
label: t('invoiceIn.list.supplierRef'), label: t('invoiceIn.supplierRef'),
}, },
{ {
align: 'left', align: 'left',
@ -177,7 +177,7 @@ const cols = computed(() => [
:required="true" :required="true"
/> />
<VnInput <VnInput
:label="t('invoiceIn.list.supplierRef')" :label="t('invoiceIn.supplierRef')"
v-model="data.supplierRef" v-model="data.supplierRef"
/> />
<VnSelect <VnSelect

View File

@ -4,6 +4,7 @@ import { useQuasar } from 'quasar';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnConfirm from 'src/components/ui/VnConfirm.vue'; import VnConfirm from 'src/components/ui/VnConfirm.vue';
import { useArrayData } from 'src/composables/useArrayData'; import { useArrayData } from 'src/composables/useArrayData';
import qs from 'qs';
const { notify, dialog } = useQuasar(); const { notify, dialog } = useQuasar();
const { t } = useI18n(); const { t } = useI18n();
@ -12,29 +13,51 @@ defineExpose({ checkToBook });
const { store } = useArrayData(); const { store } = useArrayData();
async function checkToBook(id) { async function checkToBook(id) {
let directBooking = true; let messages = [];
const hasProblemWithTax = (
await axios.get('InvoiceInTaxes/count', {
params: {
where: JSON.stringify({
invoiceInFk: id,
or: [{ taxTypeSageFk: null }, { transactionTypeSageFk: null }],
}),
},
})
).data?.count;
if (hasProblemWithTax)
messages.push(t('The VAT and Transaction fields have not been informed'));
const { data: totals } = await axios.get(`InvoiceIns/${id}/getTotals`); const { data: totals } = await axios.get(`InvoiceIns/${id}/getTotals`);
const taxableBaseNotEqualDueDay = totals.totalDueDay != totals.totalTaxableBase; const taxableBaseNotEqualDueDay = totals.totalDueDay != totals.totalTaxableBase;
const vatNotEqualDueDay = totals.totalDueDay != totals.totalVat; const vatNotEqualDueDay = totals.totalDueDay != totals.totalVat;
if (taxableBaseNotEqualDueDay && vatNotEqualDueDay) directBooking = false; if (taxableBaseNotEqualDueDay && vatNotEqualDueDay)
messages.push(t('The sum of the taxable bases does not match the due dates'));
const { data: dueDaysCount } = await axios.get('InvoiceInDueDays/count', { const dueDaysCount = (
where: { await axios.get('InvoiceInDueDays/count', {
invoiceInFk: id, params: {
dueDated: { gte: Date.vnNew() }, where: JSON.stringify({
}, invoiceInFk: id,
}); dueDated: { gte: Date.vnNew() },
}),
},
})
).data?.count;
if (dueDaysCount) directBooking = false; if (dueDaysCount) messages.push(t('Some due dates are less than or equal to today'));
if (directBooking) return toBook(id); if (!messages.length) toBook(id);
else
dialog({ dialog({
component: VnConfirm, component: VnConfirm,
componentProps: { title: t('Are you sure you want to book this invoice?') }, componentProps: {
}).onOk(async () => await toBook(id)); title: t('Are you sure you want to book this invoice?'),
message: messages.reduce((acc, msg) => `${acc}<p>${msg}</p>`, ''),
},
}).onOk(() => toBook(id));
} }
async function toBook(id) { async function toBook(id) {
@ -59,4 +82,7 @@ async function toBook(id) {
es: es:
Are you sure you want to book this invoice?: ¿Estás seguro de querer asentar esta factura? Are you sure you want to book this invoice?: ¿Estás seguro de querer asentar esta factura?
It was not able to book the invoice: No se pudo contabilizar la factura It was not able to book the invoice: No se pudo contabilizar la factura
Some due dates are less than or equal to today: Algún vencimiento tiene una fecha menor o igual que hoy
The sum of the taxable bases does not match the due dates: La suma de las bases imponibles no coincide con la de los vencimientos
The VAT and Transaction fields have not been informed: No se han informado los campos de iva y/o transacción
</i18n> </i18n>

View File

@ -3,10 +3,10 @@ invoiceIn:
searchInfo: Search incoming invoices by ID or supplier fiscal name searchInfo: Search incoming invoices by ID or supplier fiscal name
serial: Serial serial: Serial
isBooked: Is booked isBooked: Is booked
supplierRef: Invoice nº
list: list:
ref: Reference ref: Reference
supplier: Supplier supplier: Supplier
supplierRef: Supplier ref.
file: File file: File
issued: Issued issued: Issued
dueDated: Due dated dueDated: Due dated
@ -19,8 +19,6 @@ invoiceIn:
unbook: Unbook unbook: Unbook
delete: Delete delete: Delete
clone: Clone clone: Clone
toBook: To book
toUnbook: To unbook
deleteInvoice: Delete invoice deleteInvoice: Delete invoice
invoiceDeleted: invoice deleted invoiceDeleted: invoice deleted
cloneInvoice: Clone invoice cloneInvoice: Clone invoice
@ -70,4 +68,3 @@ invoiceIn:
isBooked: Is booked isBooked: Is booked
account: Ledger account account: Ledger account
correctingFk: Rectificative correctingFk: Rectificative

View File

@ -3,10 +3,10 @@ invoiceIn:
searchInfo: Buscar facturas recibidas por ID o nombre fiscal del proveedor searchInfo: Buscar facturas recibidas por ID o nombre fiscal del proveedor
serial: Serie serial: Serie
isBooked: Contabilizada isBooked: Contabilizada
supplierRef: Nº factura
list: list:
ref: Referencia ref: Referencia
supplier: Proveedor supplier: Proveedor
supplierRef: Ref. proveedor
issued: F. emisión issued: F. emisión
dueDated: F. vencimiento dueDated: F. vencimiento
file: Fichero file: Fichero
@ -15,12 +15,10 @@ invoiceIn:
descriptor: descriptor:
ticketList: Listado de tickets ticketList: Listado de tickets
descriptorMenu: descriptorMenu:
book: Asentar book: Contabilizar
unbook: Desasentar unbook: Descontabilizar
delete: Eliminar delete: Eliminar
clone: Clonar clone: Clonar
toBook: Contabilizar
toUnbook: Descontabilizar
deleteInvoice: Eliminar factura deleteInvoice: Eliminar factura
invoiceDeleted: Factura eliminada invoiceDeleted: Factura eliminada
cloneInvoice: Clonar factura cloneInvoice: Clonar factura
@ -68,4 +66,3 @@ invoiceIn:
isBooked: Contabilizada isBooked: Contabilizada
account: Cuenta contable account: Cuenta contable
correctingFk: Rectificativa correctingFk: Rectificativa

View File

@ -97,12 +97,19 @@ const columns = computed(() => [
}, },
{ {
align: 'left', align: 'left',
name: 'companyCode', name: 'companyFk',
label: t('globals.company'), label: t('globals.company'),
cardVisible: true, cardVisible: true,
component: 'select', component: 'select',
attrs: { url: 'Companies', optionLabel: 'code', optionValue: 'id' }, attrs: {
columnField: { component: null }, url: 'Companies',
optionLabel: 'code',
optionValue: 'id',
},
columnField: {
component: null,
},
format: (row, dashIfEmpty) => dashIfEmpty(row.companyCode),
}, },
{ {
align: 'left', 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 VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import FilterItemForm from 'src/components/FilterItemForm.vue'; import FilterItemForm from 'src/components/FilterItemForm.vue';
import CreateIntrastatForm from './CreateIntrastatForm.vue'; import CreateIntrastatForm from './CreateIntrastatForm.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
const route = useRoute(); const route = useRoute();
const { t } = useI18n(); const { t } = useI18n();
@ -208,30 +209,20 @@ const onIntrastatCreated = (response, formData) => {
/> />
</VnRow> </VnRow>
<VnRow class="row q-gutter-md q-mb-md"> <VnRow class="row q-gutter-md q-mb-md">
<div> <VnCheckbox
<QCheckbox v-model="data.isFragile"
v-model="data.isFragile" :label="t('item.basicData.isFragile')"
:label="t('item.basicData.isFragile')" :info="t('item.basicData.isFragileTooltip')"
class="q-mr-sm" class="q-mr-sm"
/> size="xs"
<QIcon name="info" class="cursor-pointer" size="xs"> />
<QTooltip max-width="300px"> <VnCheckbox
{{ t('item.basicData.isFragileTooltip') }} v-model="data.isPhotoRequested"
</QTooltip> :label="t('item.basicData.isPhotoRequested')"
</QIcon> :info="t('item.basicData.isPhotoRequestedTooltip')"
</div> class="q-mr-sm"
<div> size="xs"
<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>
</VnRow> </VnRow>
<VnRow> <VnRow>
<VnInput <VnInput

View File

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

View File

@ -65,10 +65,19 @@ const columns = computed(() => [
name: 'name', name: 'name',
...defaultColumnAttrs, ...defaultColumnAttrs,
create: true, create: true,
columnFilter: {
component: 'select',
attrs: {
url: 'Items',
fields: ['id', 'name', 'subName'],
optionLabel: 'name',
optionValue: 'name',
uppercase: false,
},
},
}, },
{ {
label: t('item.fixedPrice.groupingPrice'), label: t('item.fixedPrice.groupingPrice'),
field: 'rate2',
name: 'rate2', name: 'rate2',
...defaultColumnAttrs, ...defaultColumnAttrs,
component: 'input', component: 'input',
@ -76,7 +85,6 @@ const columns = computed(() => [
}, },
{ {
label: t('item.fixedPrice.packingPrice'), label: t('item.fixedPrice.packingPrice'),
field: 'rate3',
name: 'rate3', name: 'rate3',
...defaultColumnAttrs, ...defaultColumnAttrs,
component: 'input', component: 'input',
@ -85,7 +93,6 @@ const columns = computed(() => [
{ {
label: t('item.fixedPrice.minPrice'), label: t('item.fixedPrice.minPrice'),
field: 'minPrice',
name: 'minPrice', name: 'minPrice',
...defaultColumnAttrs, ...defaultColumnAttrs,
component: 'input', component: 'input',
@ -108,7 +115,6 @@ const columns = computed(() => [
}, },
{ {
label: t('item.fixedPrice.ended'), label: t('item.fixedPrice.ended'),
field: 'ended',
name: 'ended', name: 'ended',
...defaultColumnAttrs, ...defaultColumnAttrs,
columnField: { columnField: {
@ -124,7 +130,6 @@ const columns = computed(() => [
{ {
label: t('globals.warehouse'), label: t('globals.warehouse'),
field: 'warehouseFk',
name: 'warehouseFk', name: 'warehouseFk',
...defaultColumnAttrs, ...defaultColumnAttrs,
columnClass: 'shrink', columnClass: 'shrink',
@ -415,7 +420,6 @@ function handleOnDataSave({ CrudModelRef }) {
'row-key': 'id', 'row-key': 'id',
selection: 'multiple', selection: 'multiple',
}" }"
:use-model="true"
v-model:selected="rowsSelected" v-model:selected="rowsSelected"
:create-as-dialog="false" :create-as-dialog="false"
:create="{ :create="{

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. origin: Orig.
userName: Buyer userName: Buyer
weight: Weight weight: Weight
color: Color
weightByPiece: Weight/stem weightByPiece: Weight/stem
stemMultiplier: Multiplier stemMultiplier: Multiplier
producer: Producer producer: Producer
@ -215,4 +216,24 @@ item:
specie: Specie specie: Specie
search: 'Search item' search: 'Search item'
searchInfo: 'You can search by id' 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 size: Medida
origin: Orig. origin: Orig.
weight: Peso weight: Peso
color: Color
weightByPiece: Peso/tallo weightByPiece: Peso/tallo
userName: Comprador userName: Comprador
stemMultiplier: Multiplicador stemMultiplier: Multiplicador
@ -220,5 +221,30 @@ item:
achieved: 'Conseguido' achieved: 'Conseguido'
concept: 'Concepto' concept: 'Concepto'
state: 'Estado' state: 'Estado'
search: 'Buscar artículo' itemProposal: Artículos similares
searchInfo: 'Puedes buscar por id' 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

@ -43,10 +43,9 @@ const addToOrder = async () => {
); );
state.set('orderTotal', orderTotal); state.set('orderTotal', orderTotal);
const rows = orderData.value.rows.push(...items) || [];
state.set('Order', { state.set('Order', {
...orderData.value, ...orderData.value,
rows, items,
}); });
notify(t('globals.dataSaved'), 'positive'); notify(t('globals.dataSaved'), 'positive');
emit('added', -totalQuantity(items)); emit('added', -totalQuantity(items));

View File

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

View File

@ -6,7 +6,7 @@ import VehicleFilter from '../VehicleFilter.js';
<template> <template>
<VnCardBeta <VnCardBeta
data-key="Vehicle" data-key="Vehicle"
base-url="Vehicles" url="Vehicles"
:filter="VehicleFilter" :filter="VehicleFilter"
:descriptor="VehicleDescriptor" :descriptor="VehicleDescriptor"
/> />

View File

@ -136,7 +136,7 @@ const columns = computed(() => [
/> />
<FetchData <FetchData
url="Countries" url="Countries"
:filter="{ fields: ['code'] }" :filter="{ fields: ['name', 'code'] }"
@on-fetch="(data) => (countries = data)" @on-fetch="(data) => (countries = data)"
auto-load auto-load
/> />
@ -209,7 +209,7 @@ const columns = computed(() => [
v-model="data.countryCodeFk" v-model="data.countryCodeFk"
:label="$t('globals.country')" :label="$t('globals.country')"
option-value="code" option-value="code"
option-label="code" option-label="name"
:options="countries" :options="countries"
/> />
<VnInput <VnInput

View File

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

View File

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

View File

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

@ -376,6 +376,30 @@ onMounted(async () => {
</template> </template>
<template #body-cell-problems="{ row }"> <template #body-cell-problems="{ row }">
<QTd class="q-gutter-x-xs"> <QTd class="q-gutter-x-xs">
<QIcon
v-if="row.futureAgencyFk !== row.agencyFk && row.agencyFk"
color="primary"
name="vn:agency-term"
size="xs"
class="q-mr-xs"
>
<QTooltip class="column">
<span>
{{
t('advanceTickets.originAgency', {
agency: row.futureAgency,
})
}}
</span>
<span>
{{
t('advanceTickets.destinationAgency', {
agency: row.agency,
})
}}
</span>
</QTooltip>
</QIcon>
<QIcon <QIcon
v-if="row.isTaxDataChecked === 0" v-if="row.isTaxDataChecked === 0"
color="primary" color="primary"

View File

@ -23,6 +23,8 @@ ticketSale:
hasComponentLack: Component lack hasComponentLack: Component lack
ok: Ok ok: Ok
more: More more: More
transferLines: Transfer lines(no basket)/ Split
transferBasket: Some row selected is basket
advanceTickets: advanceTickets:
preparation: Preparation preparation: Preparation
origin: Origin origin: Origin
@ -188,7 +190,6 @@ ticketList:
accountPayment: Account payment accountPayment: Account payment
sendDocuware: Set delivered and send delivery note(s) to the tablet sendDocuware: Set delivered and send delivery note(s) to the tablet
addPayment: Add payment addPayment: Add payment
date: Date
company: Company company: Company
amount: Amount amount: Amount
reference: Reference reference: Reference
@ -202,8 +203,6 @@ ticketList:
creditCard: Credit card creditCard: Credit card
transfers: Transfers transfers: Transfers
province: Province province: Province
warehouse: Warehouse
hour: Hour
closure: Closure closure: Closure
toLines: Go to lines toLines: Go to lines
addressNickname: Address nickname addressNickname: Address nickname
@ -214,3 +213,79 @@ ticketList:
notVisible: Not visible notVisible: Not visible
clientFrozen: Client frozen clientFrozen: Client frozen
componentLack: Component lack 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 ok: Ok
more: Más more: Más
address: Consignatario address: Consignatario
transferLines: Transferir líneas(no cesta)/ Separar
transferBasket: No disponible para una cesta
size: Medida size: Medida
ticketComponents: ticketComponents:
serie: Serie serie: Serie
@ -213,6 +215,81 @@ ticketList:
toLines: Ir a lineas toLines: Ir a lineas
addressNickname: Alias consignatario addressNickname: Alias consignatario
ref: Referencia 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 rounding: Redondeo
noVerifiedData: Sin datos comprobados noVerifiedData: Sin datos comprobados
purchaseRequest: Petición de compra purchaseRequest: Petición de compra

View File

@ -10,7 +10,7 @@ import axios from 'axios';
import VnImg from 'src/components/ui/VnImg.vue'; import VnImg from 'src/components/ui/VnImg.vue';
import EditPictureForm from 'components/EditPictureForm.vue'; import EditPictureForm from 'components/EditPictureForm.vue';
import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue'; import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -53,6 +53,7 @@ const handlePhotoUpdated = (evt = false) => {
module="Worker" module="Worker"
:data-key="dataKey" :data-key="dataKey"
url="Workers/summary" url="Workers/summary"
:filter="{ where: { id: entityId } }"
title="user.nickname" title="user.nickname"
@on-fetch="getIsExcluded" @on-fetch="getIsExcluded"
> >

View File

@ -9,7 +9,7 @@ import CardSummary from 'components/ui/CardSummary.vue';
import VnUserLink from 'src/components/ui/VnUserLink.vue'; import VnUserLink from 'src/components/ui/VnUserLink.vue';
import VnTitle from 'src/components/common/VnTitle.vue'; import VnTitle from 'src/components/common/VnTitle.vue';
import RoleDescriptorProxy from 'src/pages/Account/Role/Card/RoleDescriptorProxy.vue'; import RoleDescriptorProxy from 'src/pages/Account/Role/Card/RoleDescriptorProxy.vue';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
import { useAdvancedSummary } from 'src/composables/useAdvancedSummary'; import { useAdvancedSummary } from 'src/composables/useAdvancedSummary';
import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue'; import WorkerDescriptorMenu from './WorkerDescriptorMenu.vue';

View File

@ -1,6 +1,6 @@
<script setup> <script setup>
import VnCardBeta from 'components/common/VnCardBeta.vue'; import VnCardBeta from 'components/common/VnCardBeta.vue';
import DepartmentDescriptor from 'pages/Department/Card/DepartmentDescriptor.vue'; import DepartmentDescriptor from 'pages/Worker/Department/Card/DepartmentDescriptor.vue';
</script> </script>
<template> <template>
<VnCardBeta <VnCardBeta

View File

@ -3,7 +3,7 @@ import { onMounted, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { useState } from 'src/composables/useState'; import { useState } from 'src/composables/useState';
import { useQuasar } from 'quasar'; import { useQuasar } from 'quasar';
import DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue'; import DepartmentDescriptorProxy from 'src/pages/Worker/Department/Card/DepartmentDescriptorProxy.vue';
import CreateDepartmentChild from './CreateDepartmentChild.vue'; import CreateDepartmentChild from './CreateDepartmentChild.vue';
import axios from 'axios'; import axios from 'axios';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';

View File

@ -1,5 +1,7 @@
<script setup> <script setup>
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import { computed, ref } from 'vue';
import FetchData from 'components/FetchData.vue';
import FormModel from 'src/components/FormModel.vue'; import FormModel from 'src/components/FormModel.vue';
import VnRow from 'components/ui/VnRow.vue'; import VnRow from 'components/ui/VnRow.vue';
import VnInput from 'src/components/common/VnInput.vue'; import VnInput from 'src/components/common/VnInput.vue';
@ -7,10 +9,33 @@ import VnInputTime from 'src/components/common/VnInputTime.vue';
import VnSelect from 'src/components/common/VnSelect.vue'; import VnSelect from 'src/components/common/VnSelect.vue';
const { t } = useI18n(); const { t } = useI18n();
const agencyFilter = {
fields: ['id', 'name'],
order: 'name ASC',
limit: 30,
};
const agencyOptions = ref([]);
const validAddresses = ref([]);
const filterWhere = computed(() => ({
id: { inq: validAddresses.value.map((item) => item.addressFk) },
}));
</script> </script>
<template> <template>
<FormModel :url="`Zones/${$route.params.id}`" auto-load model="zone"> <FetchData
:filter="agencyFilter"
@on-fetch="(data) => (agencyOptions = data)"
auto-load
url="AgencyModes/isActive"
/>
<FetchData
url="RoadmapAddresses"
auto-load
@on-fetch="(data) => (validAddresses = data)"
/>
<FormModel :url="`Zones/${route.params.id}`" auto-load model="zone">
<template #form="{ data, validate }"> <template #form="{ data, validate }">
<VnRow> <VnRow>
<VnInput <VnInput
@ -109,6 +134,8 @@ const { t } = useI18n();
hide-selected hide-selected
map-options map-options
:rules="validate('data.addressFk')" :rules="validate('data.addressFk')"
:filter-options="['id']"
:where="filterWhere"
/> />
</VnRow> </VnRow>
<VnRow> <VnRow>

View File

@ -22,15 +22,50 @@ const exprBuilder = (param, value) => {
return /^\d+$/.test(value) ? { id: value } : { name: { like: `%${value}%` } }; return /^\d+$/.test(value) ? { id: value } : { name: { like: `%${value}%` } };
} }
}; };
const tableFilter = {
include: [
{
relation: 'agencyMode',
scope: {
fields: ['id', 'name'],
},
},
{
relation: 'address',
scope: {
fields: ['id', 'nickname', 'provinceFk', 'postalCode'],
include: [
{
relation: 'province',
scope: {
fields: ['id', 'name'],
},
},
{
relation: 'postcode',
scope: {
fields: ['code', 'townFk'],
include: {
relation: 'town',
scope: {
fields: ['id', 'name'],
},
},
},
},
],
},
},
],
};
</script> </script>
<template> <template>
<VnSearchbar <VnSearchbar
data-key="ZonesList" data-key="ZonesList"
url="Zones" url="Zones"
:filter="{ :filter="tableFilter"
include: { relation: 'agencyMode', scope: { fields: ['name'] } },
}"
:expr-builder="exprBuilder" :expr-builder="exprBuilder"
:label="t('list.searchZone')" :label="t('list.searchZone')"
:info="t('list.searchInfo')" :info="t('list.searchInfo')"

View File

@ -4,7 +4,7 @@ import { useRouter } from 'vue-router';
import { computed, ref } from 'vue'; import { computed, ref } from 'vue';
import axios from 'axios'; import axios from 'axios';
import { toCurrency } from 'src/filters'; import { dashIfEmpty, toCurrency } from 'src/filters';
import { toTimeFormat } from 'src/filters/date'; import { toTimeFormat } from 'src/filters/date';
import { useVnConfirm } from 'composables/useVnConfirm'; import { useVnConfirm } from 'composables/useVnConfirm';
import useNotify from 'src/composables/useNotify.js'; import useNotify from 'src/composables/useNotify.js';
@ -17,7 +17,6 @@ import VnInputTime from 'src/components/common/VnInputTime.vue';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import ZoneFilterPanel from './ZoneFilterPanel.vue'; import ZoneFilterPanel from './ZoneFilterPanel.vue';
import ZoneSearchbar from './Card/ZoneSearchbar.vue'; import ZoneSearchbar from './Card/ZoneSearchbar.vue';
import FetchData from 'src/components/FetchData.vue';
const { t } = useI18n(); const { t } = useI18n();
const router = useRouter(); const router = useRouter();
@ -26,7 +25,6 @@ const { viewSummary } = useSummaryDialog();
const { openConfirmationModal } = useVnConfirm(); const { openConfirmationModal } = useVnConfirm();
const tableRef = ref(); const tableRef = ref();
const warehouseOptions = ref([]); const warehouseOptions = ref([]);
const validAddresses = ref([]);
const tableFilter = { const tableFilter = {
include: [ include: [
@ -161,30 +159,18 @@ const handleClone = (id) => {
openConfirmationModal( openConfirmationModal(
t('list.confirmCloneTitle'), t('list.confirmCloneTitle'),
t('list.confirmCloneSubtitle'), t('list.confirmCloneSubtitle'),
() => clone(id) () => clone(id),
); );
}; };
function showValidAddresses(row) { function formatRow(row) {
if (row.addressFk) { if (!row?.address) return '-';
const isValid = validAddresses.value.some( return dashIfEmpty(`${row?.address?.nickname},
(address) => address.addressFk === row.addressFk ${row?.address?.postcode?.town?.name} (${row?.address?.province?.name})`);
);
if (isValid)
return `${row.address?.nickname},
${row.address?.postcode?.town?.name} (${row.address?.province?.name})`;
else return '-';
}
return '-';
} }
</script> </script>
<template> <template>
<FetchData
url="RoadmapAddresses"
auto-load
@on-fetch="(data) => (validAddresses = data)"
/>
<ZoneSearchbar /> <ZoneSearchbar />
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
@ -207,7 +193,7 @@ function showValidAddresses(row) {
:right-search="false" :right-search="false"
> >
<template #column-addressFk="{ row }"> <template #column-addressFk="{ row }">
{{ showValidAddresses(row) }} {{ dashIfEmpty(formatRow(row)) }}
</template> </template>
<template #more-create-dialog="{ data }"> <template #more-create-dialog="{ data }">
<VnSelect <VnSelect

View File

@ -107,7 +107,10 @@ export default defineRouter(function (/* { store, ssrContext } */) {
'Failed to fetch dynamically imported module', 'Failed to fetch dynamically imported module',
'Importing a module script failed', 'Importing a module script failed',
]; ];
state.set('error', errorMessages.some(message.includes)); state.set(
'error',
errorMessages.some((error) => message.startsWith(error)),
);
}); });
return Router; return Router;
}); });

Some files were not shown because too many files have changed in this diff Show More