Merge branch 'dev' of https://gitea.verdnatura.es/verdnatura/salix-front into 8201-DescriptorIcons

This commit is contained in:
Jon Elias 2024-12-16 13:23:35 +01:00
commit 7fc0d0bcfe
28 changed files with 440 additions and 174 deletions

View File

@ -0,0 +1,31 @@
<script setup>
import { toDateFormat } from 'src/filters/date.js';
defineProps({ date: { type: [Date, String], required: true } });
function getBadgeAttrs(date) {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let timeDiff = today - timeTicket;
if (timeDiff == 0) return { color: 'warning', 'text-color': 'black' };
if (timeDiff < 0) return { color: 'success', 'text-color': 'black' };
return { color: 'transparent', 'text-color': 'white' };
}
function formatShippedDate(date) {
if (!date) return '-';
const dateSplit = date.split('T');
const [year, month, day] = dateSplit[0].split('-');
const newDate = new Date(year, month - 1, day);
return toDateFormat(newDate);
}
</script>
<template>
<QBadge v-bind="getBadgeAttrs(date)" class="q-pa-sm" style="font-size: 14px">
{{ formatShippedDate(date) }}
</QBadge>
</template>

View File

@ -222,8 +222,8 @@ const toModule = computed(() =>
/> />
</template> </template>
<style lang="scss"> <style lang="scss" scoped>
.body { :deep(.body) {
background-color: var(--vn-section-color); background-color: var(--vn-section-color);
.text-h5 { .text-h5 {
font-size: 20px; font-size: 20px;
@ -262,9 +262,7 @@ const toModule = computed(() =>
} }
} }
} }
</style>
<style lang="scss" scoped>
.title { .title {
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@ -1,5 +1,5 @@
<script setup> <script setup>
import { ref } from 'vue'; import { ref, toRef } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import VnLv from 'components/ui/VnLv.vue'; import VnLv from 'components/ui/VnLv.vue';
@ -13,7 +13,7 @@ const DEFAULT_PRICE_KG = 0;
const { t } = useI18n(); const { t } = useI18n();
defineProps({ const props = defineProps({
item: { item: {
type: Object, type: Object,
required: true, required: true,
@ -25,57 +25,63 @@ defineProps({
}); });
const dialog = ref(null); const dialog = ref(null);
const card = toRef(props, 'item');
</script> </script>
<template> <template>
<div class="container order-catalog-item overflow-hidden"> <div class="container order-catalog-item overflow-hidden">
<QCard class="card shadow-6"> <QCard class="card shadow-6">
<div class="img-wrapper"> <div class="img-wrapper">
<VnImg :id="item.id" class="image" zoom-resolution="1600x900" /> <VnImg :id="card.id" class="image" zoom-resolution="1600x900" />
<div v-if="item.hex && isCatalog" class="item-color-container"> <div v-if="card.hex && isCatalog" class="item-color-container">
<div <div
class="item-color" class="item-color"
:style="{ backgroundColor: `#${item.hex}` }" :style="{ backgroundColor: `#${card.hex}` }"
></div> ></div>
</div> </div>
</div> </div>
<div class="content"> <div class="content">
<span class="link"> <span class="link">
{{ item.name }} {{ card.name }}
<ItemDescriptorProxy :id="item.id" /> <ItemDescriptorProxy :id="card.id" />
</span> </span>
<p class="subName">{{ item.subName }}</p> <p class="subName">{{ card.subName }}</p>
<template v-for="index in 4" :key="`tag-${index}`"> <template v-for="index in 4" :key="`tag-${index}`">
<VnLv <VnLv
v-if="item?.[`tag${index + 4}`]" v-if="card?.[`tag${index + 4}`]"
:label="item?.[`tag${index + 4}`] + ':'" :label="card?.[`tag${index + 4}`] + ':'"
:value="item?.[`value${index + 4}`]" :value="card?.[`value${index + 4}`]"
/> />
</template> </template>
<div v-if="item.minQuantity" class="min-quantity"> <div v-if="card.minQuantity" class="min-quantity">
<QIcon name="production_quantity_limits" size="xs" /> <QIcon name="production_quantity_limits" size="xs" />
{{ item.minQuantity }} {{ card.minQuantity }}
</div> </div>
<div class="footer"> <div class="footer">
<div class="price"> <div class="price">
<p v-if="isCatalog"> <p v-if="isCatalog">
{{ item.available }} {{ t('to') }} {{ card.available }} {{ t('to') }}
{{ toCurrency(item.price) }} {{ toCurrency(card.price) }}
</p> </p>
<slot name="price" /> <slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon"> <QIcon v-if="isCatalog" name="add_circle" class="icon">
<QTooltip>{{ t('globals.add') }}</QTooltip> <QTooltip>{{ t('globals.add') }}</QTooltip>
<QPopupProxy ref="dialog"> <QPopupProxy ref="dialog">
<OrderCatalogItemDialog <OrderCatalogItemDialog
:item="item" :item="card"
@added="() => dialog.hide()" @added="
(quantityAdded) => {
card.available += quantityAdded;
dialog.hide();
}
"
/> />
</QPopupProxy> </QPopupProxy>
</QIcon> </QIcon>
</div> </div>
<p v-if="item.priceKg" class="price-kg"> <p v-if="card.priceKg" class="price-kg">
{{ t('price-kg') }} {{ t('price-kg') }}
{{ toCurrency(item.priceKg) || DEFAULT_PRICE_KG }} {{ toCurrency(card.priceKg) || DEFAULT_PRICE_KG }}
</p> </p>
</div> </div>
</div> </div>

View File

@ -129,6 +129,7 @@ globals:
small: Small small: Small
medium: Medium medium: Medium
big: Big big: Big
email: Email
pageTitles: pageTitles:
logIn: Login logIn: Login
addressEdit: Update address addressEdit: Update address
@ -329,6 +330,7 @@ globals:
email: Email email: Email
SSN: SSN SSN: SSN
fi: FI fi: FI
packing: ITP
myTeam: My team myTeam: My team
departmentFk: Department departmentFk: Department
countryFk: Country countryFk: Country

View File

@ -131,6 +131,7 @@ globals:
small: Pequeño/a small: Pequeño/a
medium: Mediano/a medium: Mediano/a
big: Grande big: Grande
email: Correo
pageTitles: pageTitles:
logIn: Inicio de sesión logIn: Inicio de sesión
addressEdit: Modificar consignatario addressEdit: Modificar consignatario
@ -335,6 +336,7 @@ globals:
SSN: NSS SSN: NSS
fi: NIF fi: NIF
myTeam: Mi equipo myTeam: Mi equipo
packing: ITP
countryFk: País countryFk: País
changePass: Cambiar contraseña changePass: Cambiar contraseña
deleteConfirmTitle: Eliminar los elementos seleccionados deleteConfirmTitle: Eliminar los elementos seleccionados
@ -497,7 +499,7 @@ invoiceOut:
ticketList: Listado de tickets ticketList: Listado de tickets
summary: summary:
issued: Fecha issued: Fecha
dued: Vencimiento dued: Fecha límite
booked: Contabilizada booked: Contabilizada
taxBreakdown: Desglose impositivo taxBreakdown: Desglose impositivo
taxableBase: Base imp. taxableBase: Base imp.

View File

@ -6,6 +6,7 @@ import CrudModel from 'components/CrudModel.vue';
import FetchData from 'components/FetchData.vue'; import FetchData from 'components/FetchData.vue';
import VnSelect from 'components/common/VnSelect.vue'; import VnSelect from 'components/common/VnSelect.vue';
import { tMobile } from 'composables/tMobile'; import { tMobile } from 'composables/tMobile';
import VnSelectWorker from 'src/components/common/VnSelectWorker.vue';
const route = useRoute(); const route = useRoute();
@ -157,19 +158,14 @@ const columns = computed(() => [
auto-width auto-width
@keyup.ctrl.enter.stop="claimDevelopmentForm.saveChanges()" @keyup.ctrl.enter.stop="claimDevelopmentForm.saveChanges()"
> >
<VnSelect <VnSelectWorker
v-if="col.name == 'worker'"
v-model="row[col.model]" v-model="row[col.model]"
:url="col.url"
:where="col.where"
:sort-by="col.sortBy"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:autofocus="col.tabIndex == 1" :autofocus="col.tabIndex == 1"
input-debounce="0" input-debounce="0"
hide-selected hide-selected
> >
<template #option="scope" v-if="col.name == 'worker'"> <template #option="scope">
<QItem v-bind="scope.itemProps"> <QItem v-bind="scope.itemProps">
<QItemSection> <QItemSection>
<QItemLabel>{{ scope.opt?.name }}</QItemLabel> <QItemLabel>{{ scope.opt?.name }}</QItemLabel>
@ -180,7 +176,20 @@ const columns = computed(() => [
</QItemSection> </QItemSection>
</QItem> </QItem>
</template> </template>
</VnSelect> </VnSelectWorker>
<VnSelect
v-else
v-model="row[col.model]"
:url="col.url"
:where="col.where"
:sort-by="col.sortBy"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:autofocus="col.tabIndex == 1"
input-debounce="0"
hide-selected
/>
</QTd> </QTd>
</template> </template>
<template #item="props"> <template #item="props">

View File

@ -120,13 +120,13 @@ const developmentColumns = ref([
{ {
name: 'claimReason', name: 'claimReason',
label: 'claim.reason', label: 'claim.reason',
field: (row) => row.claimReason.description, field: (row) => row.claimReason?.description,
sortable: true, sortable: true,
}, },
{ {
name: 'claimResult', name: 'claimResult',
label: 'claim.result', label: 'claim.result',
field: (row) => row.claimResult.description, field: (row) => row.claimResult?.description,
sortable: true, sortable: true,
}, },
{ {

View File

@ -12,6 +12,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 VnSelectDialog from 'src/components/common/VnSelectDialog.vue'; import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue'; import CustomerNewCustomsAgent from 'src/pages/Customer/components/CustomerNewCustomsAgent.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
@ -144,7 +145,6 @@ function handleLocation(data, location) {
:url="`Addresses/${route.params.addressId}`" :url="`Addresses/${route.params.addressId}`"
@on-data-saved="onDataSaved()" @on-data-saved="onDataSaved()"
auto-load auto-load
model="customer"
> >
<template #moreActions> <template #moreActions>
<QBtn <QBtn
@ -220,31 +220,35 @@ function handleLocation(data, location) {
</div> </div>
</VnRow> </VnRow>
<VnRow> <VnRow>
<div class="col"> <VnSelect
<VnSelect :label="t('Incoterms')"
:label="t('Incoterms')" :options="incoterms"
:options="incoterms" hide-selected
hide-selected option-label="name"
option-label="name" option-value="code"
option-value="code" v-model="data.incotermsFk"
v-model="data.incotermsFk" />
/> <VnSelectDialog
</div> :label="t('Customs agent')"
<div class="col"> :options="customsAgents"
<VnSelectDialog hide-selected
:label="t('Customs agent')" option-label="fiscalName"
:options="customsAgents" option-value="id"
hide-selected v-model="data.customsAgentFk"
option-label="fiscalName" :tooltip="t('New customs agent')"
option-value="id" >
v-model="data.customsAgentFk" <template #form>
:tooltip="t('New customs agent')" <CustomerNewCustomsAgent />
> </template>
<template #form> </VnSelectDialog>
<CustomerNewCustomsAgent /> </VnRow>
</template> <VnRow>
</VnSelectDialog> <VnInputNumber
</div> :label="t('Longitude')"
clearable
v-model="data.longitude"
/>
<VnInputNumber :label="t('Latitude')" clearable v-model="data.latitude" />
</VnRow> </VnRow>
<h4 class="q-mb-xs">{{ t('Notes') }}</h4> <h4 class="q-mb-xs">{{ t('Notes') }}</h4>
<VnRow <VnRow
@ -322,4 +326,6 @@ es:
Description: Descripción Description: Descripción
Add note: Añadir nota Add note: Añadir nota
Remove note: Eliminar nota Remove note: Eliminar nota
Longitude: Longitud
Latitude: Latitud
</i18n> </i18n>

View File

@ -83,7 +83,7 @@ const { openConfirmationModal } = useVnConfirm();
</template> </template>
<template #body="{ entity }"> <template #body="{ entity }">
<VnLv :label="t('department.chat')" :value="entity.chatName" /> <VnLv :label="t('department.chat')" :value="entity.chatName" />
<VnLv :label="t('department.email')" :value="entity.notificationEmail" copy /> <VnLv :label="t('globals.email')" :value="entity.notificationEmail" copy />
<VnLv <VnLv
:label="t('department.selfConsumptionCustomer')" :label="t('department.selfConsumptionCustomer')"
:value="entity.client?.name" :value="entity.client?.name"

View File

@ -58,7 +58,7 @@ onMounted(async () => {
dash dash
/> />
<VnLv <VnLv
:label="t('department.email')" :label="t('globals.email')"
:value="department.notificationEmail" :value="department.notificationEmail"
dash dash
/> />

View File

@ -16,7 +16,7 @@ import { cloneItem } from 'src/pages/Item/composables/cloneItem';
const $props = defineProps({ const $props = defineProps({
id: { id: {
type: Number, type: [Number, String],
required: false, required: false,
default: null, default: null,
}, },
@ -29,7 +29,7 @@ const $props = defineProps({
default: null, default: null,
}, },
saleFk: { saleFk: {
type: Number, type: [Number, String],
default: null, default: null,
}, },
warehouseFk: { warehouseFk: {
@ -61,7 +61,7 @@ onMounted(async () => {
const data = ref(useCardDescription()); const data = ref(useCardDescription());
const setData = async (entity) => { const setData = async (entity) => {
if (!entity) return; if (!entity) return;
data.value = useCardDescription(entity.name, entity.id); data.value = useCardDescription(entity?.name, entity?.id);
await updateStock(); await updateStock();
}; };

View File

@ -16,7 +16,7 @@ const $props = defineProps({
default: null, default: null,
}, },
entityId: { entityId: {
type: String, type: [String, Number],
default: null, default: null,
}, },
showEditButton: { showEditButton: {

View File

@ -5,14 +5,26 @@ import { useRoute } from 'vue-router';
import { dateRange } from 'src/filters'; import { dateRange } from 'src/filters';
import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue'; import EntryDescriptorProxy from 'src/pages/Entry/Card/EntryDescriptorProxy.vue';
import VnInputDate from 'src/components/common/VnInputDate.vue'; import VnInputDate from 'src/components/common/VnInputDate.vue';
import { toDateTimeFormat } from 'src/filters/date.js'; import VnDateBadge from 'src/components/common/VnDateBadge.vue';
import { dashIfEmpty } from 'src/filters'; import { dashIfEmpty } from 'src/filters';
import { toCurrency } from 'filters/index'; import { toCurrency } from 'filters/index';
import { useArrayData } from 'composables/useArrayData'; import { useArrayData } from 'composables/useArrayData';
import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue'; import VnSubToolbar from 'src/components/ui/VnSubToolbar.vue';
import axios from 'axios';
import SupplierDescriptorProxy from 'src/pages/Supplier/Card/SupplierDescriptorProxy.vue';
const { t } = useI18n(); const { t } = useI18n();
const route = useRoute(); const route = useRoute();
const from = ref();
const to = ref();
const hideInventory = ref(true);
const inventorySupplierFk = ref();
async function getInventorySupplier() {
inventorySupplierFk.value = (
await axios.get(`InventoryConfigs`)
)?.data[0]?.supplierFk;
}
const exprBuilder = (param, value) => { const exprBuilder = (param, value) => {
switch (param) { switch (param) {
@ -33,25 +45,27 @@ const exprBuilder = (param, value) => {
} }
}; };
const from = ref(); const where = {
const to = ref(); itemFk: route.params.id,
};
if (hideInventory.value) {
where.supplierFk = { neq: inventorySupplierFk };
}
const arrayData = useArrayData('ItemLastEntries', { const arrayData = useArrayData('ItemLastEntries', {
url: 'Items/lastEntriesFilter', url: 'Items/lastEntriesFilter',
order: ['landed DESC', 'buyFk DESC'], order: ['landed DESC', 'buyFk DESC'],
exprBuilder: exprBuilder, exprBuilder: exprBuilder,
userFilter: { userFilter: {
where: { where: where,
itemFk: route.params.id,
},
}, },
}); });
const itemLastEntries = ref([]); const itemLastEntries = ref([]);
const columns = computed(() => [ const columns = computed(() => [
{ {
label: t('lastEntries.ig'), label: 'Nv',
name: 'ig', name: 'ig',
align: 'center', align: 'center',
}, },
@ -59,33 +73,38 @@ const columns = computed(() => [
label: t('itemDiary.warehouse'), label: t('itemDiary.warehouse'),
name: 'warehouse', name: 'warehouse',
field: 'warehouse', field: 'warehouse',
align: 'left', align: 'center',
}, },
{ {
label: t('lastEntries.landed'), label: t('lastEntries.landed'),
name: 'id', name: 'date',
field: 'landed', field: 'landed',
align: 'left', align: 'center',
format: (val) => toDateTimeFormat(val),
}, },
{ {
label: t('lastEntries.entry'), label: t('lastEntries.entry'),
name: 'entry', name: 'entry',
field: 'stateName', field: 'stateName',
align: 'left', align: 'center',
format: (val) => dashIfEmpty(val), format: (val) => dashIfEmpty(val),
}, },
{ {
label: t('lastEntries.pvp'), label: t('lastEntries.pvp'),
name: 'pvp', name: 'pvp',
field: 'reference', field: 'reference',
align: 'left', align: 'center',
format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3), format: (_, row) => toCurrency(row.price2) + ' / ' + toCurrency(row.price3),
}, },
{
label: t('lastEntries.printedStickers'),
name: 'printedStickers',
field: 'printedStickers',
align: 'center',
format: (val) => dashIfEmpty(val),
},
{ {
label: t('lastEntries.label'), label: t('lastEntries.label'),
name: 'label', name: 'stickers',
field: 'stickers', field: 'stickers',
align: 'center', align: 'center',
format: (val) => dashIfEmpty(val), format: (val) => dashIfEmpty(val),
@ -93,11 +112,13 @@ const columns = computed(() => [
{ {
label: t('shelvings.packing'), label: t('shelvings.packing'),
name: 'packing', name: 'packing',
field: 'packing',
align: 'center', align: 'center',
}, },
{ {
label: t('lastEntries.grouping'), label: t('lastEntries.grouping'),
name: 'grouping', name: 'grouping',
field: 'grouping',
align: 'center', align: 'center',
}, },
{ {
@ -108,18 +129,19 @@ const columns = computed(() => [
}, },
{ {
label: t('lastEntries.quantity'), label: t('lastEntries.quantity'),
name: 'stems', name: 'quantity',
field: 'quantity', field: 'quantity',
align: 'center', align: 'center',
}, },
{ {
label: t('lastEntries.cost'), label: t('lastEntries.cost'),
name: 'cost', name: 'cost',
align: 'left', field: 'cost',
align: 'center',
}, },
{ {
label: t('lastEntries.kg'), label: 'Kg',
name: 'stems', name: 'weight',
field: 'weight', field: 'weight',
align: 'center', align: 'center',
}, },
@ -131,9 +153,9 @@ const columns = computed(() => [
}, },
{ {
label: t('lastEntries.supplier'), label: t('lastEntries.supplier'),
name: 'stems', name: 'supplier',
field: 'supplier', field: 'supplier',
align: 'left', align: 'center',
}, },
]); ]);
@ -157,11 +179,18 @@ const updateFilter = async () => {
else if (from.value && !to.value) filter = { gte: from.value }; else if (from.value && !to.value) filter = { gte: from.value };
else if (from.value && to.value) filter = { between: [from.value, to.value] }; else if (from.value && to.value) filter = { between: [from.value, to.value] };
arrayData.store.userFilter.where.landed = filter; const userFilter = arrayData.store.userFilter.where;
userFilter.landed = filter;
if (hideInventory.value) userFilter.supplierFk = { neq: inventorySupplierFk };
else delete userFilter.supplierFk;
await fetchItemLastEntries(); await fetchItemLastEntries();
}; };
onMounted(async () => { onMounted(async () => {
await getInventorySupplier();
const _from = Date.vnNew(); const _from = Date.vnNew();
_from.setDate(_from.getDate() - 75); _from.setDate(_from.getDate() - 75);
from.value = getDate(_from, 'from'); from.value = getDate(_from, 'from');
@ -171,14 +200,13 @@ onMounted(async () => {
updateFilter(); updateFilter();
watch([from, to], ([nFrom, nTo], [oFrom, oTo]) => { watch([from, to, hideInventory], ([nFrom, nTo], [oFrom, oTo]) => {
if (nFrom && nFrom != oFrom) nFrom = getDate(new Date(nFrom), 'from'); if (nFrom && nFrom != oFrom) nFrom = getDate(new Date(nFrom), 'from');
if (nTo && nTo != oTo) nTo = getDate(new Date(nTo), 'to'); if (nTo && nTo != oTo) nTo = getDate(new Date(nTo), 'to');
updateFilter(); updateFilter();
}); });
}); });
</script> </script>
<template> <template>
<VnSubToolbar> <VnSubToolbar>
<template #st-data> <template #st-data>
@ -187,27 +215,45 @@ onMounted(async () => {
dense dense
v-model="from" v-model="from"
class="q-mr-lg" class="q-mr-lg"
data-cy="from"
/>
<VnInputDate
:label="t('lastEntries.to')"
v-model="to"
dense
class="q-mr-lg"
data-cy="to"
/>
<QCheckbox
:label="t('Hide inventory supplier')"
v-model="hideInventory"
dense
class="q-mr-lg"
data-cy="hideInventory"
/> />
<VnInputDate :label="t('lastEntries.to')" dense v-model="to" />
</template> </template>
</VnSubToolbar> </VnSubToolbar>
<QPage class="column items-center q-pa-xd"> <QPage class="column items-center q-pa-xd">
<QTable <QTable
:rows="itemLastEntries" :rows="itemLastEntries"
:columns="columns" :columns="columns"
class="full-width q-mt-md" class="table full-width q-mt-md"
:no-data-label="t('globals.noResults')" :no-data-label="t('globals.noResults')"
> >
<template #body-cell-ig="{ row }"> <template #body-cell-ig="{ row }">
<QTd @click.stop> <QTd class="text-center">
<QCheckbox <QIcon
v-model="row.isIgnored" :name="row.isIgnored ? 'check_box' : 'check_box_outline_blank'"
:disable="true" style="color: var(--vn-label-color)"
:false-value="0" size="sm"
:true-value="1"
/> />
</QTd> </QTd>
</template> </template>
<template #body-cell-date="{ row }">
<QTd class="text-center">
<VnDateBadge :date="row.landed" />
</QTd>
</template>
<template #body-cell-entry="{ row }"> <template #body-cell-entry="{ row }">
<QTd @click.stop> <QTd @click.stop>
<div class="full-width flex justify-center"> <div class="full-width flex justify-center">
@ -229,8 +275,8 @@ onMounted(async () => {
</QTd> </QTd>
</template> </template>
<template #body-cell-pvp="{ value }"> <template #body-cell-pvp="{ value }">
<QTd @click.stop <QTd @click.stop class="text-center">
><span> {{ value }}</span> <span> {{ value }}</span>
<QTooltip> <QTooltip>
{{ t('lastEntries.grouping') }}/{{ t('lastEntries.packing') }} {{ t('lastEntries.grouping') }}/{{ t('lastEntries.packing') }}
</QTooltip></QTd </QTooltip></QTd
@ -249,7 +295,7 @@ onMounted(async () => {
</QTd> </QTd>
</template> </template>
<template #body-cell-cost="{ row }"> <template #body-cell-cost="{ row }">
<QTd @click.stop> <QTd @click.stop class="text-center">
<span> <span>
{{ toCurrency(row.cost, 'EUR', 3) }} {{ toCurrency(row.cost, 'EUR', 3) }}
<QTooltip> <QTooltip>
@ -267,10 +313,25 @@ onMounted(async () => {
</span> </span>
</QTd> </QTd>
</template> </template>
<template #body-cell-supplier="{ row }">
<QTd @click.stop>
<div class="full-width flex justify-center">
<SupplierDescriptorProxy
:id="row.supplierFk"
class="q-ma-none"
dense
/>
<span class="link">{{ row.supplier }}</span>
</div>
</QTd>
</template>
</QTable> </QTable>
</QPage> </QPage>
</template> </template>
<i18n>
es:
Hide inventory supplier: Ocultar proveedor inventario
</i18n>
<style lang="scss" scoped> <style lang="scss" scoped>
.q-badge--rounded { .q-badge--rounded {
border-radius: 50%; border-radius: 50%;
@ -282,4 +343,10 @@ onMounted(async () => {
padding: 0 11px; padding: 0 11px;
height: 28px; height: 28px;
} }
.th :first-child {
.td {
text-align: center;
background-color: red;
}
}
</style> </style>

View File

@ -46,7 +46,7 @@ const getUrl = (id, param) => `#/Item/${id}/${param}`;
<template #body="{ entity: { item, tags, visible, available, botanical } }"> <template #body="{ entity: { item, tags, visible, available, botanical } }">
<QCard class="vn-one photo"> <QCard class="vn-one photo">
<ItemDescriptorImage <ItemDescriptorImage
:entity-id="entityId" :entity-id="Number(entityId)"
:visible="visible" :visible="visible"
:available="available" :available="available"
:show-edit-button="false" :show-edit-button="false"

View File

@ -66,6 +66,7 @@ lastEntries:
package: Package package: Package
freight: Freight freight: Freight
comission: Comission comission: Comission
printedStickers: Pri.
itemTags: itemTags:
removeTag: Remove tag removeTag: Remove tag
addTag: Add tag addTag: Add tag

View File

@ -56,7 +56,7 @@ lastEntries:
landed: F. Entrega landed: F. Entrega
entry: Entrada entry: Entrada
pvp: PVP pvp: PVP
label: Etiquetas label: Eti.
grouping: Grouping grouping: Grouping
quantity: Cantidad quantity: Cantidad
cost: Coste cost: Coste
@ -66,6 +66,7 @@ lastEntries:
package: Embalaje package: Embalaje
freight: Porte freight: Porte
comission: Comisión comission: Comisión
printedStickers: Imp.
itemTags: itemTags:
removeTag: Quitar etiqueta removeTag: Quitar etiqueta
addTag: Añadir etiqueta addTag: Añadir etiqueta

View File

@ -134,6 +134,7 @@ const getLocale = (label) => {
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<VnSelect <VnSelect
@ -209,6 +210,34 @@ const getLocale = (label) => {
/> />
</QItemSection> </QItemSection>
</QItem> </QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('globals.params.departmentFk')"
v-model="params.department"
option-label="name"
option-value="name"
url="Departments"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnSelect
outlined
dense
rounded
:label="t('globals.params.packing')"
v-model="params.packing"
url="ItemPackingTypes"
option-label="code"
option-value="code"
/>
</QItemSection>
</QItem>
<QItem> <QItem>
<QItemSection> <QItemSection>
<QCheckbox <QCheckbox

View File

@ -10,20 +10,23 @@ import ZoneDescriptorProxy from 'src/pages/Zone/Card/ZoneDescriptorProxy.vue';
import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue'; import TicketSummary from 'src/pages/Ticket/Card/TicketSummary.vue';
import VnTable from 'components/VnTable/VnTable.vue'; import VnTable from 'components/VnTable/VnTable.vue';
import { useSummaryDialog } from 'src/composables/useSummaryDialog'; import { useSummaryDialog } from 'src/composables/useSummaryDialog';
import { toDateFormat } from 'src/filters/date.js';
import { toCurrency, dateRange, dashIfEmpty } from 'src/filters'; import { toCurrency, dateRange, dashIfEmpty } from 'src/filters';
import RightMenu from 'src/components/common/RightMenu.vue'; import RightMenu from 'src/components/common/RightMenu.vue';
import MonitorTicketSearchbar from './MonitorTicketSearchbar.vue'; import MonitorTicketSearchbar from './MonitorTicketSearchbar.vue';
import MonitorTicketFilter from './MonitorTicketFilter.vue'; import MonitorTicketFilter from './MonitorTicketFilter.vue';
import TicketProblems from 'src/components/TicketProblems.vue'; import TicketProblems from 'src/components/TicketProblems.vue';
import VnDateBadge from 'src/components/common/VnDateBadge.vue';
const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000; // 2min in ms const DEFAULT_AUTO_REFRESH = 2 * 60 * 1000;
const { t } = useI18n(); const { t } = useI18n();
const autoRefresh = ref(false); const autoRefresh = ref(false);
const tableRef = ref(null); const tableRef = ref(null);
const provinceOpts = ref([]); const provinceOpts = ref([]);
const stateOpts = ref([]); const stateOpts = ref([]);
const zoneOpts = ref([]); const zoneOpts = ref([]);
const DepartmentOpts = ref([]);
const ItemPackingTypeOpts = ref([]);
const visibleColumns = ref([]);
const { viewSummary } = useSummaryDialog(); const { viewSummary } = useSummaryDialog();
const [from, to] = dateRange(Date.vnNew()); const [from, to] = dateRange(Date.vnNew());
@ -51,6 +54,8 @@ function exprBuilder(param, value) {
case 'nickname': case 'nickname':
return { [`t.nickname`]: { like: `%${value}%` } }; return { [`t.nickname`]: { like: `%${value}%` } };
case 'zoneFk': case 'zoneFk':
case 'department':
return { 'd.name': value };
case 'totalWithVat': case 'totalWithVat':
return { [`t.${param}`]: value }; return { [`t.${param}`]: value };
} }
@ -137,6 +142,7 @@ const columns = computed(() => [
align: 'left', align: 'left',
format: (row) => row.practicalHour, format: (row) => row.practicalHour,
columnFilter: false, columnFilter: false,
dense: true,
}, },
{ {
label: t('salesTicketsTable.preparation'), label: t('salesTicketsTable.preparation'),
@ -190,6 +196,7 @@ const columns = computed(() => [
'false-value': 0, 'false-value': 0,
'true-value': 1, 'true-value': 1,
}, },
component: false,
}, },
{ {
label: t('salesTicketsTable.zone'), label: t('salesTicketsTable.zone'),
@ -206,6 +213,12 @@ const columns = computed(() => [
}, },
}, },
}, },
{
label: t('salesTicketsTable.payMethod'),
name: 'payMethod',
align: 'left',
columnFilter: false,
},
{ {
label: t('salesTicketsTable.total'), label: t('salesTicketsTable.total'),
name: 'totalWithVat', name: 'totalWithVat',
@ -219,6 +232,36 @@ const columns = computed(() => [
}, },
}, },
}, },
{
label: t('salesTicketsTable.department'),
name: 'department',
align: 'left',
columnFilter: {
component: 'select',
url: 'Departments',
attrs: {
options: DepartmentOpts.value,
optionValue: 'name',
optionLabel: 'name',
dense: true,
},
},
},
{
label: t('salesTicketsTable.packing'),
name: 'packing',
align: 'left',
columnFilter: {
component: 'select',
url: 'ItemPackingTypes',
attrs: {
options: ItemPackingTypeOpts.value,
'option-value': 'code',
'option-label': 'code',
dense: true,
},
},
},
{ {
align: 'right', align: 'right',
name: 'tableActions', name: 'tableActions',
@ -250,19 +293,6 @@ const columns = computed(() => [
}, },
]); ]);
const getBadgeAttrs = (date) => {
let today = Date.vnNew();
today.setHours(0, 0, 0, 0);
let timeTicket = new Date(date);
timeTicket.setHours(0, 0, 0, 0);
let timeDiff = today - timeTicket;
if (timeDiff == 0) return { color: 'warning', 'text-color': 'black' };
if (timeDiff < 0) return { color: 'success', 'text-color': 'black' };
return { color: 'transparent', 'text-color': 'white' };
};
let refreshTimer = null; let refreshTimer = null;
const autoRefreshHandler = (value) => { const autoRefreshHandler = (value) => {
@ -279,14 +309,6 @@ const totalPriceColor = (ticket) => {
if (total > 0 && total < 50) return 'warning'; if (total > 0 && total < 50) return 'warning';
}; };
const formatShippedDate = (date) => {
if (!date) return '-';
const dateSplit = date.split('T');
const [year, month, day] = dateSplit[0].split('-');
const newDate = new Date(year, month - 1, day);
return toDateFormat(newDate);
};
const openTab = (id) => const openTab = (id) =>
window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer'); window.open(`#/ticket/${id}/sale`, '_blank', 'noopener, noreferrer');
</script> </script>
@ -318,6 +340,24 @@ const openTab = (id) =>
auto-load auto-load
@on-fetch="(data) => (zoneOpts = data)" @on-fetch="(data) => (zoneOpts = data)"
/> />
<FetchData
url="ItemPackingTypes"
:filter="{
fields: ['code'],
order: 'code ASC',
}"
auto-load
@on-fetch="(data) => (ItemPackingTypeOpts = data)"
/>
<FetchData
url="Departments"
:filter="{
fields: ['id', 'name'],
order: 'id ASC',
}"
auto-load
@on-fetch="(data) => (DepartmentOpts = data)"
/>
<MonitorTicketSearchbar /> <MonitorTicketSearchbar />
<RightMenu> <RightMenu>
<template #right-panel> <template #right-panel>
@ -337,7 +377,7 @@ const openTab = (id) =>
auto-load auto-load
:row-click="({ id }) => openTab(id)" :row-click="({ id }) => openTab(id)"
:disable-option="{ card: true }" :disable-option="{ card: true }"
:user-params="{ from, to, scopeDays: 0 }" :user-params="{ from, to, scopeDays: 0, packing }"
> >
<template #top-left> <template #top-left>
<QBtn <QBtn
@ -382,13 +422,7 @@ const openTab = (id) =>
</div> </div>
</template> </template>
<template #column-shippedDate="{ row }"> <template #column-shippedDate="{ row }">
<QBadge <VnDateBadge :date="row.shippedDate" />
v-bind="getBadgeAttrs(row.shippedDate)"
class="q-pa-sm"
style="font-size: 14px"
>
{{ formatShippedDate(row.shippedDate) }}
</QBadge>
</template> </template>
<template #column-provinceFk="{ row }"> <template #column-provinceFk="{ row }">
<span :title="row.province" v-text="row.province" /> <span :title="row.province" v-text="row.province" />

View File

@ -26,8 +26,8 @@ salesTicketsTable:
componentLack: Component lack componentLack: Component lack
tooLittle: Ticket too little tooLittle: Ticket too little
identifier: Identifier identifier: Identifier
theoretical: Theoretical theoretical: H.Theor
practical: Practical practical: H.Prac
province: Province province: Province
state: State state: State
isFragile: Is fragile isFragile: Is fragile
@ -35,7 +35,10 @@ salesTicketsTable:
goToLines: Go to lines goToLines: Go to lines
preview: Preview preview: Preview
total: Total total: Total
preparation: Preparation preparation: H.Prep
payMethod: Pay method
department: Department
packing: ITP
searchBar: searchBar:
label: Search tickets label: Search tickets
info: Search tickets by id or alias info: Search tickets by id or alias

View File

@ -26,8 +26,8 @@ salesTicketsTable:
componentLack: Faltan componentes componentLack: Faltan componentes
tooLittle: Ticket demasiado pequeño tooLittle: Ticket demasiado pequeño
identifier: Identificador identifier: Identificador
theoretical: Teórica theoretical: H.Teór
practical: Práctica practical: H.Prác
province: Provincia province: Provincia
state: Estado state: Estado
isFragile: Es frágil isFragile: Es frágil
@ -35,7 +35,10 @@ salesTicketsTable:
goToLines: Ir a líneas goToLines: Ir a líneas
preview: Vista previa preview: Vista previa
total: Total total: Total
preparation: Preparación preparation: H.Prep
payMethod: Método de pago
department: Departamento
packing: ITP
searchBar: searchBar:
label: Buscar tickets label: Buscar tickets
info: Buscar tickets por identificador o alias info: Buscar tickets por identificador o alias

View File

@ -75,19 +75,6 @@ watch(
}, },
{ immediate: true } { immediate: true }
); );
const onItemSaved = (updatedItem) => {
requestAnimationFrame(() => {
scrollToItem(updatedItem.items[0].itemFk);
});
};
const scrollToItem = async (id) => {
const element = itemRefs.value[id]?.$el;
if (element) {
element.scrollIntoView({ behavior: 'smooth', block: 'center' });
}
};
provide('onItemSaved', onItemSaved);
</script> </script>
<template> <template>

View File

@ -65,7 +65,6 @@ const selectCategory = async (params, category, search) => {
params.typeFk = null; params.typeFk = null;
params.categoryFk = category.id; params.categoryFk = category.id;
await loadTypes(category?.id); await loadTypes(category?.id);
await search();
}; };
const loadTypes = async (id) => { const loadTypes = async (id) => {

View File

@ -1,12 +1,12 @@
<script setup> <script setup>
import toCurrency from 'src/filters/toCurrency'; import toCurrency from 'src/filters/toCurrency';
import { inject, ref } from 'vue'; import { computed, inject, ref } from 'vue';
import { useI18n } from 'vue-i18n'; import { useI18n } from 'vue-i18n';
import axios from 'axios'; import axios from 'axios';
import { useRoute } from 'vue-router'; import { useRoute } from 'vue-router';
import useNotify from 'composables/useNotify'; import useNotify from 'composables/useNotify';
import { useArrayData } from 'composables/useArrayData';
import VnInputNumber from 'src/components/common/VnInputNumber.vue'; import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import { useState } from 'src/composables/useState';
const { t } = useI18n(); const { t } = useI18n();
const { notify } = useNotify(); const { notify } = useNotify();
@ -18,10 +18,17 @@ const props = defineProps({
required: true, required: true,
}, },
}); });
const onItemSaved = inject('onItemSaved'); const state = useState();
const orderData = computed(() => state.get('orderData'));
const prices = ref((props.item.prices || []).map((item) => ({ ...item, quantity: 0 }))); const prices = ref((props.item.prices || []).map((item) => ({ ...item, quantity: 0 })));
const descriptorData = useArrayData('orderData');
const isLoading = ref(false); const isLoading = ref(false);
const totalQuantity = (items) =>
items.reduce((acc, item) => {
return acc + item.quantity;
}, 0);
const addToOrder = async () => { const addToOrder = async () => {
if (isLoading.value) return; if (isLoading.value) return;
isLoading.value = true; isLoading.value = true;
@ -30,10 +37,19 @@ const addToOrder = async () => {
items, items,
orderFk: Number(route.params.id), orderFk: Number(route.params.id),
}); });
const { data: orderTotal } = await axios.get(
`Orders/${Number(route.params.id)}/getTotal`
);
state.set('orderTotal', orderTotal);
const rows = orderData.value.rows.push(...items) || [];
state.set('orderData', {
...orderData.value,
rows,
});
notify(t('globals.dataSaved'), 'positive'); notify(t('globals.dataSaved'), 'positive');
await descriptorData.fetch({}); emit('added', -totalQuantity(items));
onItemSaved({ ...props, items, saved: true });
emit('added', items);
isLoading.value = false; isLoading.value = false;
}; };
const canAddToOrder = () => { const canAddToOrder = () => {

View File

@ -63,21 +63,26 @@ const setData = (entity) => {
if (!entity) return; if (!entity) return;
getTotalRef.value && getTotalRef.value.fetch(); getTotalRef.value && getTotalRef.value.fetch();
data.value = useCardDescription(entity?.client?.name, entity?.id); data.value = useCardDescription(entity?.client?.name, entity?.id);
state.set('orderData', entity); state.set('orderTotal', total);
}; };
const getConfirmationValue = (isConfirmed) => { const getConfirmationValue = (isConfirmed) => {
return t(isConfirmed ? 'globals.confirmed' : 'order.summary.notConfirmed'); return t(isConfirmed ? 'globals.confirmed' : 'order.summary.notConfirmed');
}; };
const total = ref(null); const orderTotal = computed(() => state.get('orderTotal') ?? 0);
const total = ref(0);
</script> </script>
<template> <template>
<FetchData <FetchData
ref="getTotalRef" ref="getTotalRef"
:url="`Orders/${entityId}/getTotal`" :url="`Orders/${entityId}/getTotal`"
@on-fetch="(response) => (total = response)" @on-fetch="
(response) => {
total = response;
}
"
/> />
<CardDescriptor <CardDescriptor
ref="descriptor" ref="descriptor"
@ -112,7 +117,7 @@ const total = ref(null);
:label="t('order.summary.items')" :label="t('order.summary.items')"
:value="(entity?.rows?.length || DEFAULT_ITEMS).toString()" :value="(entity?.rows?.length || DEFAULT_ITEMS).toString()"
/> />
<VnLv :label="t('order.summary.total')" :value="toCurrency(total)" /> <VnLv :label="t('order.summary.total')" :value="toCurrency(orderTotal)" />
</template> </template>
<template #actions="{ entity }"> <template #actions="{ entity }">
<QCardActions> <QCardActions>

View File

@ -10,6 +10,7 @@ import { useState } from 'src/composables/useState';
import axios from 'axios'; 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 DepartmentDescriptorProxy from 'src/pages/Department/Card/DepartmentDescriptorProxy.vue';
const $props = defineProps({ const $props = defineProps({
id: { id: {
@ -143,10 +144,14 @@ const handlePhotoUpdated = (evt = false) => {
:value="entity.user?.emailUser?.email" :value="entity.user?.emailUser?.email"
copy copy
/> />
<VnLv <VnLv :label="t('worker.list.department')">
:label="t('worker.list.department')" <template #value>
:value="entity.department ? entity.department.department.name : null" <span class="link" v-text="entity.department?.department?.name" />
/> <DepartmentDescriptorProxy
:id="entity.department?.department?.id"
/>
</template>
</VnLv>
<VnLv :value="entity.phone"> <VnLv :value="entity.phone">
<template #label> <template #label>
{{ t('globals.phone') }} {{ t('globals.phone') }}

View File

@ -108,7 +108,20 @@ const agencyOptions = ref([]);
clearable clearable
/> />
</VnRow> </VnRow>
<VnRow>
<VnSelect
:label="t('Distribution point')"
v-model="data.addressFk"
option-value="id"
option-label="nickname"
url="Addresses"
:fields="['id', 'nickname']"
sort-by="id"
hide-selected
map-options
:rules="validate('data.addressFk')"
/>
</VnRow>
<VnRow> <VnRow>
<VnInput <VnInput
v-model="data.inflation" v-model="data.inflation"
@ -143,4 +156,5 @@ es:
Inflation: Inflación Inflation: Inflación
Volumetric: Volumétrico Volumetric: Volumétrico
Max length : Medida máxima tumbado Max length : Medida máxima tumbado
Distribution point: Punto de distribución
</i18n> </i18n>

View File

@ -0,0 +1,20 @@
describe('ItemLastEntries', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.login('buyer');
cy.visit('/#/item/1/last-entries');
cy.intercept('GET', /.*lastEntriesFilter/).as('item');
cy.waitForElement('tbody');
});
it('should filter by agency', () => {
cy.get('tbody > tr')
.its('length')
.then((rowCount) => {
cy.get('[data-cy="hideInventory"]').click();
cy.wait('@item');
cy.waitForElement('tbody');
cy.get('tbody > tr').should('have.length.greaterThan', rowCount);
});
});
});

View File

@ -0,0 +1,28 @@
import { vi, describe, expect, it, beforeAll, afterEach } from 'vitest';
import { createWrapper } from 'app/test/vitest/helper';
import VnDiscount from 'components/common/vnDiscount.vue';
describe('VnDiscount', () => {
let vm;
beforeAll(() => {
vm = createWrapper(VnDiscount, {
props: {
data: {},
price: 100,
quantity: 2,
discount: 10,
}
}).vm;
});
afterEach(() => {
vi.clearAllMocks();
});
describe('total', () => {
it('should calculate total correctly', () => {
expect(vm.total).toBe(180);
});
});
});