diff --git a/.env b/.env index a9399885..ab709e5f 100644 --- a/.env +++ b/.env @@ -32,8 +32,8 @@ API_SERVICE_SPRINT_NAME_REGEX="/(?(?:-?\d+-?)*)\.(?\d+)$/" APP_WEEK_GOAL_LOW=25.0 APP_WEEK_GOAL_HIGH=34.5 APP_INVOICE_SUPPLIER_ACCOUNT=APP_INVOICE_SUPPLIER_ACCOUNT -APP_INVOICE_DEFAULT_DESCRIPTION=APP_INVOICE_DEFAULT_DESCRIPTION -APP_INVOICE_DEFAULT_RECEIVER_ACCOUNT= +APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT=APP_INVOICE_DEFAULT_RECEIVER_ACCOUNT +APP_INVOICE_DESCRIPTION_TEMPLATE="Spørgsmål vedrørende fakturaen rettes til %name%, %email%." APP_PROJECT_BILLING_DEFAULT_DESCRIPTION= ###< Planning ### diff --git a/CHANGELOG.md b/CHANGELOG.md index 8c7bc060..0703126b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,17 +8,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +* Added project lead to client when syncing projects. +* Remove description from create invoice page. +* Added generate description button to invoice when client is set. +* Fixed texts. +* Fixed classes for choices.js fields and disabled state. +* Added project lead to invoice edit page. * Changed InvoiceEntry material number and account to be set only at the invoice level. * Added default account to invoices from environment variable. * Added check for invoice entries with amount 0 when putting invoice on record. * Fixed issue with receiver account for project billing. * Fixed invoices overview sorting. Changed default sorting for invoices on record as by exportedDate. +* Changed monolog config to ignore deprecations. +* Added Leantime specific header to api service. + + * RELEASE NOTES: * Change name APP_INVOICE_RECEIVER_ACCOUNT to APP_INVOICE_SUPPLIER_ACCOUNT in `.env.local` + * Set APP_INVOICE_DESCRIPTION_TEMPLATE in `.env.local` * Set APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT in `.env.local` * Set APP_PROJECT_BILLING_DEFAULT_DESCRIPTION in `.env.local` -* Changed monolog config to ignore deprecations. -* Added Leantime specific header to api service. ## [1.1.2] diff --git a/assets/controllers/choices_controller.js b/assets/controllers/choices_controller.js index ba216904..b8438352 100644 --- a/assets/controllers/choices_controller.js +++ b/assets/controllers/choices_controller.js @@ -10,7 +10,11 @@ export default class extends Controller { connect() { this.choicesTargets.forEach((target) => { - new Choices(target, {allowHTML: true, itemSelectText: ''}); + const notDisabled = !target.disabled; + + if (notDisabled) { + new Choices(target, {allowHTML: true, itemSelectText: ''}); + } }) } } diff --git a/assets/controllers/generate-description_controller.js b/assets/controllers/generate-description_controller.js new file mode 100644 index 00000000..74053e57 --- /dev/null +++ b/assets/controllers/generate-description_controller.js @@ -0,0 +1,35 @@ +import {Controller} from '@hotwired/stimulus'; + +export default class extends Controller { + static targets = ['description']; + + endpoint; + + connect() { + this.endpoint = this.element.dataset.endpoint; + } + + generate() { + fetch(this.endpoint, { + method: 'GET', + mode: 'same-origin', + cache: 'no-cache', + credentials: 'same-origin', + headers: { + 'Content-Type': 'application/json' + }, + redirect: 'follow', + referrerPolicy: 'no-referrer', + }).then(async (resp) => { + if (resp.ok) { + const target = this.descriptionTarget; + + resp.json().then((data) => { + if (data.description !== null) { + target.value = data.description; + } + }); + } + }); + } +} diff --git a/config/packages/monolog.yaml b/config/packages/monolog.yaml index 70411766..23ec9cb9 100644 --- a/config/packages/monolog.yaml +++ b/config/packages/monolog.yaml @@ -10,6 +10,14 @@ when@dev: path: "%kernel.logs_dir%/%kernel.environment%.log" level: debug channels: ["!event", "!doctrine", "!deprecation"] + # uncomment to get logging in your browser + # you may have to allow bigger header sizes in your Web server configuration + #firephp: + # type: firephp + # level: info + #chromephp: + # type: chromephp + # level: info console: type: console process_psr_3_messages: false @@ -38,13 +46,13 @@ when@prod: handler: nested excluded_http_codes: [404, 405] buffer_size: 50 # How many messages should be saved? Prevent memory leaks - channels: ["!event", "!doctrine", "!deprecation"] + channels: ["!deprecation"] nested: type: stream path: php://stderr level: debug formatter: monolog.formatter.json - channels: ["!event", "!doctrine", "!deprecation"] + channels: ["!deprecation"] console: type: console process_psr_3_messages: false diff --git a/config/services.yaml b/config/services.yaml index f661cfd9..105bbcb5 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -7,7 +7,6 @@ imports: # Put parameters here that don't need to change on each machine where the app is deployed # https://symfony.com/doc/current/best_practices.html#use-parameters-for-application-configuration parameters: - app.default_invoice_description: '%env(APP_INVOICE_DEFAULT_DESCRIPTION)%' app.jira_custom_fields: 'Epic Link': '%env(JIRA_API_SERVICE_CUSTOM_FIELD_EPIC_LINK)%' 'Account': '%env(JIRA_API_SERVICE_CUSTOM_FIELD_ACCOUNT)%' @@ -20,6 +19,7 @@ services: autoconfigure: true # Automatically registers your services as commands, event subscribers, etc. bind: + $defaultInvoiceDescriptionTemplate: '%env(APP_INVOICE_DESCRIPTION_TEMPLATE)%' $invoiceSupplierAccount: '%env(string:APP_INVOICE_SUPPLIER_ACCOUNT)%' $invoiceDefaultReceiverAccount: '%env(string:APP_INVOICE_RECEIVER_DEFAULT_ACCOUNT)%' $projectBillingDefaultDescription: '%env(string:APP_PROJECT_BILLING_DEFAULT_DESCRIPTION)%' diff --git a/migrations/Version20231219130737.php b/migrations/Version20231219130737.php new file mode 100644 index 00000000..91ba64f1 --- /dev/null +++ b/migrations/Version20231219130737.php @@ -0,0 +1,31 @@ +addSql('ALTER TABLE client ADD project_lead_name VARCHAR(255) DEFAULT NULL, ADD project_lead_mail VARCHAR(255) DEFAULT NULL'); + } + + public function down(Schema $schema): void + { + // this down() migration is auto-generated, please modify it to your needs + $this->addSql('ALTER TABLE client DROP project_lead_name, DROP project_lead_mail'); + } +} diff --git a/package-lock.json b/package-lock.json index c94f9546..51ecc2df 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4,6 +4,7 @@ "requires": true, "packages": { "": { + "name": "app", "license": "UNLICENSED", "devDependencies": { "@babel/core": "^7.17.0", @@ -41,12 +42,13 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "dependencies": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" }, "engines": { "node": ">=6.9.0" @@ -92,13 +94,14 @@ } }, "node_modules/@babel/generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", - "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "dependencies": { - "@babel/types": "^7.20.7", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "engines": { @@ -219,9 +222,9 @@ } }, "node_modules/@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true, "engines": { "node": ">=6.9.0" @@ -240,25 +243,25 @@ } }, "node_modules/@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "dependencies": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" @@ -388,30 +391,30 @@ } }, "node_modules/@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "dependencies": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true, "engines": { "node": ">=6.9.0" @@ -456,13 +459,13 @@ } }, "node_modules/@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "dependencies": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" }, "engines": { @@ -470,9 +473,9 @@ } }, "node_modules/@babel/parser": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz", - "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true, "bin": { "parser": "bin/babel-parser.js" @@ -1579,34 +1582,34 @@ } }, "node_modules/@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse": { - "version": "7.20.12", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz", - "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "debug": "^4.1.0", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" }, "engines": { @@ -1614,13 +1617,13 @@ } }, "node_modules/@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "dependencies": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" }, "engines": { @@ -5690,10 +5693,16 @@ } }, "node_modules/nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], "bin": { "nanoid": "bin/nanoid.cjs" }, @@ -6197,9 +6206,9 @@ } }, "node_modules/postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "dev": true, "funding": [ { @@ -6209,10 +6218,14 @@ { "type": "tidelift", "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } ], "dependencies": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" }, @@ -8653,6 +8666,7 @@ } }, "vendor/symfony/stimulus-bundle/assets": { + "name": "@symfony/stimulus-bundle", "version": "1.0.0", "dev": true, "license": "MIT", @@ -8674,12 +8688,13 @@ } }, "@babel/code-frame": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.18.6.tgz", - "integrity": "sha512-TDCmlK5eOvH+eH7cdAFlNXeVJqWIQ7gW9tY1GJIpUtFb6CmjVyq2VM3u71bOyR8CRihcCgMUYoDNyLXao3+70Q==", + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.23.5.tgz", + "integrity": "sha512-CgH3s1a96LipHCmSUmYFPwY7MNx8C3avkq7i4Wl3cfa662ldtUe4VM1TPXX70pfmrlWTb6jLqTYrZyT2ZTJBgA==", "dev": true, "requires": { - "@babel/highlight": "^7.18.6" + "@babel/highlight": "^7.23.4", + "chalk": "^2.4.2" } }, "@babel/compat-data": { @@ -8712,13 +8727,14 @@ } }, "@babel/generator": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.20.7.tgz", - "integrity": "sha512-7wqMOJq8doJMZmP4ApXTzLxSr7+oO2jroJURrVEp6XShrQUObV8Tq/D0NCcoYg2uHqUrjzO0zwBjoYzelxK+sw==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.23.6.tgz", + "integrity": "sha512-qrSfCYxYQB5owCmGLbl8XRpX1ytXlpueOb0N0UmQwA073KZxejgQTzAmJezxvpwQD9uGtK2shHdi55QT+MbjIw==", "dev": true, "requires": { - "@babel/types": "^7.20.7", + "@babel/types": "^7.23.6", "@jridgewell/gen-mapping": "^0.3.2", + "@jridgewell/trace-mapping": "^0.3.17", "jsesc": "^2.5.1" }, "dependencies": { @@ -8808,9 +8824,9 @@ } }, "@babel/helper-environment-visitor": { - "version": "7.18.9", - "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.18.9.tgz", - "integrity": "sha512-3r/aACDJ3fhQ/EVgFy0hpj8oHyHpQc+LPtJoY9SzTThAsStm4Ptegq92vqKoE3vD706ZVFWITnMnxucw+S9Ipg==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", "dev": true }, "@babel/helper-explode-assignable-expression": { @@ -8823,22 +8839,22 @@ } }, "@babel/helper-function-name": { - "version": "7.19.0", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.19.0.tgz", - "integrity": "sha512-WAwHBINyrpqywkUH0nTnNgI5ina5TFn85HKS0pbPDfxFfhyR/aNQEn4hGi1P1JyT//I0t4OgXUlofzWILRvS5w==", + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", "dev": true, "requires": { - "@babel/template": "^7.18.10", - "@babel/types": "^7.19.0" + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" } }, "@babel/helper-hoist-variables": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.18.6.tgz", - "integrity": "sha512-UlJQPkFqFULIcyW5sbzgbkxn2FKRgwWiRexcuaR8RNJRy8+LLveqPjwZV/bwrLZCN0eUHD/x8D0heK1ozuoo6Q==", + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-member-expression-to-functions": { @@ -8935,24 +8951,24 @@ } }, "@babel/helper-split-export-declaration": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.18.6.tgz", - "integrity": "sha512-bde1etTx6ZyTmobl9LLMMQsaizFVZrquTEHOqKeQESMKo4PlObf+8+JA25ZsIpZhT/WEd39+vOdLXAFG/nELpA==", + "version": "7.22.6", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.22.6.tgz", + "integrity": "sha512-AsUnxuLhRYsisFiaJwvp1QF+I3KjD5FOxut14q/GzovUe6orHLesW2C7d754kRm53h5gqrz6sFl6sxc4BVtE/g==", "dev": true, "requires": { - "@babel/types": "^7.18.6" + "@babel/types": "^7.22.5" } }, "@babel/helper-string-parser": { - "version": "7.19.4", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.19.4.tgz", - "integrity": "sha512-nHtDoQcuqFmwYNYPz3Rah5ph2p8PFeFCsZk9A/48dPc/rGocJ5J3hAAZ7pb76VWX3fZKu+uEr/FhH5jLx7umrw==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.23.4.tgz", + "integrity": "sha512-803gmbQdqwdf4olxrX4AJyFBV/RTr3rSmOj0rKwesmzlfhYNDEs+/iOcznzpNWlJlIlTJC2QfPFcHB6DlzdVLQ==", "dev": true }, "@babel/helper-validator-identifier": { - "version": "7.19.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.19.1.tgz", - "integrity": "sha512-awrNfaMtnHUr653GgGEs++LlAvW6w+DcPrOliSMXWCKo597CwL5Acf/wWdNkf/tfEQE3mjkeD1YOVZOUV/od1w==", + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.22.20.tgz", + "integrity": "sha512-Y4OZ+ytlatR8AI+8KZfKuL5urKp7qey08ha31L8b3BwewJAoJamTzyvxPR/5D+KkdJCGPq/+8TukHBlY10FX9A==", "dev": true }, "@babel/helper-validator-option": { @@ -8985,20 +9001,20 @@ } }, "@babel/highlight": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.18.6.tgz", - "integrity": "sha512-u7stbOuYjaPezCuLj29hNW1v64M2Md2qupEKP1fHc7WdOA3DgLh37suiSrZYY7haUB7iBeQZ9P1uiRF359do3g==", + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.23.4.tgz", + "integrity": "sha512-acGdbYSfp2WheJoJm/EBBBLh/ID8KDc64ISZ9DYtBmC8/Q204PZJLHyzeB5qMzJ5trcOkybd78M4x2KWsUq++A==", "dev": true, "requires": { - "@babel/helper-validator-identifier": "^7.18.6", - "chalk": "^2.0.0", + "@babel/helper-validator-identifier": "^7.22.20", + "chalk": "^2.4.2", "js-tokens": "^4.0.0" } }, "@babel/parser": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.20.7.tgz", - "integrity": "sha512-T3Z9oHybU+0vZlY9CiDSJQTD5ZapcW18ZctFMi0MOAl/4BjFF4ul7NVSARLdbGO5vDqy9eQiGTV0LtKfvCYvcg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.23.6.tgz", + "integrity": "sha512-Z2uID7YJ7oNvAI20O9X0bblw7Qqs8Q2hFy0R9tAfnfLkp5MW0UH9eUvnDSnFwKZ0AvgS1ucqR4KzvVHgnke1VQ==", "dev": true }, "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { @@ -9736,42 +9752,42 @@ } }, "@babel/template": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.20.7.tgz", - "integrity": "sha512-8SegXApWe6VoNw0r9JHpSteLKTpTiLZ4rMlGIm9JQ18KiCtyQiAMEazujAHrUS5flrcqYZa75ukev3P6QmUwUw==", + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.22.15.tgz", + "integrity": "sha512-QPErUVm4uyJa60rkI73qneDacvdvzxshT3kksGqlGWYdOTIUOwJ7RDUL8sGqslY1uXWSL6xMFKEXDS3ox2uF0w==", "dev": true, "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7" + "@babel/code-frame": "^7.22.13", + "@babel/parser": "^7.22.15", + "@babel/types": "^7.22.15" } }, "@babel/traverse": { - "version": "7.20.12", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.20.12.tgz", - "integrity": "sha512-MsIbFN0u+raeja38qboyF8TIT7K0BFzz/Yd/77ta4MsUsmP2RAnidIlwq7d5HFQrH/OZJecGV6B71C4zAgpoSQ==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.18.6", - "@babel/generator": "^7.20.7", - "@babel/helper-environment-visitor": "^7.18.9", - "@babel/helper-function-name": "^7.19.0", - "@babel/helper-hoist-variables": "^7.18.6", - "@babel/helper-split-export-declaration": "^7.18.6", - "@babel/parser": "^7.20.7", - "@babel/types": "^7.20.7", - "debug": "^4.1.0", + "version": "7.23.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.23.7.tgz", + "integrity": "sha512-tY3mM8rH9jM0YHFGyfC0/xf+SB5eKUu7HPj7/k3fpi9dAlsMc5YbQvDi0Sh2QTPXqMhyaAtzAr807TIyfQrmyg==", + "dev": true, + "requires": { + "@babel/code-frame": "^7.23.5", + "@babel/generator": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.22.6", + "@babel/parser": "^7.23.6", + "@babel/types": "^7.23.6", + "debug": "^4.3.1", "globals": "^11.1.0" } }, "@babel/types": { - "version": "7.20.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.20.7.tgz", - "integrity": "sha512-69OnhBxSSgK0OzTJai4kyPDiKTIe3j+ctaHdIGVbRahTLAT7L3R9oeXHC2aVSuGYt3cVnoAMDmOCgJ2yaiLMvg==", + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.23.6.tgz", + "integrity": "sha512-+uarb83brBzPKN38NX1MkB6vb6+mwvR6amUulqAE7ccQw1pEl+bCia9TbdG1lsnFP7lZySvUn37CHyXQdfTwzg==", "dev": true, "requires": { - "@babel/helper-string-parser": "^7.19.4", - "@babel/helper-validator-identifier": "^7.19.1", + "@babel/helper-string-parser": "^7.23.4", + "@babel/helper-validator-identifier": "^7.22.20", "to-fast-properties": "^2.0.0" } }, @@ -12802,9 +12818,9 @@ } }, "nanoid": { - "version": "3.3.4", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.4.tgz", - "integrity": "sha512-MqBkQh/OHTS2egovRtLk45wEyNXwF+cokD+1YPf9u5VfJiRdAiRwB2froX5Co9Rh20xs4siNPm8naNotSD6RBw==", + "version": "3.3.7", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", + "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==", "dev": true }, "negotiator": { @@ -13172,12 +13188,12 @@ } }, "postcss": { - "version": "8.4.21", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.21.tgz", - "integrity": "sha512-tP7u/Sn/dVxK2NnruI4H9BG+x+Wxz6oeZ1cJ8P6G/PZY0IKk4k/63TDsQf2kQq3+qoJeLm2kIBUNlZe3zgb4Zg==", + "version": "8.4.32", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.32.tgz", + "integrity": "sha512-D/kj5JNu6oo2EIy+XL/26JEDTlIbB8hw85G8StOE6L74RQAVVP5rej6wxCNqyMbR4RkPfqvezVbPw81Ngd6Kcw==", "dev": true, "requires": { - "nanoid": "^3.3.4", + "nanoid": "^3.3.7", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } diff --git a/src/Controller/InvoiceController.php b/src/Controller/InvoiceController.php index f5a60b5a..cf3c5b4b 100644 --- a/src/Controller/InvoiceController.php +++ b/src/Controller/InvoiceController.php @@ -19,6 +19,7 @@ use PhpOffice\PhpSpreadsheet\Writer\Csv; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; use Symfony\Component\Form\Extension\Core\Type\ChoiceType; +use Symfony\Component\HttpFoundation\JsonResponse; use Symfony\Component\HttpFoundation\Request; use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpKernel\Exception\HttpException; @@ -135,7 +136,7 @@ public function edit(Request $request, Invoice $invoice, InvoiceRepository $invo $form->add('client', null, [ 'label' => 'invoices.client', 'label_attr' => ['class' => 'label'], - 'row_attr' => ['class' => 'form-row'], + 'row_attr' => ['class' => 'form-row form-choices'], 'attr' => [ 'class' => 'form-element', 'data-choices-target' => 'choices', @@ -186,6 +187,21 @@ public function edit(Request $request, Invoice $invoice, InvoiceRepository $invo ]); } + #[Route('/{id}/generate-description', name: 'app_invoices_generate_description', methods: ['GET'])] + public function generateDescription(Invoice $invoice, $defaultInvoiceDescriptionTemplate): JsonResponse + { + $client = $invoice->getClient(); + + if (!empty($client) && !(empty($client->getProjectLeadName())) && !empty($client->getProjectLeadMail())) { + $description = $defaultInvoiceDescriptionTemplate; + + $description = str_replace('%name%', $client->getProjectLeadName() ?? '', $description); + $description = str_replace('%email%', $client->getProjectLeadMail() ?? '', $description); + } + + return new JsonResponse(['description' => $description ?? null]); + } + #[Route('/{id}', name: 'app_invoices_delete', methods: ['POST'])] public function delete(Request $request, Invoice $invoice, InvoiceRepository $invoiceRepository): Response { diff --git a/src/Controller/InvoiceEntryWorklogController.php b/src/Controller/InvoiceEntryWorklogController.php index 6b33536a..fed90a5d 100644 --- a/src/Controller/InvoiceEntryWorklogController.php +++ b/src/Controller/InvoiceEntryWorklogController.php @@ -47,7 +47,7 @@ public function worklogs(Request $request, Invoice $invoice, InvoiceEntry $invoi 'required' => false, 'label' => 'worklog.version', 'label_attr' => ['class' => 'label'], - 'row_attr' => ['class' => 'form-row'], + 'row_attr' => ['class' => 'form-row form-choices'], 'attr' => [ 'class' => 'form-element', 'data-choices-target' => 'choices', @@ -69,7 +69,7 @@ public function worklogs(Request $request, Invoice $invoice, InvoiceEntry $invoi 'required' => false, 'label' => 'worklog.epic', 'label_attr' => ['class' => 'label'], - 'row_attr' => ['class' => 'form-row'], + 'row_attr' => ['class' => 'form-row form-choices'], 'attr' => [ 'class' => 'form-element', 'data-choices-target' => 'choices', diff --git a/src/Entity/Client.php b/src/Entity/Client.php index d60ec7fe..cf27cbd8 100644 --- a/src/Entity/Client.php +++ b/src/Entity/Client.php @@ -57,6 +57,12 @@ class Client #[ORM\Column(length: 255, nullable: true)] private ?string $customerKey = null; + #[ORM\Column(length: 255, nullable: true)] + private ?string $projectLeadName = null; + + #[ORM\Column(length: 255, nullable: true)] + private ?string $projectLeadMail = null; + public function __construct() { $this->invoices = new ArrayCollection(); @@ -243,4 +249,28 @@ public function setCustomerKey(?string $customerKey): self return $this; } + + public function getProjectLeadName(): ?string + { + return $this->projectLeadName; + } + + public function setProjectLeadName(?string $projectLeadName): static + { + $this->projectLeadName = $projectLeadName; + + return $this; + } + + public function getProjectLeadMail(): ?string + { + return $this->projectLeadMail; + } + + public function setProjectLeadMail(?string $projectLeadMail): static + { + $this->projectLeadMail = $projectLeadMail; + + return $this; + } } diff --git a/src/Form/InvoiceNewType.php b/src/Form/InvoiceNewType.php index f84619e6..fa49f372 100644 --- a/src/Form/InvoiceNewType.php +++ b/src/Form/InvoiceNewType.php @@ -5,7 +5,6 @@ use App\Entity\Invoice; use Doctrine\ORM\EntityRepository; use Symfony\Component\Form\AbstractType; -use Symfony\Component\Form\Extension\Core\Type\TextareaType; use Symfony\Component\Form\FormBuilderInterface; use Symfony\Component\OptionsResolver\OptionsResolver; @@ -18,10 +17,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'required' => true, 'attr' => ['class' => 'form-element'], ]) - ->add('description', TextareaType::class, [ - 'required' => true, - 'attr' => ['class' => 'form-element', 'rows' => 4], - ]) ->add('project', null, [ 'required' => true, 'query_builder' => function (EntityRepository $er) { diff --git a/src/Form/InvoiceType.php b/src/Form/InvoiceType.php index b82756fa..3b3a9477 100644 --- a/src/Form/InvoiceType.php +++ b/src/Form/InvoiceType.php @@ -26,17 +26,17 @@ public function buildForm(FormBuilderInterface $builder, array $options): void 'help' => 'invoices.name_helptext', ]) ->add('description', TextareaType::class, [ - 'required' => true, + 'required' => false, 'label' => 'invoices.description', 'label_attr' => ['class' => 'label'], 'row_attr' => ['class' => 'form-row'], - 'attr' => ['class' => 'form-element', 'rows' => 4], + 'attr' => ['class' => 'form-element', 'data-generate-description-target' => 'description', 'rows' => 4], 'help' => 'invoices.description_helptext', ]) ->add('client', null, [ 'label' => 'invoices.client', 'label_attr' => ['class' => 'label'], - 'row_attr' => ['class' => 'form-row'], + 'row_attr' => ['class' => 'form-row form-choices'], 'attr' => ['class' => 'form-element'], 'help' => 'invoices.client_helptext', ]) diff --git a/src/Model/Invoices/ClientData.php b/src/Model/Invoices/ClientData.php index 9b128a6a..60af6844 100644 --- a/src/Model/Invoices/ClientData.php +++ b/src/Model/Invoices/ClientData.php @@ -16,4 +16,6 @@ class ClientData public ?string $psp = null; public ?string $ean = null; public ?string $salesChannel = null; + public ?string $projectLeadName = null; + public ?string $projectLeadMail = null; } diff --git a/src/Service/BillingService.php b/src/Service/BillingService.php index 3b597b38..62add3f0 100644 --- a/src/Service/BillingService.php +++ b/src/Service/BillingService.php @@ -348,6 +348,8 @@ public function syncProjects(callable $progressCallback): void $client->setStandardPrice($clientData->standardPrice); $client->setCustomerKey($clientData->customerKey); $client->setSalesChannel($clientData->salesChannel); + $client->setProjectLeadName($clientData->projectLeadName); + $client->setProjectLeadMail($clientData->projectLeadMail); if (!$client->getProjects()->contains($client)) { $client->addProject($project); diff --git a/src/Service/JiraApiService.php b/src/Service/JiraApiService.php index 9a491bcc..cc2424e3 100644 --- a/src/Service/JiraApiService.php +++ b/src/Service/JiraApiService.php @@ -1059,6 +1059,8 @@ public function getClientDataForProject(string $projectId): array $client->account = $account->key ?? null; $client->customerKey = $account->customer->key ?? null; $client->salesChannel = $account->category->key ?? null; + $client->projectLeadName = $account->lead->displayName ?? null; + $client->projectLeadMail = $account->lead->emailAddress ?? null; switch ($account->category->name ?? null) { case 'INTERN': diff --git a/templates/invoices/edit.html.twig b/templates/invoices/edit.html.twig index 61edbdac..b967dbec 100644 --- a/templates/invoices/edit.html.twig +++ b/templates/invoices/edit.html.twig @@ -9,7 +9,12 @@
{{ form_row(form.name) }} - {{ form_row(form.description) }} +
+ {% if invoice.client and invoice.client.projectLeadName and invoice.client.projectLeadMail %} + + {% endif %} + {{ form_row(form.description) }} +
{{ form_row(form.client) }} {{ form_row(form.periodFrom) }} {{ form_row(form.periodTo) }} @@ -35,6 +40,7 @@
  • {{ 'invoices.client_information'|trans }}
  • {% if invoice.client != null %}
  • {{ 'invoices.client_name'|trans }}: {{ invoice.client.name }}
  • +
  • {{ 'invoices.client_project_lead'|trans }}: {{ invoice.client.projectLeadName }} ({{ invoice.client.projectLeadMail }})
  • {{ 'invoices.client_contact'|trans }}: {{ invoice.client.contact }}
  • {{ 'invoices.client_standard_price'|trans }}: {{ invoice.client.standardPrice }}
  • {{ 'invoices.client_sales_channel'|trans }}: {{ invoice.client.salesChannel }}
  • diff --git a/templates/invoices/new.html.twig b/templates/invoices/new.html.twig index e2c27715..4be51f8f 100644 --- a/templates/invoices/new.html.twig +++ b/templates/invoices/new.html.twig @@ -8,7 +8,6 @@ {{ form_start(form) }}
    {{ form_row(form.name) }} - {{ form_row(form.description) }} {{ form_row(form.project) }} diff --git a/translations/messages.da.yaml b/translations/messages.da.yaml index 17924812..b25ccb4a 100644 --- a/translations/messages.da.yaml +++ b/translations/messages.da.yaml @@ -167,12 +167,12 @@ invoices: period_from_helptext: 'Dette felt skal udfyldes på eksterne fakturaer.' period_to_helptext: 'Dette felt skal udfyldes på eksterne fakturaer.' name_helptext: 'Vælg faktuens navn.' - description_helptext: 'Indsættes i tekstfeltet i fakturaen.' + description_helptext: 'Indsættes i tekstfeltet i fakturaen. Autoudfyld er tilgængelig, hvis der er valgt en kundekonto med en project lead.' project_helptext: 'Vælg projekt.' client_helptext: 'Vælg hvilken kundekonto fakturaen skal udstedes til.' + payer_account_helptext: 'Anvendes kun når der er tale om ITK-projektposteringer på PSP-element. Hvis valgt indsættes følgende i starten af tekstfeltet i fakturaen: "Betales af [KONTO]".' default_material_number_helptext: 'Dette bliver sat automatisk i alle fakturaindgange.' default_receiver_account_helptext: 'Dette bliver sat automatisk i alle fakturaindgange.' - payer_account_helptext: 'Hvis valgt indsættes følgende først i tekstfeltet i fakturaen: "Betales af [KONTO]".' client_type_internal: 'Intern' client_type_external: 'Ekstern' client_ean: 'EAN' @@ -226,6 +226,8 @@ invoices: action_export_selected: 'Eksporter valg' action_view: 'Se' list_exported_date: 'Eksportdato' + generate_description: 'Autoudfyld' + client_project_lead: 'Project Lead' required_fields_not_set_for_creating_entries: '"Til konto" og "Materialenummer" skal være valgt før der kan oprettes fakturalinjer.' material_number_enum: @@ -269,6 +271,7 @@ worklog: period_to_helptext: '' worker_helptext: '' version_helptext: '' + epic_helptext: '' owned_by_other: 'Anden faktura' action_save: 'Gem valgte worklogs' error_already_billed: 'Fejl: Indeholder worklog der allerede er faktureret. Prøv at genindlæse siden...'