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

This commit is contained in:
Jose Antonio Tubau 2025-05-16 10:15:38 +00:00
commit 1ffb5eca15
19 changed files with 705 additions and 256 deletions

87
Jenkinsfile vendored
View File

@ -108,7 +108,6 @@ pipeline {
}
stage('E2E') {
environment {
CREDS = credentials('docker-registry')
COMPOSE_PROJECT = "${PROJECT_NAME}-${env.BUILD_ID}"
COMPOSE_PARAMS = "-p ${env.COMPOSE_PROJECT} -f test/cypress/docker-compose.yml --project-directory ."
}
@ -118,23 +117,24 @@ pipeline {
sh 'rm -rf test/cypress/screenshots'
env.COMPOSE_TAG = PROTECTED_BRANCH.contains(env.CHANGE_TARGET) ? env.CHANGE_TARGET : 'dev'
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh 'docker login --username $CREDS_USR --password $CREDS_PSW $REGISTRY'
sh "docker-compose ${env.COMPOSE_PARAMS} pull back"
sh "docker-compose ${env.COMPOSE_PARAMS} pull db"
sh "docker-compose ${env.COMPOSE_PARAMS} up -d"
def modules = sh(script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}", returnStdout: true).trim()
def modules = sh(
script: "node test/cypress/docker/find/find.js ${env.COMPOSE_TAG}",
returnStdout: true
).trim()
echo "E2E MODULES: ${modules}"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'"
script {
def image = docker.build('lilium-dev', '-f docs/Dockerfile.dev docs')
sh "docker compose ${env.COMPOSE_PARAMS} up -d"
image.inside("--network ${env.COMPOSE_PROJECT}_default -e CI -e TZ --init") {
sh "sh test/cypress/docker/cypressParallel.sh 1 '${modules}'"
}
}
}
}
post {
always {
sh "docker-compose ${env.COMPOSE_PARAMS} down -v"
sh "docker compose ${env.COMPOSE_PARAMS} down -v"
archiveArtifacts artifacts: 'test/cypress/screenshots/**/*', allowEmptyArchive: true
junit(
testResults: 'junit/e2e-*.xml',
@ -153,17 +153,8 @@ pipeline {
VERSION = readFile 'VERSION.txt'
}
steps {
script {
sh 'quasar build'
def baseImage = "salix-frontend:${env.VERSION}"
def image = docker.build(baseImage, ".")
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
image.push()
image.push(env.BRANCH_NAME)
if (IS_LATEST) image.push('latest')
}
}
sh 'quasar build'
dockerBuild 'salix-frontend', '.'
}
}
stage('Deploy') {
@ -186,3 +177,53 @@ pipeline {
}
}
def dockerBuild(imageName, context, dockerfile = null) {
if (dockerfile == null)
dockerfile = "${context}/Dockerfile"
def certDir = '/home/jenkins/.buildkit/certs'
def buildxArgs = [
"--name=buildkitd",
"--driver=remote",
"--driver-opt="
+ "cacert=${certDir}/ca.pem,"
+ "cert=${certDir}/cert.pem,"
+ "key=${certDir}/key.pem,"
+ "servername=buildkitd",
"tcp://buildkitd:1234"
]
def cacheImage = "${env.REGISTRY_CACHE}/${imageName}"
def pushImage = "${env.REGISTRY}/${imageName}"
def baseImage = "${pushImage}:${env.VERSION}"
def buildArgs = [
context,
"--push",
"--builder=buildkitd",
"--file=${dockerfile}",
"--cache-from=type=registry,ref=${cacheImage}:cache-${env.BRANCH_NAME}",
"--cache-from=type=registry,ref=${cacheImage}:cache-dev",
"--cache-to=type=registry,ref=${cacheImage}:cache-${env.BRANCH_NAME},mode=max",
"--tag=${pushImage}:${env.BRANCH_NAME}"
]
def isLatest = ['master', 'main'].contains(env.BRANCH_NAME)
if (isLatest)
buildArgs.push("--tag=${pushImage}:latest")
// FIXME: Nested docker.withRegistry() does not work
// https://issues.jenkins.io/browse/JENKINS-59777
withCredentials([usernamePassword(
credentialsId: 'registry-cache',
usernameVariable: 'CACHE_USR',
passwordVariable: 'CACHE_PSW')
]) {
docker.withRegistry("https://${env.REGISTRY}", 'docker-registry') {
sh 'echo "$CACHE_PSW" | docker login --username "$CACHE_USR" --password-stdin "http://$REGISTRY_CACHE"'
sh "docker buildx create ${buildxArgs.join(' ')}"
docker.build(baseImage, buildArgs.join(' '))
}
}
}

View File

@ -1,6 +1,6 @@
{
"name": "salix-front",
"version": "25.18.0",
"version": "25.22.0",
"description": "Salix frontend",
"productName": "Salix",
"author": "Verdnatura",

View File

@ -102,6 +102,10 @@ const $props = defineProps({
type: Boolean,
default: false,
},
customMethod: {
type: String,
default: null,
},
});
const emit = defineEmits(['onFetch', 'onDataSaved', 'submit']);
const modelValue = computed(
@ -237,7 +241,9 @@ async function save() {
const url =
$props.urlCreate || $props.urlUpdate || $props.url || arrayData.store.url;
const response = await Promise.resolve(
$props.saveFn ? $props.saveFn(body) : axios[method](url, body),
$props.saveFn
? $props.saveFn(body)
: axios[$props.customMethod ?? method](url, body),
);
if ($props.urlCreate) notify('globals.dataCreated', 'positive');

View File

@ -1,7 +1,7 @@
<script setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute, useRouter } from 'vue-router';
import { useRoute } from 'vue-router';
import axios from 'axios';
import { date } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
@ -21,7 +21,6 @@ const stateStore = useStateStore();
const validationsStore = useValidator();
const { models } = validationsStore;
const route = useRoute();
const router = useRouter();
const { t } = useI18n();
const props = defineProps({
model: {
@ -273,7 +272,7 @@ onUnmounted(() => {
:data-key
:url="dataKey + 's'"
:user-filter="filter"
:filter="{ where: { and: [{ originFk: route.params.id }] } }"
:user-params="{ originFk: route.params.id }"
:skeleton="false"
auto-load
@on-fetch="setLogTree"

View File

@ -42,7 +42,7 @@ const card = toRef(props, 'item');
</div>
<div class="content">
<span class="link" @click.stop>
{{ card.name }}
{{ card.longName }}
<ItemDescriptorProxy :id="card.id" />
</span>
<p class="subName">{{ card.subName }}</p>
@ -57,11 +57,12 @@ const card = toRef(props, 'item');
<QIcon name="production_quantity_limits" size="xs" />
{{ card.minQuantity }}
</div>
<div class="footer">
<div class="footer q-mt-auto">
<div class="price">
<p v-if="isCatalog">
{{ card.available }} {{ t('to') }}
{{ toCurrency(card.price) }}
<span class="text-primary">{{ card.available }}</span>
{{ t('to') }}
<span class="text-bold" >{{ toCurrency(card.price) }}</span>
</p>
<slot name="price" />
<QIcon v-if="isCatalog" name="add_circle" class="icon">
@ -144,28 +145,29 @@ const card = toRef(props, 'item');
}
.footer {
.price {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
justify-content: space-between;
p {
font-size: 12px;
}
.price {
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
display: flex;
align-items: center;
justify-content: space-between;
.icon {
color: $primary;
font-size: 24px;
cursor: pointer;
}
p {
font-size: 12px;
}
.price-kg {
font-size: 12px;
.icon {
color: $primary;
font-size: 24px;
cursor: pointer;
}
}
.price-kg {
font-size: 12px;
}
}
.item-color-container {

View File

@ -313,6 +313,7 @@ export function useArrayData(key, userOptions) {
const { params, limit } = getCurrentFilter();
store.currentFilter = JSON.parse(JSON.stringify(params));
delete store.currentFilter.filter.include;
delete store.currentFilter.filter.fields;
store.currentFilter.filter = JSON.stringify(store.currentFilter.filter);
return { params, limit };
}

View File

@ -896,6 +896,7 @@ components:
rate3: Packing price
minPrice: Min. Price
itemFk: Item id
dated: Date
userPanel:
copyToken: Token copied to clipboard
settings: Settings

View File

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

View File

@ -2,14 +2,20 @@
import { computed, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import { useRoute } from 'vue-router';
import { useQuasar } from 'quasar';
import { toCurrency, toDateHourMin } from 'src/filters';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnConfirm from 'components/ui/VnConfirm.vue';
import VnRow from 'components/ui/VnRow.vue';
import axios from 'axios';
import WorkerDescriptorProxy from 'src/pages/Worker/Card/WorkerDescriptorProxy.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
const { t } = useI18n();
const route = useRoute();
const quasar = useQuasar();
const tableRef = ref();
@ -45,26 +51,45 @@ const columns = computed(() => [
align: 'right',
field: 'rating',
label: t('customer.summary.rating'),
name: 'rating',
create: true,
columnCreate: {
component: 'number',
autofocus: true,
},
name: 'rating'
},
{
align: 'right',
field: 'recommendedCredit',
format: ({ recommendedCredit }) => toCurrency(recommendedCredit),
label: t('customer.summary.recommendCredit'),
name: 'recommendedCredit',
create: true,
columnCreate: {
component: 'number',
autofocus: true,
},
name: 'recommendedCredit'
},
]);
const defaultInitialData = {
rating: null,
recommendedCredit: null
};
const createRating = async (data) => {
await axios.post(`Clients/${route.params.id}/setRating`, data);
tableRef.value?.reload();
};
const handleSave = async (data) => {
if (data.rating || data.recommendedCredit) {
await createRating(data);
return;
}
quasar.dialog({
component: VnConfirm,
componentProps: {
title: t('terminationTitle'),
message: t('terminationMessage'),
},
})
.onOk(async () => {
await createRating({ rating: 0, recommendedCredit: 0 });
});
};
</script>
<template>
@ -72,8 +97,7 @@ const columns = computed(() => [
ref="tableRef"
data-key="ClientInformas"
url="ClientInformas"
:filter="filter"
:order="['created DESC']"
:user-filter="filter"
:columns="columns"
:right-search="false"
:is-editable="false"
@ -82,22 +106,43 @@ const columns = computed(() => [
:disable-option="{ card: true }"
auto-load
:create="{
urlCreate: `Clients/${route.params.id}/setRating`,
title: 'Create rating',
onDataSaved: () => tableRef.reload(),
formInitialData: {},
onDataSaved: ()=> tableRef.reload(),
formInitialData: defaultInitialData,
saveFn: handleSave
}"
>
<template #column-employee="{ row }">
<span class="link">{{ row.worker.user.nickname }}</span>
<WorkerDescriptorProxy :id="row.worker.id" />
</template>
<template #more-create-dialog="{ data }">
<VnRow>
<VnInputNumber
v-model="data.rating"
:label="t('customer.summary.rating')"
:required="!!data.recommendedCredit"
/>
<VnInputNumber
v-model="data.recommendedCredit"
:label="t('customer.summary.recommendCredit')"
:required="!!data.rating"
/>
</VnRow>
</template>
</VnTable>
</template>
<i18n>
en:
terminationTitle: Confirm contract termination
terminationMessage: Are you sure you want to terminate the contract? This action will set the rating and recommended credit to 0.
es:
Recommended credit: Crédito recomendado
Since: Desde
Employee: Empleado
Create rating: Crear calificación
terminationTitle: Confirmar baja de contrato
terminationMessage: ¿Está seguro que desea dar de baja el contrato? Esta acción establecerá la calificación y el crédito recomendado en 0.
</i18n>

View File

@ -80,7 +80,6 @@ function isRequired({ isTaxDataChecked: taxDataChecked }) {
@on-data-saved="checkEtChanges"
>
<template #form="{ data, validate, validations }">
{{ isTaxDataChecked }} {{ data.isTaxDataChecked }}
<VnRow>
<VnInput
:label="t('Social name')"

View File

@ -1,16 +1,21 @@
<script setup>
import { ref, computed, markRaw } from 'vue';
import { ref, computed, nextTick } from 'vue';
import { useRoute } from 'vue-router';
import { useI18n } from 'vue-i18n';
import { useArrayData } from 'src/composables/useArrayData';
import { getTotal } from 'src/composables/getTotal';
import { toCurrency } from 'src/filters';
import FetchData from 'src/components/FetchData.vue';
import VnSelect from 'src/components/common/VnSelect.vue';
import CrudModel from 'src/components/CrudModel.vue';
import VnInputNumber from 'src/components/common/VnInputNumber.vue';
import VnSelectDialog from 'src/components/common/VnSelectDialog.vue';
import CreateNewExpenseForm from 'src/components/CreateNewExpenseForm.vue';
import { getExchange } from 'src/composables/getExchange';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnSelectExpense from 'src/components/common/VnSelectExpense.vue';
import { useAccountShortToStandard } from 'src/composables/useAccountShortToStandard';
const { t } = useI18n();
const arrayData = useArrayData();
const route = useRoute();
const invoiceIn = computed(() => arrayData.store.data);
@ -19,142 +24,100 @@ const expenses = ref([]);
const sageTaxTypes = ref([]);
const sageTransactionTypes = ref([]);
const rowsSelected = ref([]);
const invoiceInVatTableRef = ref();
const invoiceInFormRef = ref();
defineProps({ actionIcon: { type: String, default: 'add' } });
function taxRate(invoiceInTax) {
const sageTaxTypeId = invoiceInTax.taxTypeSageFk;
const taxRateSelection = sageTaxTypes.value.find(
(transaction) => transaction.id == sageTaxTypeId,
);
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return (taxTypeSage / 100) * taxableBase;
}
defineProps({
actionIcon: {
type: String,
default: 'add',
},
});
const columns = computed(() => [
{
name: 'expenseFk',
name: 'expense',
label: t('Expense'),
component: markRaw(VnSelectExpense),
format: (row) => {
const expense = expenses.value.find((e) => e.id === row.expenseFk);
return expense ? `${expense.id}: ${expense.name}` : row.expenseFk;
},
field: (row) => row.expenseFk,
options: expenses.value,
model: 'expenseFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.name}`,
sortable: true,
align: 'left',
isEditable: true,
create: true,
width: '250px',
},
{
name: 'taxableBase',
name: 'taxablebase',
label: t('Taxable base'),
component: 'number',
attrs: {
clearable: true,
'clear-icon': 'close',
},
field: (row) => row.taxableBase,
model: 'taxableBase',
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'isDeductible',
label: t('invoiceIn.isDeductible'),
component: 'checkbox',
field: (row) => row.isDeductible,
model: 'isDeductible',
align: 'center',
isEditable: true,
create: true,
createAttrs: {
defaultValue: true,
},
width: '100px',
},
{
name: 'taxTypeSageFk',
name: 'sageiva',
label: t('Sage iva'),
component: 'select',
attrs: {
options: sageTaxTypes.value,
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`,
filterOptions: ['id', 'vat'],
'data-cy': 'vat-sageiva',
},
format: (row) => {
const taxType = sageTaxTypes.value.find((t) => t.id === row.taxTypeSageFk);
return taxType ? `${taxType.id}: ${taxType.vat}` : row.taxTypeSageFk;
},
field: (row) => row.taxTypeSageFk,
options: sageTaxTypes.value,
model: 'taxTypeSageFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.vat}`,
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'transactionTypeSageFk',
name: 'sagetransaction',
label: t('Sage transaction'),
component: 'select',
attrs: {
options: sageTransactionTypes.value,
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`,
filterOptions: ['id', 'transaction'],
},
format: (row) => {
const transType = sageTransactionTypes.value.find(
(t) => t.id === row.transactionTypeSageFk,
);
return transType
? `${transType.id}: ${transType.transaction}`
: row.transactionTypeSageFk;
},
field: (row) => row.transactionTypeSageFk,
options: sageTransactionTypes.value,
model: 'transactionTypeSageFk',
optionValue: 'id',
optionLabel: (row) => `${row.id}: ${row.transaction}`,
sortable: true,
align: 'left',
isEditable: true,
create: true,
},
{
name: 'rate',
label: t('Rate'),
sortable: false,
format: (row) => taxRate(row).toFixed(2),
sortable: true,
field: (row) => taxRate(row, row.taxTypeSageFk),
align: 'left',
},
{
name: 'foreignValue',
name: 'foreignvalue',
label: t('Foreign value'),
component: 'number',
sortable: true,
field: (row) => row.foreignValue,
align: 'left',
create: true,
disable: !isNotEuro(currency.value),
},
{
name: 'total',
label: t('Total'),
label: 'Total',
align: 'left',
format: (row) => (Number(row.taxableBase || 0) + Number(taxRate(row))).toFixed(2),
},
]);
const tableRows = computed(
() => invoiceInVatTableRef.value?.CrudModelRef?.formData || [],
);
const taxableBaseTotal = computed(() => {
return getTotal(tableRows.value, 'taxableBase');
return getTotal(invoiceInFormRef.value.formData, 'taxableBase');
});
const taxRateTotal = computed(() => {
return tableRows.value.reduce((sum, row) => sum + Number(taxRate(row)), 0);
return getTotal(invoiceInFormRef.value.formData, null, {
cb: taxRate,
});
});
const combinedTotal = computed(() => {
return +taxableBaseTotal.value + +taxRateTotal.value;
});
const filter = computed(() => ({
const filter = {
fields: [
'id',
'invoiceInFk',
@ -168,75 +131,389 @@ const filter = computed(() => ({
where: {
invoiceInFk: route.params.id,
},
}));
};
const isNotEuro = (code) => code != 'EUR';
async function handleForeignValueUpdate(val, row) {
if (!isNotEuro(currency.value)) return;
row.taxableBase = await getExchange(
val,
invoiceIn.value?.currencyFk,
invoiceIn.value?.issued,
function taxRate(invoiceInTax) {
const sageTaxTypeId = invoiceInTax.taxTypeSageFk;
const taxRateSelection = sageTaxTypes.value.find(
(transaction) => transaction.id == sageTaxTypeId,
);
const taxTypeSage = taxRateSelection?.rate ?? 0;
const taxableBase = invoiceInTax?.taxableBase ?? 0;
return ((taxTypeSage / 100) * taxableBase).toFixed(2);
}
function autocompleteExpense(evt, row, col, ref) {
const val = evt.target.value;
if (!val) return;
const param = isNaN(val) ? row[col.model] : val;
const lookup = expenses.value.find(
({ id }) => id == useAccountShortToStandard(param),
);
ref.vnSelectDialogRef.vnSelectRef.toggleOption(lookup);
}
function setCursor(ref) {
nextTick(() => {
const select = ref.vnSelectDialogRef
? ref.vnSelectDialogRef.vnSelectRef
: ref.vnSelectRef;
select.$el.querySelector('input').setSelectionRange(0, 0);
});
}
</script>
<template>
<FetchData url="Expenses" auto-load @on-fetch="(data) => (expenses = data)" />
<FetchData
ref="expensesRef"
url="Expenses"
auto-load
@on-fetch="(data) => (expenses = data)"
/>
<FetchData url="SageTaxTypes" auto-load @on-fetch="(data) => (sageTaxTypes = data)" />
<FetchData
url="sageTransactionTypes"
auto-load
@on-fetch="(data) => (sageTransactionTypes = data)"
/>
<VnTable
<CrudModel
ref="invoiceInFormRef"
v-if="invoiceIn"
ref="invoiceInVatTableRef"
data-key="InvoiceInTaxes"
url="InvoiceInTaxes"
save-url="InvoiceInTaxes/crud"
:filter="filter"
:data-required="{ invoiceInFk: $route.params.id }"
:insert-on-load="true"
auto-load
v-model:selected="rowsSelected"
:columns="columns"
:is-editable="true"
:table="{ selection: 'multiple', 'row-key': '$index' }"
footer
:right-search="false"
:column-search="false"
:disable-option="{ card: true }"
class="q-pa-none"
:create="{
urlCreate: 'InvoiceInTaxes',
title: t('Add tax'),
formInitialData: { invoiceInFk: $route.params.id, isDeductible: true },
onDataSaved: () => invoiceInVatTableRef.reload(),
}"
:crud-model="{ goTo: `/invoice-in/${$route.params.id}/due-day` }"
:go-to="`/invoice-in/${$route.params.id}/due-day`"
>
<template #column-footer-taxableBase>
{{ toCurrency(taxableBaseTotal) }}
<template #body="{ rows }">
<QTable
v-model:selected="rowsSelected"
selection="multiple"
:columns="columns"
:rows="rows"
row-key="$index"
:grid="$q.screen.lt.sm"
>
<template #body-cell-expense="{ row, col }">
<QTd>
<VnSelectDialog
:ref="`expenseRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
:acls="[
{ model: 'Expense', props: '*', accessType: 'WRITE' },
]"
@keydown.tab.prevent="
autocompleteExpense(
$event,
row,
col,
$refs[`expenseRef-${row.$index}`],
)
"
@update:model-value="
setCursor($refs[`expenseRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #form>
<CreateNewExpenseForm
@on-data-saved="$refs.expensesRef.fetch()"
/>
</template>
</VnSelectDialog>
</QTd>
</template>
<template #body-cell-isDeductible="{ row }">
<QTd align="center">
<QCheckbox
v-model="row.isDeductible"
data-cy="isDeductible_checkbox"
/>
</QTd>
</template>
<template #body-cell-taxablebase="{ row }">
<QTd shrink>
<VnInputNumber
clear-icon="close"
v-model="row.taxableBase"
clearable
/>
</QTd>
</template>
<template #body-cell-sageiva="{ row, col }">
<QTd>
<VnSelect
:ref="`sageivaRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'vat']"
data-cy="vat-sageiva"
@update:model-value="
setCursor($refs[`sageivaRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{ scope.opt.vat }}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-sagetransaction="{ row, col }">
<QTd>
<VnSelect
:ref="`sagetransactionRef-${row.$index}`"
v-model="row[col.model]"
:options="col.options"
:option-value="col.optionValue"
:option-label="col.optionLabel"
:filter-options="['id', 'transaction']"
@update:model-value="
setCursor($refs[`sagetransactionRef-${row.$index}`])
"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.transaction
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QTd>
</template>
<template #body-cell-foreignvalue="{ row }">
<QTd shrink>
<VnInputNumber
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
v-model="row.foreignValue"
@update:model-value="
async (val) => {
if (!isNotEuro(currency)) return;
row.taxableBase = await getExchange(
val,
row.currencyFk,
invoiceIn.issued,
);
}
"
/>
</QTd>
</template>
<template #bottom-row>
<QTr class="bg">
<QTd />
<QTd />
<QTd>
{{ toCurrency(taxableBaseTotal) }}
</QTd>
<QTd />
<QTd />
<QTd />
<QTd>
{{ toCurrency(taxRateTotal) }}
</QTd>
<QTd />
<QTd>
{{ toCurrency(combinedTotal) }}
</QTd>
</QTr>
</template>
<template #item="props">
<div class="q-pa-xs col-xs-12 col-sm-6 grid-style-transition">
<QCard bordered flat class="q-my-xs">
<QCardSection>
<QCheckbox v-model="props.selected" dense />
</QCardSection>
<QSeparator />
<QList>
<QItem>
<VnSelectDialog
:label="t('Expense')"
class="full-width"
v-model="props.row['expenseFk']"
:options="expenses"
option-value="id"
:option-label="(row) => `${row.id}:${row.name}`"
:filter-options="['id', 'name']"
:tooltip="t('Create a new expense')"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
{{ `${scope.opt.id}: ${scope.opt.name}` }}
</QItem>
</template>
<template #form>
<CreateNewExpenseForm />
</template>
</VnSelectDialog>
</QItem>
<QItem>
<VnInputNumber
:label="t('Taxable base')"
:class="{
'no-pointer-events': isNotEuro(currency),
}"
class="full-width"
:disable="isNotEuro(currency)"
clear-icon="close"
v-model="props.row.taxableBase"
clearable
/>
</QItem>
<QItem>
<VnSelect
:label="t('Sage iva')"
class="full-width"
v-model="props.row['taxTypeSageFk']"
:options="sageTaxTypes"
option-value="id"
:option-label="(row) => `${row.id}:${row.vat}`"
:filter-options="['id', 'vat']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.vat
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItem>
<QItem>
<VnSelect
class="full-width"
v-model="props.row['transactionTypeSageFk']"
:options="sageTransactionTypes"
option-value="id"
:option-label="
(row) => `${row.id}:${row.transaction}`
"
:filter-options="['id', 'transaction']"
>
<template #option="scope">
<QItem v-bind="scope.itemProps">
<QItemSection>
<QItemLabel>{{
scope.opt.transaction
}}</QItemLabel>
<QItemLabel>
{{ `#${scope.opt.id}` }}
</QItemLabel>
</QItemSection>
</QItem>
</template>
</VnSelect>
</QItem>
<QItem>
{{ toCurrency(taxRate(props.row), currency) }}
</QItem>
<QItem>
<VnInputNumber
:label="t('Foreign value')"
class="full-width"
:class="{
'no-pointer-events': !isNotEuro(currency),
}"
:disable="!isNotEuro(currency)"
v-model="props.row.foreignValue"
/>
</QItem>
</QList>
</QCard>
</div>
</template>
</QTable>
</template>
<template #column-footer-rate>
{{ toCurrency(taxRateTotal) }}
</template>
<template #column-footer-total>
{{ toCurrency(combinedTotal) }}
</template>
</VnTable>
</CrudModel>
<QPageSticky position="bottom-right" :offset="[25, 25]">
<QBtn
color="primary"
icon="add"
size="lg"
v-shortcut="'+'"
round
@click="invoiceInFormRef.insert()"
>
<QTooltip>{{ t('Add tax') }}</QTooltip>
</QBtn>
</QPageSticky>
</template>
<style lang="scss" scoped>
.bg {
background-color: var(--vn-light-gray);
}
@media (max-width: $breakpoint-xs) {
.q-dialog {
.q-card {
&__section:not(:first-child) {
.q-item {
flex-direction: column;
.q-checkbox {
margin-top: 2rem;
}
}
}
}
}
}
.q-item {
min-height: 0;
}
.default-icon {
cursor: pointer;
border-radius: 50px;
background-color: $primary;
}
</style>
<i18n>
es:
Expense: Gasto
Create a new expense: Crear nuevo gasto
Add tax: Añadir Gasto/IVA # Changed label slightly
Add tax: Crear gasto
Taxable base: Base imp.
Sage iva: Sage iva # Kept original label
Sage tax: Sage iva
Sage transaction: Sage transacción
Rate: Cuota # Changed label
Rate: Tasa
Foreign value: Divisa
Total: Total
invoiceIn.isDeductible: Deducible
</i18n>

View File

@ -1,9 +1,11 @@
<script setup>
import { onMounted, ref, onUnmounted, computed, watch } from 'vue';
import axios from 'axios';
import { useI18n } from 'vue-i18n';
import { useQuasar } from 'quasar';
import { useStateStore } from 'stores/useStateStore';
import { useState } from 'src/composables/useState';
import { beforeSave } from 'src/composables/updateMinPriceBeforeSave';
import FetchedTags from 'components/ui/FetchedTags.vue';
@ -14,13 +16,13 @@ import RightMenu from 'src/components/common/RightMenu.vue';
import VnTable from 'src/components/VnTable/VnTable.vue';
import VnColor from 'src/components/common/VnColor.vue';
import { toDate } from 'src/filters';
import { toDate, toCurrency } from 'src/filters';
import { isLower, isBigger } from 'src/filters/date.js';
import ItemFixedPriceFilter from './ItemFixedPriceFilter.vue';
import ItemDescriptorProxy from './Card/ItemDescriptorProxy.vue';
import { toCurrency } from 'src/filters';
const stateStore = useStateStore();
const quasar = useQuasar();
const { t } = useI18n();
const tableRef = ref();
const editFixedPriceForm = ref(null);
@ -218,11 +220,36 @@ const dateStyle = (date) =>
}
: { color: dateColor, 'background-color': 'transparent' };
const onDataSaved = () => {
tableRef.value.CrudModelRef.saveChanges();
const onDataSaved = async (data) => {
for (const row of data) {
await axios.patch('FixedPrices/upsertFixedPrice', row);
}
selectedRows.value = [];
tableRef.value.reload();
tableRef.value.CrudModelRef.reset();
};
async function saveData(data, getChanges) {
const changes = getChanges();
if (changes?.updates?.length) {
for (const change of changes.updates) {
const row = data.find((row) => row.id === change.where.id);
await axios.patch('FixedPrices/upsertFixedPrice', row);
}
}
if (data?.deletes?.length) {
for (const deleteItem of data.deletes) {
await axios.delete(`FixedPrices/${deleteItem}`);
quasar.notify({
message: t('globals.dataDeleted'),
color: 'positive',
});
}
}
tableRef.value.reload();
}
onMounted(() => {
if (tableRef.value) {
tableRef.value.showForm = false;
@ -273,7 +300,8 @@ watch(
data-key="ItemFixedPrices"
url="FixedPrices/filter"
:order="'name DESC'"
save-url="FixedPrices/crud"
save-url="FixedPrices/upsertFixedPrice"
:saveFn="saveData"
:columns="columns"
:is-editable="true"
:right-search="false"
@ -283,7 +311,8 @@ watch(
}"
v-model:selected="selectedRows"
:create="{
urlCreate: 'FixedPrices',
urlCreate: 'FixedPrices/upsertFixedPrice',
customMethod: 'patch',
title: t('Create fixed price'),
formInitialData: { warehouseFk: warehouse },
onDataSaved: () => tableRef.reload(),

View File

@ -3,6 +3,7 @@ import { useI18n } from 'vue-i18n';
import VnInputDate from 'src/components/common/VnInputDate.vue';
import VnSelect from 'components/common/VnSelect.vue';
import VnCheckbox from 'src/components/common/VnCheckbox.vue';
import ItemsFilterPanel from 'src/components/ItemsFilterPanel.vue';
const { t } = useI18n();
@ -51,42 +52,41 @@ const props = defineProps({
/>
</QItemSection>
</QItem>
<QSeparator />
<QItemSection>
<QIcon name="info" size="sm" class="info-icon cursor-pointer">
<QTooltip>{{ t('params.incompatibleFilters') }}</QTooltip>
</QIcon>
<QItem>
<QItemSection>
<VnInputDate
v-model="params.dated"
:label="t('params.date')"
filled
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<VnCheckbox
v-model="params.showBadDates"
:label="t(`params.showBadDates`)"
toggle-indeterminate
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
</QItemSection>
<QSeparator />
<QItem>
<QItemSection>
<VnInputDate
v-model="params.started"
:label="t('params.started')"
filled
@update:model-value="searchFn()"
/>
</QItemSection>
<QItemSection>
<VnInputDate
v-model="params.ended"
:label="t('params.ended')"
filled
@update:model-value="searchFn()"
/>
</QItemSection>
</QItem>
<QItem>
<QItemSection>
<QCheckbox
<VnCheckbox
:label="t('params.mine')"
v-model="params.mine"
toggle-indeterminate
@update:model-value="searchFn()"
/>
<QCheckbox
v-model="params.showBadDates"
:label="t(`params.showBadDates`)"
toggle-indeterminate
@update:model-value="searchFn()"
>
</QCheckbox>
<QCheckbox
<VnCheckbox
:label="t('params.hasMinPrice')"
v-model="params.hasMinPrice"
toggle-indeterminate
@ -97,6 +97,13 @@ const props = defineProps({
</template>
</ItemsFilterPanel>
</template>
<style lang="scss" scoped>
.info-icon {
position: relative;
top: 0;
left: 90%;
}
</style>
<i18n>
en:
params:
@ -107,6 +114,8 @@ en:
mine: Mine
showBadDates: Show future items
hasMinPrice: Has Min Price
date: Date
incompatibleFilters: Cannot select "Date" and "Show future items" at the same time
es:
params:
buyerFk: Comprador
@ -116,4 +125,6 @@ es:
mine: Para mi
showBadDates: Ver items a futuro
hasMinPrice: Precio mínimo
date: Fecha
incompatibleFilters: No se puede seleccionar "Fecha" y "Ver items a futuro" a la vez
</i18n>

View File

@ -26,7 +26,6 @@ const $props = defineProps({
default: () => {},
},
});
const { t } = useI18n();
const emit = defineEmits(['onDataSaved']);
@ -38,7 +37,7 @@ const inputs = {
select: markRaw(VnSelect),
};
const newValue = ref(null);
const newFieldValue = ref(null);
const selectedField = ref(null);
const closeButton = ref(null);
const isLoading = ref(false);
@ -46,7 +45,11 @@ const isLoading = ref(false);
const onSubmit = async () => {
isLoading.value = true;
$props.rows.forEach((row) => {
row[selectedField.value.name] = newValue.value;
const newValue =
selectedField.value.component === 'number'
? parseInt(newFieldValue.value)
: newFieldValue.value;
row[selectedField.value.name] = newValue;
});
emit('onDataSaved', $props.rows);
closeForm();
@ -77,11 +80,19 @@ const closeForm = () => {
data-cy="EditFixedPriceSelectOption"
@update:model-value="newValue = null"
:class="{ 'is-select': selectedField?.component === 'select' }"
/>
>
<template #option="{ opt, itemProps }">
<QItem v-bind="itemProps" class="q-pa-xs row items-center">
<QItemSection class="col-9 justify-center">
<span>{{ opt?.label }}</span>
</QItemSection>
</QItem>
</template>
</VnSelect>
<component
:is="inputs[selectedField?.component || 'input']"
v-bind="selectedField?.attrs || {}"
v-model="newValue"
v-model="newFieldValue"
:label="t('Value')"
data-cy="EditFixedPriceValueOption"
style="width: 200px"

View File

@ -19,7 +19,7 @@ const { t } = useI18n();
const dataKey = 'OrderCatalogList';
const catalogParams = {
orderFk: route.params.id,
orderBy: JSON.stringify({ field: 'relevancy DESC, name', way: 'ASC', isTag: false }),
orderBy: JSON.stringify({ field: 'relevancy DESC, longName', way: 'ASC', isTag: false }),
};
const arrayData = useArrayData(dataKey, {
url: 'Orders/CatalogFilter',

View File

@ -46,6 +46,28 @@ const { openConfirmationModal } = useVnConfirm();
:summary="$props.summary"
:to-module="{ name: 'WorkerDepartment' }"
data-key="Department"
:filter="{
include: [
{
relation: 'client',
scope: {
fields: ['id', 'name'],
},
},
{
relation: 'worker',
scope: {
fields: ['id', 'name'],
include: {
relation: 'user',
scope: {
fields: ['id', 'name'],
},
},
},
},
],
}"
>
<template #menu="{}">
<QItem

View File

@ -1,4 +1,3 @@
version: '3.7'
services:
back:
image: 'registry.verdnatura.es/salix-back:${COMPOSE_TAG:-dev}'

View File

@ -7,7 +7,7 @@ function checkSageFields(isRequired = false) {
describe('Client fiscal data', () => {
describe('#1008', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit('#/customer/1108/fiscal-data');
});
@ -27,12 +27,22 @@ describe('Client fiscal data', () => {
});
describe('#1007', () => {
beforeEach(() => {
cy.viewport(1280, 720);
cy.viewport(1920, 1080);
cy.login('developer');
cy.visit('#/customer/1107/fiscal-data');
});
it('check as equalizated', () => {
cy.get('[data-cy="vnCheckboxInvoice by address"] > .q-checkbox__inner').then(
($el) => {
if (!$el.hasClass('q-checkbox__inner--truthy')) {
cy.wrap($el).click({ force: true });
}
},
);
cy.get(
'[data-cy="vnCheckboxInvoice by address"] > .q-checkbox__inner',
).should('have.class', 'q-checkbox__inner--truthy');
cy.dataCy('vnCheckboxIs equalizated').click();
cy.get('.q-btn-group > .q-btn--standard > .q-btn__content').click();
@ -40,7 +50,6 @@ describe('Client fiscal data', () => {
'contain',
'You changed the equalization tax',
);
cy.get('.q-card > :nth-child(2) > span').should(
'have.text',
'Do you want to spread the change?',

View File

@ -40,7 +40,7 @@ describe('ZoneCalendar', { testIsolation: true }, () => {
it('should exclude an event', () => {
cy.get('.q-mb-sm > .q-radio__inner').click();
cy.get('.q-current-day > .q-calendar-month__day--label__wrapper').click();
cy.get('.q-mt-lg > .q-btn--standard').click();
cy.get(submitBtn).click();
cy.get(
'.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]',
).click();
@ -48,19 +48,15 @@ describe('ZoneCalendar', { testIsolation: true }, () => {
cy.dataCy('VnConfirm_confirm').click();
});
it(
'should not exclude an event if there are tickets for that zone and day',
{ testIsoaltion: true },
() => {
cy.visit(`/#/zone/3/events`);
cy.get('.q-mb-sm > .q-radio__inner').click();
cy.get(
'.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]',
).click();
cy.get('.q-mt-lg > .q-btn--standard').click();
cy.checkNotification(
'Can not close this zone because there are tickets programmed for that day',
);
},
);
it('should not exclude an event if there are tickets for that zone and day', () => {
cy.get('[data-cy="vn-searchbar_input"]').type('3{enter}');
cy.get('[aria-label="Exclude"] > .q-radio__label').click();
cy.get(
'.q-current-day > .q-calendar-month__day--content > [data-cy="ZoneCalendarDay"]',
).click();
cy.get(submitBtn).click();
cy.checkNotification(
'Can not close this zone because there are tickets programmed for that day',
);
});
});