From 8f06d0603786c62c746f7d757fcb9853ec9553a9 Mon Sep 17 00:00:00 2001 From: flfeders Date: Fri, 25 Sep 2026 11:42:17 +0200 Subject: [PATCH] fix(documents): normalize units when selecting and loading rows --- frontend/pages/createDocument/edit/[[id]].vue | 24 +++-- frontend/tests/document-editor-units.test.cjs | 97 +++++++++++++++++++ 2 files changed, 108 insertions(+), 13 deletions(-) create mode 100644 frontend/tests/document-editor-units.test.cjs diff --git a/frontend/pages/createDocument/edit/[[id]].vue b/frontend/pages/createDocument/edit/[[id]].vue index 44534237..5e93793c 100644 --- a/frontend/pages/createDocument/edit/[[id]].vue +++ b/frontend/pages/createDocument/edit/[[id]].vue @@ -333,6 +333,13 @@ const normalizeEntityId = (value) => { } return typeof value === "object" ? (value.id ?? null) : value } +const normalizeUnitId = (unit) => { + const value = unit && typeof unit === "object" ? unit.id : unit + if (typeof value !== "number" && typeof value !== "string") return null + + const id = Number(value) + return Number.isSafeInteger(id) && id > 0 ? id : null +} const normalizeCreatedDocumentRow = (row) => { let normalizedRow = row @@ -359,6 +366,7 @@ const normalizeCreatedDocumentRow = (row) => { return { ...normalizedRow, id: normalizedRow.id || uuidv4(), + unit: normalizeUnitId(normalizedRow.unit), costCentre: normalizedRow.costCentre || normalizedRow.costcentre || null, purchasePrice: Number(normalizedRow.purchasePrice || 0), linkedEntitys: Array.isArray(normalizedRow.linkedEntitys) ? normalizedRow.linkedEntitys : [], @@ -689,6 +697,7 @@ const setupPage = async () => { } + itemInfo.value.rows = normalizeCreatedDocumentRows(itemInfo.value.rows) loaded.value = true } @@ -2035,19 +2044,8 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = { if (row.service) { console.log(service) - if(service.unit) { - row.unit = service.unit - } else { - let selectedService = services.value.find(i => i.id === row.service) - console.log(selectedService) - if(selectedService.unit?.id) { - row.unit = selectedService.unit.id - } else { - row.unit = selectedService.unit - } - } + row.unit = normalizeUnitId(service?.unit ?? services.value.find(i => i.id === row.service)?.unit) - //row.unit = service.unit ? service.unit : services.value.find(i => i.id === row.service).unit row.inputPrice = ((service.sellingPriceComposed.total || service.sellingPrice) ? (service.sellingPriceComposed.total || service.sellingPrice) : (services.value.find(i => i.id === row.service).sellingPriceComposed.total || services.value.find(i => i.id === row.service).sellingPrice)) row.description = service.description ? service.description : (services.value.find(i => i.id === row.service) ? services.value.find(i => i.id === row.service).description : "") row.costCentre = service.costcentre || services.value.find(i => i.id === row.service)?.costcentre || itemInfo.value.costcentre @@ -2061,7 +2059,7 @@ const setRowData = async (row, service = {sellingPriceComposed: {}}, product = { if (row.product) { console.log("Product Detected") - row.unit = product.unit ? product.unit : products.value.find(i => i.id === row.product).unit + row.unit = normalizeUnitId(product?.unit ?? products.value.find(i => i.id === row.product)?.unit) row.inputPrice = (product.selling_price ? product.selling_price : products.value.find(i => i.id === row.product).selling_price) row.costCentre = row.costCentre || itemInfo.value.costcentre //row.price = Number((row.originalPrice * (1 + itemInfo.value.customSurchargePercentage /100)).toFixed(2)) diff --git a/frontend/tests/document-editor-units.test.cjs b/frontend/tests/document-editor-units.test.cjs new file mode 100644 index 00000000..e3122f0c --- /dev/null +++ b/frontend/tests/document-editor-units.test.cjs @@ -0,0 +1,97 @@ +const { test } = require('node:test') +const assert = require('node:assert/strict') +const fs = require('node:fs') +const vm = require('node:vm') +const ts = require('typescript') +const dayjs = require('dayjs') +const { parse } = require('@vue/compiler-sfc') + +const { descriptor } = parse(fs.readFileSync(`${__dirname}/../pages/createDocument/edit/[[id]].vue`, 'utf8')) +const script = ts.createSourceFile('document-editor.js', descriptor.scriptSetup.content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS) +const handlers = new Set(['normalizeEntityId', 'normalizeUnitId', 'normalizeCreatedDocumentRow', 'normalizeCreatedDocumentRows', 'normalizeTaxTypeValue', 'setupPage', 'setRowData']) +const source = script.statements + .filter(statement => ts.isVariableStatement(statement) && statement.declarationList.declarations.some(declaration => handlers.has(declaration.name.getText(script)))) + .map(statement => statement.getText(script)).join('\n') + +function fixture(unit) { + const ref = value => ({ value }) + const product = { id: 20, name: 'Artikel', unit, selling_price: 10, tax_percentage: 19 } + const service = { id: 30, name: 'Leistung', unit, sellingPrice: 20, sellingPriceComposed: {}, taxPercentage: 19 } + const context = vm.createContext({ + dayjs, uuidv4: () => 'row-id', console: { log() {} }, + products: ref([product]), services: ref([service]), + productcategories: ref([]), servicecategories: ref([]), + itemInfo: ref({ type: 'invoices', taxType: 'Standard', rows: [], agriculture: {} }), + route: { params: {}, query: {} }, loaded: ref(false), templateName: ref(''), + taxTypeItems: [{ key: 'Standard' }], + dataStore: { documentTypesForCreation: { invoices: { labelSingle: 'Rechnung' } } }, + setupData: async () => {}, setDocumentTypeConfig() {}, setContactPersonData: async () => {}, + checkCompatibilityWithInputPrice() {}, updateCustomSurcharge() {}, + useEntities: relation => ({ + select: async () => relation === 'products' ? [product] : [service], + selectSingle: async () => { + if (relation === 'documenttemplates') return { name: 'Vorlage', documentType: 'invoices', templateData: { rows: [{ id: 'saved-row', mode: 'normal', product: 20, unit }] } } + return { id: 40, type: 'invoices', taxType: 'Standard', rows: [{ id: 'saved-row', mode: 'normal', product: 20, unit }] } + }, + }), + }) + vm.runInContext(source, context) + return { context, run: code => vm.runInContext(code, context) } +} + +const formats = [1, { id: 1, name: 'Stück' }, '1', { id: '1', name: 'Stück' }] + +test('article and service selection resolve relation objects and legacy IDs to a selectable unit', async () => { + for (const unit of formats) { + const { run } = fixture(unit) + for (const entity of ['product', 'service']) { + run(`itemInfo.value.rows = [{ ${entity}: ${entity === 'product' ? 20 : 30} }]`) + await run('setRowData(itemInfo.value.rows[0])') + assert.equal(run('itemInfo.value.rows[0].unit'), 1, `${entity}: ${JSON.stringify(unit)}`) + } + } +}) + +test('article and service modal results retain their selected unit', async () => { + for (const unit of formats) { + const { run } = fixture(unit) + run('itemInfo.value.rows = [{}]') + await run('setRowData(itemInfo.value.rows[0], null, products.value[0])') + assert.equal(run('itemInfo.value.rows[0].unit'), 1) + run('itemInfo.value.rows = [{}]') + await run('setRowData(itemInfo.value.rows[0], services.value[0], null)') + assert.equal(run('itemInfo.value.rows[0].unit'), 1) + } +}) + +test('saved documents and both template entry points restore units without overwriting the stored choice', async () => { + for (const route of [{ params: { id: 40 }, query: {} }, { params: {}, query: { templateId: 50 } }, { params: {}, query: { templateFromDocument: 40 } }]) { + for (const unit of formats) { + const { context, run } = fixture(unit) + context.route = route + context.products.value[0].unit = { id: 2, name: 'Meter' } + await run('setupPage()') + assert.equal(run('loaded.value'), true) + assert.equal(run('itemInfo.value.rows[0].unit'), 1, JSON.stringify(route)) + assert.equal(run('itemInfo.value.rows[0].id'), 'saved-row') + } + } +}) + +test('imported JSON rows preserve quantity, prices and the stored unit', () => { + const { run, context } = fixture(1) + context.rows = [JSON.stringify({ id: 'imported', mode: 'normal', unit: { id: '1' }, quantity: 2.5, price: 12.34 })] + const row = run('normalizeCreatedDocumentRows(rows)[0]') + assert.equal(row.unit, 1) + assert.equal(row.id, 'imported') + assert.equal(row.quantity, 2.5) + assert.equal(row.price, 12.34) +}) + +test('missing and malformed units remain unselected', () => { + const { run, context } = fixture(1) + for (const unit of [null, undefined, '', {}, { id: null }, 'invalid', 0, -1, true]) { + context.row = { unit } + assert.equal(run('normalizeCreatedDocumentRow(row).unit'), null) + } +}) -- 2.49.1