fix(documents): restrict advance invoice creation to projects
All checks were successful
Build and Release FEDEO / verify (push) Successful in 1m8s
Build and Release FEDEO / build-and-release (push) Successful in 6m24s

This commit is contained in:
2026-10-15 13:05:06 +02:00
parent 6eea3c0687
commit 5dc9e686fc
7 changed files with 200 additions and 11 deletions

View File

@@ -0,0 +1,131 @@
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 { parse, compileScript } = require('@vue/compiler-sfc')
const documentTypesForCreation = Object.fromEntries([
'invoices', 'advanceInvoices', 'serialInvoices', 'quotes', 'costEstimates',
'deliveryNotes', 'packingSlips', 'confirmationOrders', 'cancellationInvoices',
].map(key => [key, { labelSingle: key }]))
function fixture(filename, names, globals = {}) {
const { descriptor } = parse(fs.readFileSync(`${__dirname}/../${filename}`, 'utf8'))
compileScript(descriptor, { id: filename, inlineTemplate: true })
const script = ts.createSourceFile(filename, descriptor.scriptSetup.content, ts.ScriptTarget.Latest, true, ts.ScriptKind.JS)
const source = script.statements.filter(statement => ts.isVariableStatement(statement)
&& statement.declarationList.declarations.some(declaration => names.includes(declaration.name.getText(script))))
.map(statement => statement.getText(script)).join('\n')
const navigations = []
const notices = []
const context = vm.createContext({
ref: value => ({ value }), computed: getter => ({ get value() { return getter() } }),
dataStore: { documentTypesForCreation }, URLSearchParams,
router: { push: url => navigations.push(url), replace: url => navigations.push(url) },
modal: { close() {} }, toast: { add: notice => notices.push(notice) },
...globals,
})
vm.runInContext(source, context)
return { context, navigations, notices, run: code => vm.runInContext(code, context) }
}
test('copying any document excludes advance invoices and requires an allowed target', () => {
for (const type of Object.keys(documentTypesForCreation)) {
const { run, navigations } = fixture('components/copyCreatedDocumentModal.vue', [
'documentTypeToUse', 'documentTypeItems', 'selectedDocumentType', 'optionsToImport', 'startImport',
], { props: { id: '12', type } })
assert.equal(run("documentTypeItems.value.some(item => item.key === 'advanceInvoices')"), false)
assert.equal(run('documentTypeToUse.value'), type === 'advanceInvoices' ? null : type)
run("documentTypeToUse.value = 'advanceInvoices'; startImport()")
assert.equal(navigations.length, 0)
run("documentTypeToUse.value = 'invoices'; startImport()")
const query = new URL(navigations[0], 'http://localhost').searchParams
assert.equal(query.get('type'), 'invoices')
assert.equal(query.get('createddocument'), '12')
}
})
test('+ Dokument excludes advance invoices even when opened inside a project', () => {
for (const queryStringData of ['', 'project=7&customer=8']) {
const { run, navigations } = fixture('components/createDocumentModal.vue', ['documentTypes', 'createDocument'], {
props: { queryStringData },
})
assert.equal(run("documentTypes.value.some(item => item.key === 'advanceInvoices')"), false)
run("createDocument('advanceInvoices')")
assert.equal(navigations.length, 0)
run("createDocument('quotes')")
assert.equal(new URL(navigations[0], 'http://localhost').searchParams.get('type'), 'quotes')
}
})
test('advance invoice templates cannot create outgoing documents', () => {
const { run, navigations } = fixture('components/createDocumentFromTemplateModal.vue', [
'templates', 'selectedType', 'documentTypes', 'visibleTemplates', 'selectTemplate',
], { props: { queryStringData: 'project=7' } })
run("templates.value = [{ id: 1, documentType: 'advanceInvoices' }, { id: 2, documentType: 'quotes' }]")
assert.equal(run("documentTypes.value.some(item => item.key === 'advanceInvoices')"), false)
assert.equal(run('visibleTemplates.value.length'), 1)
assert.equal(run('visibleTemplates.value[0].id'), 2)
run('selectTemplate(templates.value[0])')
assert.equal(navigations.length, 0)
run('selectTemplate(templates.value[1])')
assert.equal(new URL(navigations[0], 'http://localhost').searchParams.get('templateId'), '2')
})
function editorFixture(query = {}, id) {
return fixture('pages/createDocument/edit/[[id]].vue', [
'originalDocumentType', 'canUseAdvanceInvoice', 'documentTypeItems', 'normalizeEntityId',
'validateAdvanceInvoiceCreation', 'saveDocument',
], {
route: { params: { id }, query }, isTemplateMode: { value: false },
itemInfo: { value: { type: 'advanceInvoices', project: 7 } },
})
}
test('only the project action opens a new advance invoice with its project and customer', () => {
const { run, navigations, context } = fixture('components/EntityShowSubCreatedDocuments.vue', ['createAdvanceInvoice'], {
props: { topLevelType: 'projects', item: { id: 7 }, queryStringData: 'customer=8&project=99' },
})
run('createAdvanceInvoice()')
const query = Object.fromEntries(new URL(navigations[0], 'http://localhost').searchParams)
assert.equal(query.project, '7')
assert.equal(query.customer, '8')
const editor = editorFixture(query)
assert.equal(editor.run('validateAdvanceInvoiceCreation()'), true)
assert.equal(editor.run("documentTypeItems.value.some(item => item.type === 'advanceInvoices')"), true)
context.props.topLevelType = 'customers'
run('createAdvanceInvoice()')
assert.equal(navigations.length, 1)
})
test('editor rejects alternate creation routes and saving a switched document type', async () => {
const projectQuery = { type: 'advanceInvoices', loadMode: 'advanceInvoice', project: '7' }
for (const query of [
{}, { type: 'advanceInvoices' }, { type: 'advanceInvoices', project: '7' },
{ ...projectQuery, project: '' }, { ...projectQuery, project: 'invalid' },
{ ...projectQuery, createddocument: '12' }, { ...projectQuery, optionsToImport: '{}' },
{ ...projectQuery, linkedDocuments: '[12]' }, { ...projectQuery, templateId: '5' },
]) {
const { run, notices, navigations } = editorFixture(query)
assert.equal(run("documentTypeItems.value.some(item => item.type === 'advanceInvoices')"), false)
assert.equal(run('validateAdvanceInvoiceCreation()'), false)
await run("saveDocument('Entwurf')")
assert.equal(notices.length, 2)
assert.equal(navigations.length, 0)
}
})
test('new advance invoices require a project; existing advance invoices remain editable', () => {
const fresh = editorFixture({ type: 'advanceInvoices', loadMode: 'advanceInvoice', project: '7' })
fresh.run('itemInfo.value.project = null')
assert.equal(fresh.run('validateAdvanceInvoiceCreation()'), false)
const existing = editorFixture({}, '12')
existing.run("originalDocumentType.value = 'advanceInvoices'; itemInfo.value.project = null")
assert.equal(existing.run('validateAdvanceInvoiceCreation()'), true)
assert.equal(existing.run("documentTypeItems.value.some(item => item.type === 'advanceInvoices')"), true)
existing.run("originalDocumentType.value = 'invoices'")
assert.equal(existing.run('validateAdvanceInvoiceCreation()'), false)
existing.run("itemInfo.value.type = 'invoices'")
assert.equal(existing.run('validateAdvanceInvoiceCreation()'), true)
})