diff --git a/.github/AL-Go-Settings.json b/.github/AL-Go-Settings.json index 1cf32f4f77..538e32ded9 100644 --- a/.github/AL-Go-Settings.json +++ b/.github/AL-Go-Settings.json @@ -1,14 +1,21 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json", "type": "PTE", "templateUrl": "https://github.com/microsoft/AL-Go-PTE@preview", "bcContainerHelperVersion": "preview", "runs-on": "windows-latest", "cacheImageName": "", "UsePsSession": false, - "artifact": "bcinsider/Sandbox/29.0.47140.0//latest", + "artifact": "bcinsider/Sandbox/29.0.49078.0//latest", "country": "base", "useProjectDependencies": true, + "incrementalBuilds": { + "onPush": false, + "onPull_Request": true, + "onSchedule": false, + "retentionDays": 30, + "mode": "modifiedApps" + }, "repoVersion": "29.0", "conditionalSettings": [ { @@ -131,7 +138,7 @@ ] }, "UpdateALGoSystemFilesEnvironment": "Official-Build", - "templateSha": "ab2dd7a59581956673267c0e5aca5e9f7a1f27da", + "templateSha": "ee6d3d7724013c45767d1b83db2b04f737def7d7", "commitOptions": { "messageSuffix": "Related to AB#539394", "pullRequestAutoMerge": true, diff --git a/.github/RELEASENOTES.copy.md b/.github/RELEASENOTES.copy.md index 9d9a5bbdfb..5d17892b63 100644 --- a/.github/RELEASENOTES.copy.md +++ b/.github/RELEASENOTES.copy.md @@ -1,11 +1,87 @@ -## preview +## v9.0 -Note that when using the preview version of AL-Go for GitHub, we recommend you Update your AL-Go system files, as soon as possible when informed that an update is available. +### Needs Context in Build job moved from environment variable to file + +`NeedsContext` is currently available as an environment variable in the build step of AL-Go. In some cases on repos with a large amount of projects, it's possible for this variable to exceed the max size GitHub allows for such variables. To work around this issue, we now place the contents of `NeedsContext` in a json file, where `NeedsContext` is the path to that file. + +If you have any custom processes that uses `NeedsContext`, those needs to be updated to now first read the contents of the json file. The structure of the json is identical to what was previously in the variable, so only extra step is to read the file. + +### Added support for workspace compilation + +With v28 of Business Central, the ALTool now also provides the ability to compile workspaces of apps. This has the added advantage that the ALTool can compute the dependency graph for the apps in the workspace and compile apps in parallel (if possible). For AL-Go projects with large amounts of apps that can save a lot of time. If you want to try this out you can enable it via the following setting + +```json + "workspaceCompilation": { + "enabled": true + } +``` + +By default apps are compiled sequentially but this can be changed via the parallelism property. This allows you to configure the maximum amount of parallel compilation processes. Set to 0 or -1 to use all available processors. + +```json + "workspaceCompilation": { + "enabled": true, + "parallelism": 4 + } +``` + +### Test Projects — split builds and tests for faster feedback + +AL-Go now supports **test projects**: a new project type that separates test execution from compilation. A test project does not build any apps itself — instead it depends on one or more regular projects, installs the apps they produce, and runs tests against them. + +This lets you re-run tests without waiting for a full recompilation, and makes it easy to organize large repositories where builds and test suites have different scopes or cadences. + +**Getting started** + +Add a `projectsToTest` setting to the project-level `.AL-Go/settings.json` of an empty project (no `appFolders` or `testFolders`): + +```json +{ + "projectsToTest": ["build/projects/MyProject"] +} +``` + +AL-Go will automatically: + +- Resolve the dependency so the test project always builds after its target project(s). +- Install the Test Runner, Test Framework, and Test Libraries into the container. +- Run all tests from the installed test apps. + +**Key rules** + +- A test project must **not** contain buildable code (no `appFolders`, `testFolders`, or `bcptTestFolders`). AL-Go will fail with a clear error if it detects both `projectsToTest` and buildable folders. +- A test project cannot depend on another test project. +- You can target multiple projects: `"projectsToTest": ["build/projects/ProjectA", "build/projects/ProjectB"]`. +- Use full project paths as they appear in the repository. + +### Improving error detection and build reliability when downloading project dependencies + +The `DownloadProjectDependencies` action now downloads app files from URLs specified in the `installApps` and `installTestApps` settings upfront, rather than validating URLs at build time. This change provides: + +- Earlier detection of inaccessible or misconfigured URLs +- Clearer error messages when secrets are missing or URLs are invalid +- Warnings for potential issues like duplicate filenames + +### Improve overall performance by postponing projects with no dependants + +The time it takes to build projects can vary significantly depending on factors such as whether you are using Linux or Windows, Containers or CompilerFolders, and whether apps are being published or tests are being run. + +By default, projects are built according to their dependency order. As soon as all dependencies for a project are satisfied, the project is added to the next layer of jobs. + +The new setting `postponeProjectInBuildOrder` allows you to delay long running jobs (f.ex. test runs) with no dependants until the final layer of the build order. This can improve overall build performance by preventing subsequent layers from waiting on projects that take longer to complete but are not required for further dependencies. ### Issues - Attempt to start docker service in case it is not running - NextMajor (v28) fails when downloading dependencies from NuGet-feed +- Issue 2084 Multiple artifacts failure if you re-run failed jobs after flaky tests +- Issue 2085 Projects that doesn't contain both Apps and TestApps are wrongly seen as not built. +- Issue 2086 Postpone jobs, which doesn't have any dependents to the end of the build order. +- Rework input handling of workflow 'Update AL-Go System Files' for trigger 'workflow_call' + +### New Settings + +- `postponeProjectInBuildOrder` is a new project setting, which will (if set to true) cause the project to be postponed until the last build job when possible. If set on test projects, then all tests can be deferred until all builds have succeeded. ## v8.3 diff --git a/.github/workflows/CICD.yaml b/.github/workflows/CICD.yaml index 29784d2ff6..6ecdc41486 100644 --- a/.github/workflows/CICD.yaml +++ b/.github/workflows/CICD.yaml @@ -51,7 +51,7 @@ jobs: trackALAlertsInGitHub: ${{ steps.SetALCodeAnalysisVar.outputs.trackALAlertsInGitHub }} steps: - name: Dump Workflow Information - uses: microsoft/AL-Go/Actions/DumpWorkflowInfo@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v9.0 with: shell: powershell @@ -62,13 +62,13 @@ jobs: - name: Initialize the workflow id: init - uses: microsoft/AL-Go/Actions/WorkflowInitialize@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowInitialize@v9.0 with: shell: powershell - name: Read settings id: ReadSettings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell get: type,powerPlatformSolutionFolder,useGitSubmodules,trackALAlertsInGitHub @@ -82,7 +82,7 @@ jobs: - name: Read submodules token id: ReadSubmodulesToken if: env.useGitSubmodules != 'false' && env.useGitSubmodules != '' - uses: microsoft/AL-Go/Actions/ReadSecrets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSecrets@v9.0 with: shell: powershell gitHubSecrets: ${{ toJson(secrets) }} @@ -103,7 +103,7 @@ jobs: - name: Determine Projects To Build id: determineProjectsToBuild - uses: microsoft/AL-Go/Actions/DetermineProjectsToBuild@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineProjectsToBuild@v9.0 with: shell: powershell maxBuildDepth: ${{ env.workflowDepth }} @@ -116,7 +116,7 @@ jobs: - name: Determine Delivery Target Secrets id: DetermineDeliveryTargetSecrets - uses: microsoft/AL-Go/Actions/DetermineDeliveryTargets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineDeliveryTargets@v9.0 with: shell: powershell projectsJson: '${{ steps.determineProjectsToBuild.outputs.ProjectsJson }}' @@ -124,7 +124,7 @@ jobs: - name: Read secrets id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSecrets@v9.0 with: shell: powershell gitHubSecrets: ${{ toJson(secrets) }} @@ -132,7 +132,7 @@ jobs: - name: Determine Delivery Targets id: DetermineDeliveryTargets - uses: microsoft/AL-Go/Actions/DetermineDeliveryTargets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineDeliveryTargets@v9.0 env: Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' with: @@ -142,7 +142,7 @@ jobs: - name: Determine Deployment Environments id: DetermineDeploymentEnvironments - uses: microsoft/AL-Go/Actions/DetermineDeploymentEnvironments@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineDeploymentEnvironments@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: @@ -158,21 +158,21 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell get: templateUrl - name: Read secrets id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSecrets@v9.0 with: shell: powershell gitHubSecrets: ${{ toJson(secrets) }} getSecrets: 'ghTokenWorkflow' - name: Check for updates to AL-Go system files - uses: microsoft/AL-Go/Actions/CheckForUpdates@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/CheckForUpdates@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: @@ -241,7 +241,7 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download artifacts - ErrorLogs - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 if: (success() || failure()) with: pattern: '*-*ErrorLogs-*' @@ -251,7 +251,7 @@ jobs: - name: Process AL Code Analysis Logs id: ProcessALCodeAnalysisLogs if: (success() || failure()) - uses: microsoft/AL-Go/Actions/ProcessALCodeAnalysisLogs@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ProcessALCodeAnalysisLogs@v9.0 with: shell: powershell @@ -280,41 +280,41 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: '.artifacts' - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell - name: Determine ArtifactUrl id: determineArtifactUrl - uses: microsoft/AL-Go/Actions/DetermineArtifactUrl@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineArtifactUrl@v9.0 with: shell: powershell - name: Setup Pages if: needs.Initialization.outputs.deployALDocArtifact == 1 - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Build Reference Documentation - uses: microsoft/AL-Go/Actions/BuildReferenceDocumentation@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/BuildReferenceDocumentation@v9.0 with: shell: powershell artifacts: '.artifacts' artifactUrl: ${{ env.artifact }} - name: Upload pages artifact - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: ".aldoc/_site/" - name: Deploy to GitHub Pages if: needs.Initialization.outputs.deployALDocArtifact == 1 id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 Deploy: needs: [ Initialization, Build1, Build ] @@ -336,12 +336,12 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: '.artifacts' - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: ${{ matrix.shell }} get: type,powerPlatformSolutionFolder @@ -355,7 +355,7 @@ jobs: - name: Read secrets id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSecrets@v9.0 with: shell: ${{ matrix.shell }} gitHubSecrets: ${{ toJson(secrets) }} @@ -363,7 +363,7 @@ jobs: - name: Deploy to Business Central id: Deploy - uses: microsoft/AL-Go/Actions/Deploy@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/Deploy@v9.0 env: Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' with: @@ -375,7 +375,7 @@ jobs: - name: Deploy to Power Platform if: env.type == 'PTE' && env.powerPlatformSolutionFolder != '' - uses: microsoft/AL-Go/Actions/DeployPowerPlatform@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DeployPowerPlatform@v9.0 env: Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' with: @@ -398,25 +398,25 @@ jobs: uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - name: Download artifacts - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 with: path: '.artifacts' - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell - name: Read secrets id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSecrets@v9.0 with: shell: powershell gitHubSecrets: ${{ toJson(secrets) }} getSecrets: '${{ matrix.deliveryTarget }}Context' - name: Deliver - uses: microsoft/AL-Go/Actions/Deliver@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/Deliver@v9.0 env: Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' with: @@ -436,7 +436,7 @@ jobs: - name: Finalize the workflow id: PostProcess - uses: microsoft/AL-Go/Actions/WorkflowPostProcess@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: diff --git a/.github/workflows/DeployReferenceDocumentation.yaml b/.github/workflows/DeployReferenceDocumentation.yaml index 041f7c39e4..b225006eee 100644 --- a/.github/workflows/DeployReferenceDocumentation.yaml +++ b/.github/workflows/DeployReferenceDocumentation.yaml @@ -30,24 +30,24 @@ jobs: - name: Initialize the workflow id: init - uses: microsoft/AL-Go/Actions/WorkflowInitialize@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowInitialize@v9.0 with: shell: powershell - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell - name: Determine ArtifactUrl id: determineArtifactUrl - uses: microsoft/AL-Go/Actions/DetermineArtifactUrl@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineArtifactUrl@v9.0 with: shell: powershell - name: Determine Deployment Environments id: DetermineDeploymentEnvironments - uses: microsoft/AL-Go/Actions/DetermineDeploymentEnvironments@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineDeploymentEnvironments@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: @@ -57,28 +57,28 @@ jobs: - name: Setup Pages if: steps.DetermineDeploymentEnvironments.outputs.deployALDocArtifact == 1 - uses: actions/configure-pages@983d7736d9b0ae728b81ab479565c72886d7745b # v5.0.0 + uses: actions/configure-pages@45bfe0192ca1faeb007ade9deae92b16b8254a0d # v6.0.0 - name: Build Reference Documentation - uses: microsoft/AL-Go/Actions/BuildReferenceDocumentation@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/BuildReferenceDocumentation@v9.0 with: shell: powershell artifacts: 'latest' artifactUrl: ${{ env.artifact }} - name: Upload pages artifact - uses: actions/upload-pages-artifact@7b1f4a764d45c48632c6b24a0339c27f5614fb0b # v4.0.0 + uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0 with: path: ".aldoc/_site/" - name: Deploy to GitHub Pages if: steps.DetermineDeploymentEnvironments.outputs.deployALDocArtifact == 1 id: deployment - uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 + uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0 - name: Finalize the workflow if: always() - uses: microsoft/AL-Go/Actions/WorkflowPostProcess@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: diff --git a/.github/workflows/DocumentationMaintenance.yaml b/.github/workflows/DocumentationMaintenance.yaml new file mode 100644 index 0000000000..e92c3fc6e6 --- /dev/null +++ b/.github/workflows/DocumentationMaintenance.yaml @@ -0,0 +1,157 @@ +name: 'Documentation Maintenance' + +on: + pull_request: + branches: ['main', 'releases/*', 'features/*'] + paths: ['src/**/*.al'] + +concurrency: + group: docs-${{ github.event.pull_request.number }} + cancel-in-progress: true + +permissions: + actions: read + contents: read + pull-requests: write + +jobs: + AuditDocs: + if: github.event.pull_request.head.repo.full_name == github.event.pull_request.base.repo.full_name + runs-on: ubuntu-latest + timeout-minutes: 30 + env: + COPILOT_GITHUB_TOKEN: ${{ secrets.COPILOT_GH_TOKEN }} + steps: + - name: Check for existing docs review + id: dedup + env: + GH_TOKEN: ${{ github.token }} + run: | + if [ "${{ vars.RUN_DOCS_CHECK_ON_EVERY_ITERATION }}" = "true" ]; then + echo "Dedup check disabled via RUN_DOCS_CHECK_ON_EVERY_ITERATION variable." + echo "skip=false" >> "$GITHUB_OUTPUT" + exit 0 + fi + + EXISTING=$(gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews \ + --jq '[.[] | select(.body | startswith("AL Documentation Audit"))] | length' 2>/dev/null || echo "0") + if [ "$EXISTING" != "0" ]; then + echo "Docs review already posted, skipping." + echo "skip=true" >> "$GITHUB_OUTPUT" + else + echo "skip=false" >> "$GITHUB_OUTPUT" + fi + + - name: Checkout + if: steps.dedup.outputs.skip != 'true' + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + fetch-depth: 0 + ref: ${{ github.event.pull_request.head.sha }} + + - name: Setup Node.js + if: steps.dedup.outputs.skip != 'true' + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + + - name: Install Copilot CLI + if: steps.dedup.outputs.skip != 'true' + run: npm install -g @github/copilot + + - name: Install al-docs plugin + if: steps.dedup.outputs.skip != 'true' + run: | + copilot plugin install ./tools/al-docs-plugin + PLUGIN_DIR="$HOME/.copilot/installed-plugins" + mkdir -p ~/.copilot + echo "{\"trusted_folders\":[\"$PLUGIN_DIR\"]}" > ~/.copilot/config.json + + - name: Find changed apps + if: steps.dedup.outputs.skip != 'true' + id: scope + run: | + MERGE_BASE=$(git merge-base ${{ github.event.pull_request.base.sha }} ${{ github.event.pull_request.head.sha }}) + CHANGED=$(git diff --name-only "$MERGE_BASE"..${{ github.event.pull_request.head.sha }} -- '*.al') + + if [ -z "$CHANGED" ]; then + echo "count=0" >> "$GITHUB_OUTPUT" + exit 0 + fi + + APPS=$(echo "$CHANGED" | while read -r f; do + d=$(dirname "$f") + while [ "$d" != "." ]; do + [ -f "$d/app.json" ] && echo "$d" && break + d=$(dirname "$d") + done + done | sort -u) + + COUNT=$(echo "$APPS" | wc -l | tr -d ' ') + echo "count=$COUNT" >> "$GITHUB_OUTPUT" + echo "apps<> "$GITHUB_OUTPUT" + echo "$APPS" >> "$GITHUB_OUTPUT" + echo "EOF" >> "$GITHUB_OUTPUT" + + - name: Run al-docs audit + if: steps.dedup.outputs.skip != 'true' && steps.scope.outputs.count != '0' + id: audit + run: | + while IFS= read -r APP_PATH; do + [ -z "$APP_PATH" ] && continue + APP_NAME=$(jq -r '.name // "unknown"' "$APP_PATH/app.json" 2>/dev/null || echo "unknown") + REPORT_FILE="/tmp/audit-${APP_NAME// /-}.md" + + echo "Auditing $APP_NAME at $APP_PATH..." + + timeout 600 copilot -p \ + "Run the al-docs skill in audit mode on the app at $APP_PATH. After the audit, summarize in 2-3 sentences: coverage percentage, what is missing." \ + --model claude-opus-4.6 \ + --no-ask-user \ + --autopilot \ + --allow-tool=read \ + --allow-tool=glob \ + --allow-tool=grep \ + --allow-tool='shell(git log:read)' \ + --allow-tool='shell(git diff:read)' \ + --allow-tool='shell(find:read)' \ + --allow-tool='shell(wc:read)' \ + --allow-tool='shell(sort:read)' \ + --allow-tool='shell(uniq:read)' \ + --allow-tool='shell(jq:read)' \ + --share="$REPORT_FILE" || true + + done <<< '${{ steps.scope.outputs.apps }}' + + # Combine all audit reports + cat /tmp/audit-*.md > /tmp/full-report.md 2>/dev/null || true + + - name: Request docs changes + if: steps.dedup.outputs.skip != 'true' && steps.scope.outputs.count != '0' + env: + GH_TOKEN: ${{ github.token }} + run: | + if copilot -p "Read /tmp/full-report.md. Does the audit show documentation gaps that need to be fixed? Reply only YES or NO." \ + -s --no-ask-user --allow-tool=read | grep -qi "yes"; then + + # Build comment with deterministic template -- copilot only extracts coverage % + BODY="AL Documentation Audit"$'\n\n' + BODY+="Documentation gaps were detected in the following apps:"$'\n\n' + + for REPORT in /tmp/audit-*.md; do + [ -f "$REPORT" ] || continue + # App name from the report filename (set during audit step from app.json) + APP=$(basename "$REPORT" .md | sed 's/^audit-//') + COV=$(copilot -p "Read $REPORT. What is the documentation coverage number between 0 and 100? Reply with ONLY the number, nothing else." \ + -s --no-ask-user --allow-tool=read 2>&1 | grep -oE '[0-9]+' | head -1) + [ "$COV" = "100" ] && continue + BODY+="- **${APP}**: ${COV:-unknown}% documentation coverage"$'\n' + done + + BODY+=$'\n'"To generate documentation, run \`/al-docs init\` or \`/al-docs update\` using [GitHub Copilot CLI](https://github.com/github/copilot-cli) or [Claude Code](https://claude.com/claude-code)."$'\n' + BODY+="_This review is for awareness to help keep documentation in sync with code changes. It is okay to dismiss this request._" + + gh api repos/${{ github.repository }}/pulls/${{ github.event.pull_request.number }}/reviews \ + -f body="$BODY" \ + -f event="COMMENT" + else + echo "No documentation gaps found." + fi diff --git a/.github/workflows/IncrementVersionNumber.yaml b/.github/workflows/IncrementVersionNumber.yaml index 634ad57517..a9a6a1c08d 100644 --- a/.github/workflows/IncrementVersionNumber.yaml +++ b/.github/workflows/IncrementVersionNumber.yaml @@ -48,7 +48,7 @@ jobs: pull-requests: write steps: - name: Dump Workflow Information - uses: microsoft/AL-Go/Actions/DumpWorkflowInfo@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v9.0 with: shell: powershell @@ -57,24 +57,24 @@ jobs: - name: Initialize the workflow id: init - uses: microsoft/AL-Go/Actions/WorkflowInitialize@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowInitialize@v9.0 with: shell: powershell - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell - name: Validate Workflow Input if: ${{ github.event_name == 'workflow_dispatch' }} - uses: microsoft/AL-Go/Actions/ValidateWorkflowInput@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ValidateWorkflowInput@v9.0 with: shell: powershell - name: Read secrets id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSecrets@v9.0 with: shell: powershell gitHubSecrets: ${{ toJson(secrets) }} @@ -82,7 +82,7 @@ jobs: useGhTokenWorkflowForPush: '${{ github.event.inputs.useGhTokenWorkflow }}' - name: Increment Version Number - uses: microsoft/AL-Go/Actions/IncrementVersionNumber@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/IncrementVersionNumber@v9.0 with: shell: powershell token: ${{ steps.ReadSecrets.outputs.TokenForPush }} @@ -93,7 +93,7 @@ jobs: - name: Finalize the workflow if: always() - uses: microsoft/AL-Go/Actions/WorkflowPostProcess@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: diff --git a/.github/workflows/PowerShell.yaml b/.github/workflows/PowerShell.yaml index 2773b8f761..1b960f7a61 100644 --- a/.github/workflows/PowerShell.yaml +++ b/.github/workflows/PowerShell.yaml @@ -22,7 +22,7 @@ jobs: security-events: write # for github/codeql-action/upload-sarif to upload SARIF results steps: - name: Harden Runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0 with: egress-policy: audit diff --git a/.github/workflows/PullRequestHandler.yaml b/.github/workflows/PullRequestHandler.yaml index 82aa02b1f4..9a6f75f8c6 100644 --- a/.github/workflows/PullRequestHandler.yaml +++ b/.github/workflows/PullRequestHandler.yaml @@ -31,7 +31,7 @@ jobs: if: (github.event.pull_request.base.repo.full_name != github.event.pull_request.head.repo.full_name) && (github.event_name != 'pull_request') runs-on: windows-latest steps: - - uses: microsoft/AL-Go/Actions/VerifyPRChanges@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + - uses: microsoft/AL-Go-Actions/VerifyPRChanges@v9.0 Initialization: needs: [ PregateCheck ] @@ -49,7 +49,7 @@ jobs: trackALAlertsInGitHub: ${{ steps.SetALCodeAnalysisVar.outputs.trackALAlertsInGitHub }} steps: - name: Dump Workflow Information - uses: microsoft/AL-Go/Actions/DumpWorkflowInfo@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v9.0 with: shell: powershell @@ -61,13 +61,13 @@ jobs: - name: Initialize the workflow id: init - uses: microsoft/AL-Go/Actions/WorkflowInitialize@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowInitialize@v9.0 with: shell: powershell - name: Read settings id: ReadSettings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell get: shortLivedArtifactsRetentionDays,trackALAlertsInGitHub @@ -86,7 +86,7 @@ jobs: - name: Determine Projects To Build id: determineProjectsToBuild - uses: microsoft/AL-Go/Actions/DetermineProjectsToBuild@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineProjectsToBuild@v9.0 with: shell: powershell maxBuildDepth: ${{ env.workflowDepth }} @@ -155,7 +155,7 @@ jobs: ref: ${{ format('refs/pull/{0}/head', github.event.pull_request.number) }} - name: Download artifacts - ErrorLogs - uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131 # v7.0.0 + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 if: (success() || failure()) with: pattern: '*-*ErrorLogs-*' @@ -165,7 +165,7 @@ jobs: - name: Process AL Code Analysis Logs id: ProcessALCodeAnalysisLogs if: (success() || failure()) - uses: microsoft/AL-Go/Actions/ProcessALCodeAnalysisLogs@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ProcessALCodeAnalysisLogs@v9.0 with: shell: powershell @@ -186,7 +186,7 @@ jobs: steps: - name: Pull Request Status Check id: PullRequestStatusCheck - uses: microsoft/AL-Go/Actions/PullRequestStatusCheck@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/PullRequestStatusCheck@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: @@ -194,7 +194,7 @@ jobs: - name: Finalize the workflow id: PostProcess - uses: microsoft/AL-Go/Actions/WorkflowPostProcess@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v9.0 if: success() || failure() env: GITHUB_TOKEN: ${{ github.token }} diff --git a/.github/workflows/SubmitStabilityJobs.yaml b/.github/workflows/SubmitStabilityJobs.yaml index 14239e98b7..c76bf1be0d 100644 --- a/.github/workflows/SubmitStabilityJobs.yaml +++ b/.github/workflows/SubmitStabilityJobs.yaml @@ -32,7 +32,7 @@ jobs: branch: ${{ fromJson(needs.GetBranches.outputs.officialBranches) }} fail-fast: false steps: - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: app-id: ${{ vars.APP_ID }} diff --git a/.github/workflows/Troubleshooting.yaml b/.github/workflows/Troubleshooting.yaml index a5b5576aa1..8f9e0e3903 100644 --- a/.github/workflows/Troubleshooting.yaml +++ b/.github/workflows/Troubleshooting.yaml @@ -30,7 +30,7 @@ jobs: lfs: true - name: Troubleshooting - uses: microsoft/AL-Go/Actions/Troubleshooting@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/Troubleshooting@v9.0 with: shell: powershell gitHubSecrets: ${{ toJson(secrets) }} diff --git a/.github/workflows/UpdateALGoProjects.yaml b/.github/workflows/UpdateALGoProjects.yaml index 7d42a5502b..1a6b1df968 100644 --- a/.github/workflows/UpdateALGoProjects.yaml +++ b/.github/workflows/UpdateALGoProjects.yaml @@ -20,7 +20,7 @@ jobs: updateBranches: ${{ steps.getOfficialBranches.outputs.branchesJson }} steps: - name: Harden Runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0 with: egress-policy: audit @@ -48,7 +48,7 @@ jobs: with: ref: ${{ matrix.branch }} - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: app-id: ${{ vars.APP_ID }} diff --git a/.github/workflows/UpdateBCArtifactVersion.yaml b/.github/workflows/UpdateBCArtifactVersion.yaml index 8374b15ee3..d5ae235e35 100644 --- a/.github/workflows/UpdateBCArtifactVersion.yaml +++ b/.github/workflows/UpdateBCArtifactVersion.yaml @@ -20,7 +20,7 @@ jobs: updateBranches: ${{ steps.getOfficialBranches.outputs.branchesJson }} steps: - name: Harden Runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0 with: egress-policy: audit @@ -48,7 +48,7 @@ jobs: with: ref: ${{ matrix.branch }} - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: app-id: ${{ vars.APP_ID }} diff --git a/.github/workflows/UpdateGitHubGoSystemFiles.yaml b/.github/workflows/UpdateGitHubGoSystemFiles.yaml index 21e77cf89a..b3328d09ba 100644 --- a/.github/workflows/UpdateGitHubGoSystemFiles.yaml +++ b/.github/workflows/UpdateGitHubGoSystemFiles.yaml @@ -21,6 +21,10 @@ on: default: '' workflow_call: inputs: + caller: + description: Name of the calling workflow (use github.workflow as value when calling) + type: string + required: true templateUrl: description: Template Repository URL (current is https://github.com/microsoft/AL-Go-PTE@preview) type: string @@ -52,6 +56,7 @@ defaults: shell: powershell env: + WorkflowEventName: ${{ inputs.caller && 'workflow_call' || github.event_name }} ALGoOrgSettings: ${{ vars.ALGoOrgSettings }} ALGoRepoSettings: ${{ vars.ALGoRepoSettings }} @@ -68,22 +73,23 @@ jobs: - name: Read settings id: ReadSettings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell get: templateUrl - name: Get Workflow Multi-Run Branches id: GetBranches - uses: microsoft/AL-Go/Actions/GetWorkflowMultiRunBranches@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/GetWorkflowMultiRunBranches@v9.0 with: shell: powershell - includeBranches: ${{ github.event.inputs.includeBranches }} + workflowEventName: ${{ env.WorkflowEventName }} + includeBranches: ${{ inputs.includeBranches }} - name: Determine Template URL id: DetermineTemplateUrl env: - TemplateUrlAsInput: '${{ github.event.inputs.templateUrl }}' + TemplateUrlAsInput: '${{ inputs.templateUrl }}' run: | $templateUrl = $env:templateUrl # Available from ReadSettings step if ($ENV:TemplateUrlAsInput) { @@ -105,7 +111,7 @@ jobs: steps: - name: Dump Workflow Information - uses: microsoft/AL-Go/Actions/DumpWorkflowInfo@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DumpWorkflowInfo@v9.0 with: shell: powershell @@ -116,19 +122,19 @@ jobs: - name: Initialize the workflow id: init - uses: microsoft/AL-Go/Actions/WorkflowInitialize@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowInitialize@v9.0 with: shell: powershell - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: powershell get: commitOptions - name: Read secrets id: ReadSecrets - uses: microsoft/AL-Go/Actions/ReadSecrets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSecrets@v9.0 with: shell: powershell gitHubSecrets: ${{ toJson(secrets) }} @@ -136,12 +142,12 @@ jobs: - name: Calculate Commit Options env: - directCommit: '${{ github.event.inputs.directCommit }}' - downloadLatest: '${{ github.event.inputs.downloadLatest }}' + directCommit: '${{ inputs.directCommit }}' + downloadLatest: '${{ inputs.downloadLatest }}' run: | $errorActionPreference = "Stop"; $ProgressPreference = "SilentlyContinue"; Set-StrictMode -Version 2.0 - if('${{ github.event_name }}' -in 'workflow_dispatch', 'workflow_call') { - Write-Host "Using inputs from ${{ github.event_name }} event" + if($env:WorkflowEventName -in 'workflow_dispatch', 'workflow_call') { + Write-Host "Using inputs from $($env:WorkflowEventName) event" $directCommit = $env:directCommit $downloadLatest = $env:downloadLatest } @@ -155,7 +161,7 @@ jobs: Add-Content -Encoding UTF8 -Path $env:GITHUB_ENV -Value "downloadLatest=$downloadLatest" - name: Update AL-Go system files - uses: microsoft/AL-Go/Actions/CheckForUpdates@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/CheckForUpdates@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: @@ -169,7 +175,7 @@ jobs: - name: Finalize the workflow if: always() - uses: microsoft/AL-Go/Actions/WorkflowPostProcess@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/WorkflowPostProcess@v9.0 env: GITHUB_TOKEN: ${{ github.token }} with: diff --git a/.github/workflows/UpdatePackageVersions.yaml b/.github/workflows/UpdatePackageVersions.yaml index 4cf0da4161..045006ef94 100644 --- a/.github/workflows/UpdatePackageVersions.yaml +++ b/.github/workflows/UpdatePackageVersions.yaml @@ -20,7 +20,7 @@ jobs: updateBranches: ${{ steps.getOfficialBranches.outputs.branchesJson }} steps: - name: Harden Runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0 with: egress-policy: audit @@ -48,7 +48,7 @@ jobs: with: ref: ${{ matrix.branch }} - - uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1 + - uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 id: app-token with: app-id: ${{ vars.APP_ID }} diff --git a/.github/workflows/WorkitemValidation.yaml b/.github/workflows/WorkitemValidation.yaml index 2e106c0af6..bda8fa6cf4 100644 --- a/.github/workflows/WorkitemValidation.yaml +++ b/.github/workflows/WorkitemValidation.yaml @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Harden Runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0 with: egress-policy: audit @@ -40,7 +40,7 @@ jobs: needs: GitHubIssueValidation steps: - name: Harden Runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0 with: egress-policy: audit diff --git a/.github/workflows/_BuildALGoProject.yaml b/.github/workflows/_BuildALGoProject.yaml index fa44c782c2..8a7ab6aa52 100644 --- a/.github/workflows/_BuildALGoProject.yaml +++ b/.github/workflows/_BuildALGoProject.yaml @@ -104,16 +104,16 @@ jobs: lfs: true - name: Read settings - uses: microsoft/AL-Go/Actions/ReadSettings@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSettings@v9.0 with: shell: ${{ inputs.shell }} project: ${{ inputs.project }} buildMode: ${{ inputs.buildMode }} - get: useCompilerFolder,keyVaultCodesignCertificateName,doNotSignApps,doNotRunTests,doNotRunBcptTests,doNotRunpageScriptingTests,artifact,generateDependencyArtifact,trustedSigning,useGitSubmodules,trackALAlertsInGitHub + get: useCompilerFolder,workspaceCompilation,keyVaultCodesignCertificateName,doNotSignApps,doNotRunTests,doNotRunBcptTests,doNotRunpageScriptingTests,artifact,generateDependencyArtifact,trustedSigning,useGitSubmodules,trackALAlertsInGitHub - name: Determine whether to build project id: DetermineBuildProject - uses: microsoft/AL-Go/Actions/DetermineBuildProject@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineBuildProject@v9.0 with: shell: ${{ inputs.shell }} skippedProjectsJson: ${{ inputs.skippedProjectsJson }} @@ -123,7 +123,7 @@ jobs: - name: Read secrets id: ReadSecrets if: steps.DetermineBuildProject.outputs.BuildIt == 'True' - uses: microsoft/AL-Go/Actions/ReadSecrets@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/ReadSecrets@v9.0 with: shell: ${{ inputs.shell }} gitHubSecrets: ${{ toJson(secrets) }} @@ -141,14 +141,14 @@ jobs: - name: Determine ArtifactUrl id: determineArtifactUrl if: steps.DetermineBuildProject.outputs.BuildIt == 'True' - uses: microsoft/AL-Go/Actions/DetermineArtifactUrl@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DetermineArtifactUrl@v9.0 with: shell: ${{ inputs.shell }} project: ${{ inputs.project }} - name: Cache Business Central Artifacts if: steps.DetermineBuildProject.outputs.BuildIt == 'True' && env.useCompilerFolder == 'True' && inputs.useArtifactCache && env.artifactCacheKey - uses: actions/cache@cdf6c1fa76f9f475f3d7449005a359c84ca0f306 # v5.0.3 + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 with: path: ${{ runner.temp }}/.artifactcache key: ${{ env.artifactCacheKey }} @@ -156,7 +156,7 @@ jobs: - name: Download Project Dependencies id: DownloadProjectDependencies if: steps.DetermineBuildProject.outputs.BuildIt == 'True' - uses: microsoft/AL-Go/Actions/DownloadProjectDependencies@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/DownloadProjectDependencies@v9.0 env: Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' with: @@ -166,13 +166,40 @@ jobs: projectDependenciesJson: ${{ inputs.projectDependenciesJson }} baselineWorkflowRunId: ${{ inputs.baselineWorkflowRunId }} + - name: Compile Apps + id: compile + uses: microsoft/AL-Go-Actions/CompileApps@v9.0 + if: steps.DetermineBuildProject.outputs.BuildIt == 'True' && fromJson(env.workspaceCompilation).enabled == true + env: + Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' + BuildMode: ${{ inputs.buildMode }} + with: + shell: ${{ inputs.shell }} + artifact: ${{ env.artifact }} + project: ${{ inputs.project }} + buildMode: ${{ inputs.buildMode }} + dependencyAppsJson: ${{ steps.DownloadProjectDependencies.outputs.DownloadedApps }} + dependencyTestAppsJson: ${{ steps.DownloadProjectDependencies.outputs.DownloadedTestApps }} + baselineWorkflowRunId: ${{ inputs.baselineWorkflowRunId }} + baselineWorkflowSHA: ${{ inputs.baselineWorkflowSHA }} + + - name: Save needs context to file + shell: pwsh + run: | + $nc = @' + ${{ inputs.needsContext }} + '@ + $needsContextPath = Join-Path $ENV:RUNNER_TEMP 'needsContext.json' + [System.IO.File]::WriteAllText($needsContextPath, $nc.Trim()) + Add-Content -Encoding UTF8 -Path $env:GITHUB_ENV -Value "NeedsContext=$needsContextPath" + - name: Build - uses: microsoft/AL-Go/Actions/RunPipeline@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/RunPipeline@v9.0 if: steps.DetermineBuildProject.outputs.BuildIt == 'True' env: Secrets: '${{ steps.ReadSecrets.outputs.Secrets }}' BuildMode: ${{ inputs.buildMode }} - NeedsContext: ${{ inputs.needsContext }} + NeedsContext: ${{ env.NeedsContext }} with: shell: ${{ inputs.shell }} artifact: ${{ env.artifact }} @@ -186,7 +213,7 @@ jobs: - name: Sign id: sign if: steps.DetermineBuildProject.outputs.BuildIt == 'True' && inputs.signArtifacts && env.doNotSignApps == 'False' && (env.keyVaultCodesignCertificateName != '' || (fromJson(env.trustedSigning).Endpoint != '' && fromJson(env.trustedSigning).Account != '' && fromJson(env.trustedSigning).CertificateProfile != '')) && (hashFiles(format('{0}/.buildartifacts/Apps/*.app',inputs.project)) != '') - uses: microsoft/AL-Go/Actions/Sign@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/Sign@v9.0 with: shell: ${{ inputs.shell }} azureCredentialsJson: '${{ fromJson(steps.ReadSecrets.outputs.Secrets).AZURE_CREDENTIALS }}' @@ -194,7 +221,7 @@ jobs: - name: Calculate Artifact names id: calculateArtifactsNames - uses: microsoft/AL-Go/Actions/CalculateArtifactNames@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/CalculateArtifactNames@v9.0 if: success() || failure() with: shell: ${{ inputs.shell }} @@ -203,7 +230,7 @@ jobs: suffix: ${{ inputs.artifactsNameSuffix }} - name: Publish artifacts - apps - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: inputs.artifactsRetentionDays >= 0 && (hashFiles(format('{0}/.buildartifacts/Apps/*',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.AppsArtifactsName }} @@ -212,7 +239,7 @@ jobs: retention-days: ${{ inputs.artifactsRetentionDays }} - name: Publish artifacts - dependencies - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: inputs.artifactsRetentionDays >= 0 && env.generateDependencyArtifact == 'True' && (hashFiles(format('{0}/.buildartifacts/Dependencies/*',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.DependenciesArtifactsName }} @@ -221,7 +248,7 @@ jobs: retention-days: ${{ inputs.artifactsRetentionDays }} - name: Publish artifacts - test apps - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: inputs.artifactsRetentionDays >= 0 && (hashFiles(format('{0}/.buildartifacts/TestApps/*',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.TestAppsArtifactsName }} @@ -230,7 +257,7 @@ jobs: retention-days: ${{ inputs.artifactsRetentionDays }} - name: Publish artifacts - build output - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: (success() || failure()) && (hashFiles(format('{0}/BuildOutput.txt',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.BuildOutputArtifactsName }} @@ -238,7 +265,7 @@ jobs: if-no-files-found: ignore - name: Publish artifacts - container event log - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: (failure()) && (hashFiles(format('{0}/ContainerEventLog.evtx',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.ContainerEventLogArtifactsName }} @@ -246,7 +273,7 @@ jobs: if-no-files-found: ignore - name: Publish artifacts - test results - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: (success() || failure()) && (hashFiles(format('{0}/.buildartifacts/TestResults.xml',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.TestResultsArtifactsName }} @@ -254,7 +281,7 @@ jobs: if-no-files-found: ignore - name: Publish artifacts - bcpt test results - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: (success() || failure()) && (hashFiles(format('{0}/.buildartifacts/bcptTestResults.json',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.BcptTestResultsArtifactsName }} @@ -262,7 +289,7 @@ jobs: if-no-files-found: ignore - name: Publish artifacts - page scripting test results - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: (success() || failure()) && (hashFiles(format('{0}/.buildartifacts/PageScriptingTestResults.xml',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.PageScriptingTestResultsArtifactsName }} @@ -270,7 +297,7 @@ jobs: if-no-files-found: ignore - name: Publish artifacts - page scripting test result details - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: (success() || failure()) && (hashFiles(format('{0}/.buildartifacts/PageScriptingTestResultDetails/*',inputs.project)) != '') with: name: ${{ steps.calculateArtifactsNames.outputs.PageScriptingTestResultDetailsArtifactsName }} @@ -278,7 +305,7 @@ jobs: if-no-files-found: ignore - name: Publish artifacts - ErrorLogs - uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0 + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 if: (success() || failure()) && inputs.artifactsRetentionDays >= 0 && (hashFiles(format('{0}/.buildartifacts/ErrorLogs/*',inputs.project)) != '') && env.trackALAlertsInGitHub == 'True' with: name: ${{ steps.calculateArtifactsNames.outputs.ErrorLogsArtifactsName }} @@ -289,7 +316,7 @@ jobs: - name: Analyze Test Results id: analyzeTestResults if: (success() || failure()) && env.doNotRunTests == 'False' - uses: microsoft/AL-Go/Actions/AnalyzeTests@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/AnalyzeTests@v9.0 with: shell: ${{ inputs.shell }} project: ${{ inputs.project }} @@ -298,7 +325,7 @@ jobs: - name: Analyze BCPT Test Results id: analyzeTestResultsBCPT if: (success() || failure()) && env.doNotRunBcptTests == 'False' - uses: microsoft/AL-Go/Actions/AnalyzeTests@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/AnalyzeTests@v9.0 with: shell: ${{ inputs.shell }} project: ${{ inputs.project }} @@ -307,7 +334,7 @@ jobs: - name: Analyze Page Scripting Test Results id: analyzeTestResultsPageScripting if: (success() || failure()) && env.doNotRunpageScriptingTests == 'False' - uses: microsoft/AL-Go/Actions/AnalyzeTests@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/AnalyzeTests@v9.0 with: shell: ${{ inputs.shell }} project: ${{ inputs.project }} @@ -315,7 +342,7 @@ jobs: - name: Cleanup if: always() && steps.DetermineBuildProject.outputs.BuildIt == 'True' - uses: microsoft/AL-Go/Actions/PipelineCleanup@91c2f1bab7959cffc66fd9513a1d83ec9f641e30 + uses: microsoft/AL-Go-Actions/PipelineCleanup@v9.0 with: shell: ${{ inputs.shell }} project: ${{ inputs.project }} diff --git a/.github/workflows/ai-triage-dispatch.yml b/.github/workflows/ai-triage-dispatch.yml new file mode 100644 index 0000000000..70ed8c59dc --- /dev/null +++ b/.github/workflows/ai-triage-dispatch.yml @@ -0,0 +1,26 @@ +name: Dispatch AI Triage + +on: + issues: + types: [opened, labeled] + +permissions: {} + +jobs: + dispatch: + if: >- + github.event.issue.state == 'open' && ( + github.event.action == 'opened' || + github.event.label.name == 'ai-triage' + ) + runs-on: ubuntu-latest + steps: + - name: Trigger triage on BCAppsTriage + env: + TOKEN: ${{ secrets.TRIAGE_DISPATCH_TOKEN }} + run: | + curl -s -X POST \ + -H "Authorization: token $TOKEN" \ + -H "Accept: application/vnd.github+json" \ + https://api.github.com/repos/microsoft/BCAppsTriage/dispatches \ + -d '{"event_type":"issue-triage","client_payload":{"issue_number":${{ github.event.issue.number }}}}' diff --git a/.github/workflows/scorecard-analysis.yml b/.github/workflows/scorecard-analysis.yml index 90217a30c9..c39998ce5d 100644 --- a/.github/workflows/scorecard-analysis.yml +++ b/.github/workflows/scorecard-analysis.yml @@ -18,7 +18,7 @@ jobs: steps: - name: Harden Runner - uses: step-security/harden-runner@a90bcbc6539c36a85cdfeb73f7e2f433735f215b # v2.15.0 + uses: step-security/harden-runner@6c3c2f2c1c457b00c10c4848d6f5491db3b629df # v2.18.0 with: egress-policy: audit diff --git a/README.md b/README.md index 37e9b26cf9..77a1b27010 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ ## The Repository This repository contains the source code for several _Business Central_ applications, developed by Microsoft. -The source code in this repository is available to everyone under the standard [MIT license](https://github.com/microsoft/vscode/blob/main/LICENSE.txt). +The source code in this repository is available to everyone under the standard [MIT license](https://github.com/microsoft/BCApps/blob/main/LICENSE). ✨ **BCApps run on [AL-Go for GitHub](https://github.com/microsoft/AL-Go)** ✨ @@ -24,7 +24,7 @@ Want to learn more about how to contribute to this repository? Please refer to o - `src` folder stores the source code for Business Central application, such as `System Application` and developer tools like *test libraries*, *test runner* and others. - `src\rulesets` is where the rulesets for building the applications are stored. -- `build` folder contain the *magic* around how the applications are build. +- `build` folder contains the *magic* around how the applications are built. - `build\projects` is where all AL-Go projects are defined. diff --git a/build/Packages.json b/build/Packages.json index a42084497f..2ac5d6eb20 100644 --- a/build/Packages.json +++ b/build/Packages.json @@ -1,10 +1,10 @@ { "Microsoft.Dynamics.BusinessCentral.Translations": { - "Version": "29.0.26061.1", + "Version": "29.0.26110.3", "Source": "NuGet.org" }, "AppBaselines-BCArtifacts": { - "Version": "28.1.47012.0", + "Version": "28.1.49255.0", "Source": "BCArtifacts", "_comment": "Used to fetch app baselines from BC artifacts" } diff --git a/build/projects/Apps (W1)/.AL-Go/RunTestsInBcContainer.ps1 b/build/projects/Apps (W1)/.AL-Go/RunTestsInBcContainer.ps1 index 32907415a1..a09dda86e2 100644 --- a/build/projects/Apps (W1)/.AL-Go/RunTestsInBcContainer.ps1 +++ b/build/projects/Apps (W1)/.AL-Go/RunTestsInBcContainer.ps1 @@ -2,6 +2,12 @@ Param( [Hashtable]$parameters ) Import-Module (Join-Path $PSScriptRoot "../../../scripts/EnlistmentHelperFunctions.psm1" -Resolve) +Import-Module (Join-Path $PSScriptRoot "../../../scripts/BuildOptimization.psm1" -Resolve) + +$baseFolder = Get-BaseFolder +if (Test-ShouldSkipTestApp -AppName $parameters["appName"] -BaseFolder $baseFolder) { + return $true +} #TODO: Revert this when 616057 is fixed if (((Get-BuildMode) -eq "CZ") -and ($parameters["appName"] -eq "Quality Management-Tests")) { diff --git a/build/projects/Apps (W1)/.AL-Go/cloudDevEnv.ps1 b/build/projects/Apps (W1)/.AL-Go/cloudDevEnv.ps1 index b01c429b66..87db0a523c 100644 --- a/build/projects/Apps (W1)/.AL-Go/cloudDevEnv.ps1 +++ b/build/projects/Apps (W1)/.AL-Go/cloudDevEnv.ps1 @@ -141,12 +141,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/Apps (W1)/.AL-Go/localDevEnv.ps1 b/build/projects/Apps (W1)/.AL-Go/localDevEnv.ps1 index 4ea5eb0bf0..b10af793d9 100644 --- a/build/projects/Apps (W1)/.AL-Go/localDevEnv.ps1 +++ b/build/projects/Apps (W1)/.AL-Go/localDevEnv.ps1 @@ -154,12 +154,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/Apps (W1)/.AL-Go/settings.json b/build/projects/Apps (W1)/.AL-Go/settings.json index 65f8e158fb..9880b935cb 100644 --- a/build/projects/Apps (W1)/.AL-Go/settings.json +++ b/build/projects/Apps (W1)/.AL-Go/settings.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json", "projectName": "Apps (W1)", "appFolders": [ "../../../src/Apps/W1/*/App", diff --git a/build/projects/Business Foundation Tests/.AL-Go/RunTestsInBcContainer.ps1 b/build/projects/Business Foundation Tests/.AL-Go/RunTestsInBcContainer.ps1 index 55c85d6a07..3274b4d99d 100644 --- a/build/projects/Business Foundation Tests/.AL-Go/RunTestsInBcContainer.ps1 +++ b/build/projects/Business Foundation Tests/.AL-Go/RunTestsInBcContainer.ps1 @@ -3,6 +3,13 @@ Param( ) Import-Module (Join-Path $PSScriptRoot "../../../scripts/EnlistmentHelperFunctions.psm1" -Resolve) +Import-Module (Join-Path $PSScriptRoot "../../../scripts/BuildOptimization.psm1" -Resolve) + +$baseFolder = Get-BaseFolder +if (Test-ShouldSkipTestApp -AppName $parameters["appName"] -BaseFolder $baseFolder) { + return $true +} + $testType = Get-ALGoSetting -Key "testType" $parameters["returnTrueIfAllPassed"] = $true diff --git a/build/projects/Business Foundation Tests/.AL-Go/cloudDevEnv.ps1 b/build/projects/Business Foundation Tests/.AL-Go/cloudDevEnv.ps1 index b01c429b66..87db0a523c 100644 --- a/build/projects/Business Foundation Tests/.AL-Go/cloudDevEnv.ps1 +++ b/build/projects/Business Foundation Tests/.AL-Go/cloudDevEnv.ps1 @@ -141,12 +141,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/Business Foundation Tests/.AL-Go/localDevEnv.ps1 b/build/projects/Business Foundation Tests/.AL-Go/localDevEnv.ps1 index 4ea5eb0bf0..b10af793d9 100644 --- a/build/projects/Business Foundation Tests/.AL-Go/localDevEnv.ps1 +++ b/build/projects/Business Foundation Tests/.AL-Go/localDevEnv.ps1 @@ -154,12 +154,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/Business Foundation Tests/.AL-Go/settings.json b/build/projects/Business Foundation Tests/.AL-Go/settings.json index 84ff0927f8..d823397091 100644 --- a/build/projects/Business Foundation Tests/.AL-Go/settings.json +++ b/build/projects/Business Foundation Tests/.AL-Go/settings.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json", "projectName": "Business Foundation Tests", "testFolders": [ "../../../src/Business Foundation/Test" diff --git a/build/projects/Performance Toolkit Tests/.AL-Go/RunTestsInBcContainer.ps1 b/build/projects/Performance Toolkit Tests/.AL-Go/RunTestsInBcContainer.ps1 index 1637222d85..8d1b68c9f6 100644 --- a/build/projects/Performance Toolkit Tests/.AL-Go/RunTestsInBcContainer.ps1 +++ b/build/projects/Performance Toolkit Tests/.AL-Go/RunTestsInBcContainer.ps1 @@ -2,5 +2,13 @@ Param( [Hashtable]$parameters ) +Import-Module (Join-Path $PSScriptRoot "../../../scripts/EnlistmentHelperFunctions.psm1" -Resolve) +Import-Module (Join-Path $PSScriptRoot "../../../scripts/BuildOptimization.psm1" -Resolve) + +$baseFolder = Get-BaseFolder +if (Test-ShouldSkipTestApp -AppName $parameters["appName"] -BaseFolder $baseFolder) { + return $true +} + $script = Join-Path $PSScriptRoot "../../../scripts/RunTestsInBcContainer.ps1" -Resolve . $script -parameters $parameters \ No newline at end of file diff --git a/build/projects/Performance Toolkit Tests/.AL-Go/cloudDevEnv.ps1 b/build/projects/Performance Toolkit Tests/.AL-Go/cloudDevEnv.ps1 index b01c429b66..87db0a523c 100644 --- a/build/projects/Performance Toolkit Tests/.AL-Go/cloudDevEnv.ps1 +++ b/build/projects/Performance Toolkit Tests/.AL-Go/cloudDevEnv.ps1 @@ -141,12 +141,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/Performance Toolkit Tests/.AL-Go/localDevEnv.ps1 b/build/projects/Performance Toolkit Tests/.AL-Go/localDevEnv.ps1 index 4ea5eb0bf0..b10af793d9 100644 --- a/build/projects/Performance Toolkit Tests/.AL-Go/localDevEnv.ps1 +++ b/build/projects/Performance Toolkit Tests/.AL-Go/localDevEnv.ps1 @@ -154,12 +154,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/Performance Toolkit Tests/.AL-Go/settings.json b/build/projects/Performance Toolkit Tests/.AL-Go/settings.json index 4109cb1e35..b27c543253 100644 --- a/build/projects/Performance Toolkit Tests/.AL-Go/settings.json +++ b/build/projects/Performance Toolkit Tests/.AL-Go/settings.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json", "projectName": "Performance Toolkit Tests", "testFolders": [ "../../../src/Tools/Performance Toolkit/Test" diff --git a/build/projects/System Application Modules/.AL-Go/cloudDevEnv.ps1 b/build/projects/System Application Modules/.AL-Go/cloudDevEnv.ps1 index b01c429b66..87db0a523c 100644 --- a/build/projects/System Application Modules/.AL-Go/cloudDevEnv.ps1 +++ b/build/projects/System Application Modules/.AL-Go/cloudDevEnv.ps1 @@ -141,12 +141,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/System Application Modules/.AL-Go/localDevEnv.ps1 b/build/projects/System Application Modules/.AL-Go/localDevEnv.ps1 index 4ea5eb0bf0..b10af793d9 100644 --- a/build/projects/System Application Modules/.AL-Go/localDevEnv.ps1 +++ b/build/projects/System Application Modules/.AL-Go/localDevEnv.ps1 @@ -154,12 +154,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/System Application Modules/.AL-Go/settings.json b/build/projects/System Application Modules/.AL-Go/settings.json index 0f4bac5cac..c33d2d66e2 100644 --- a/build/projects/System Application Modules/.AL-Go/settings.json +++ b/build/projects/System Application Modules/.AL-Go/settings.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json", "projectName": "System Application Modules", "appFolders": [ "../../../src/System Application/App/*", diff --git a/build/projects/System Application Tests/.AL-Go/RunTestsInBcContainer.ps1 b/build/projects/System Application Tests/.AL-Go/RunTestsInBcContainer.ps1 index 55c85d6a07..3274b4d99d 100644 --- a/build/projects/System Application Tests/.AL-Go/RunTestsInBcContainer.ps1 +++ b/build/projects/System Application Tests/.AL-Go/RunTestsInBcContainer.ps1 @@ -3,6 +3,13 @@ Param( ) Import-Module (Join-Path $PSScriptRoot "../../../scripts/EnlistmentHelperFunctions.psm1" -Resolve) +Import-Module (Join-Path $PSScriptRoot "../../../scripts/BuildOptimization.psm1" -Resolve) + +$baseFolder = Get-BaseFolder +if (Test-ShouldSkipTestApp -AppName $parameters["appName"] -BaseFolder $baseFolder) { + return $true +} + $testType = Get-ALGoSetting -Key "testType" $parameters["returnTrueIfAllPassed"] = $true diff --git a/build/projects/System Application Tests/.AL-Go/cloudDevEnv.ps1 b/build/projects/System Application Tests/.AL-Go/cloudDevEnv.ps1 index b01c429b66..87db0a523c 100644 --- a/build/projects/System Application Tests/.AL-Go/cloudDevEnv.ps1 +++ b/build/projects/System Application Tests/.AL-Go/cloudDevEnv.ps1 @@ -141,12 +141,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/System Application Tests/.AL-Go/localDevEnv.ps1 b/build/projects/System Application Tests/.AL-Go/localDevEnv.ps1 index 4ea5eb0bf0..b10af793d9 100644 --- a/build/projects/System Application Tests/.AL-Go/localDevEnv.ps1 +++ b/build/projects/System Application Tests/.AL-Go/localDevEnv.ps1 @@ -154,12 +154,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/System Application Tests/.AL-Go/settings.json b/build/projects/System Application Tests/.AL-Go/settings.json index 60eda45e1d..b0620fd9ae 100644 --- a/build/projects/System Application Tests/.AL-Go/settings.json +++ b/build/projects/System Application Tests/.AL-Go/settings.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json", "projectName": "System Application Tests", "testFolders": [ "../../../src/System Application/Test", diff --git a/build/projects/System Application/.AL-Go/cloudDevEnv.ps1 b/build/projects/System Application/.AL-Go/cloudDevEnv.ps1 index b01c429b66..87db0a523c 100644 --- a/build/projects/System Application/.AL-Go/cloudDevEnv.ps1 +++ b/build/projects/System Application/.AL-Go/cloudDevEnv.ps1 @@ -141,12 +141,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/System Application/.AL-Go/localDevEnv.ps1 b/build/projects/System Application/.AL-Go/localDevEnv.ps1 index 4ea5eb0bf0..b10af793d9 100644 --- a/build/projects/System Application/.AL-Go/localDevEnv.ps1 +++ b/build/projects/System Application/.AL-Go/localDevEnv.ps1 @@ -154,12 +154,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/System Application/.AL-Go/settings.json b/build/projects/System Application/.AL-Go/settings.json index 2e7c71d99e..289204f4b4 100644 --- a/build/projects/System Application/.AL-Go/settings.json +++ b/build/projects/System Application/.AL-Go/settings.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json", "projectName": "System Application, Business Foundation and Tools", "appFolders": [ "../../../src/System Application/App", @@ -7,7 +7,8 @@ "../../../src/Tools/AI Test Toolkit", "../../../src/Tools/Performance Toolkit/App", "../../../src/Tools/Test Framework/Test Libraries/*", - "../../../src/Tools/Test Framework/Test Runner" + "../../../src/Tools/Test Framework/Test Runner", + "../../../src/Tools/Red Team Scan" ], "testFolders": [ "../../../src/System Application/Test Library", diff --git a/build/projects/Test Stability Tools/.AL-Go/cloudDevEnv.ps1 b/build/projects/Test Stability Tools/.AL-Go/cloudDevEnv.ps1 index b01c429b66..87db0a523c 100644 --- a/build/projects/Test Stability Tools/.AL-Go/cloudDevEnv.ps1 +++ b/build/projects/Test Stability Tools/.AL-Go/cloudDevEnv.ps1 @@ -141,12 +141,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/Test Stability Tools/.AL-Go/localDevEnv.ps1 b/build/projects/Test Stability Tools/.AL-Go/localDevEnv.ps1 index 4ea5eb0bf0..b10af793d9 100644 --- a/build/projects/Test Stability Tools/.AL-Go/localDevEnv.ps1 +++ b/build/projects/Test Stability Tools/.AL-Go/localDevEnv.ps1 @@ -154,12 +154,12 @@ Write-Host -ForegroundColor Yellow @' $tmpFolder = Join-Path ([System.IO.Path]::GetTempPath()) "$([Guid]::NewGuid().ToString())" New-Item -Path $tmpFolder -ItemType Directory -Force | Out-Null -$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt -$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/ReadSettings.psm1' -folder $tmpFolder -$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/DebugLogHelper.psm1' -folder $tmpFolder -$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/AL-Go-Helper.ps1' -folder $tmpFolder -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null -DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/Environment.Packages.proj' -folder $tmpFolder | Out-Null +$GitHubHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Github-Helper.psm1' -folder $tmpFolder -notifyAuthenticatedAttempt +$ReadSettingsModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/ReadSettings.psm1' -folder $tmpFolder +$debugLoggingModule = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/DebugLogHelper.psm1' -folder $tmpFolder +$ALGoHelperPath = DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/AL-Go-Helper.ps1' -folder $tmpFolder +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json' -folder $tmpFolder | Out-Null +DownloadHelperFile -url 'https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/Environment.Packages.proj' -folder $tmpFolder | Out-Null Import-Module $GitHubHelperPath Import-Module $ReadSettingsModule diff --git a/build/projects/Test Stability Tools/.AL-Go/settings.json b/build/projects/Test Stability Tools/.AL-Go/settings.json index e7987c0304..c523649e87 100644 --- a/build/projects/Test Stability Tools/.AL-Go/settings.json +++ b/build/projects/Test Stability Tools/.AL-Go/settings.json @@ -1,5 +1,5 @@ { - "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go/91c2f1bab7959cffc66fd9513a1d83ec9f641e30/Actions/.Modules/settings.schema.json", + "$schema": "https://raw.githubusercontent.com/microsoft/AL-Go-Actions/v9.0/.Modules/settings.schema.json", "projectName": "Test Stability Tools", "appFolders": [ "../../../src/Tools/Test Framework/Test Stability Tools/Prevent Metadata Updates" diff --git a/build/scripts/BuildOptimization.psm1 b/build/scripts/BuildOptimization.psm1 new file mode 100644 index 0000000000..10a374ecca --- /dev/null +++ b/build/scripts/BuildOptimization.psm1 @@ -0,0 +1,419 @@ +<# +.SYNOPSIS + Test-skip logic for CI/CD build optimization. +.DESCRIPTION + Determines whether a test app should be skipped based on which files changed + and the app dependency graph. Called from RunTestsInBcContainer.ps1. +#> + +$ErrorActionPreference = "Stop" + + +<# +.SYNOPSIS + Builds a dependency graph from all app.json files under the given base folder. +.PARAMETER BaseFolder + Root of the repository. +.OUTPUTS + Hashtable keyed by lowercase app ID. Each value is a PSCustomObject with + Id, Name, AppFolder, Dependencies (string[]), Dependents (List[string]). +#> +function Get-AppDependencyGraph { + [CmdletBinding()] + [OutputType([hashtable])] + param( + [Parameter(Mandatory)] + [string] $BaseFolder + ) + + $graph = @{} + $appJsonFiles = Get-ChildItem -Path $BaseFolder -Recurse -Filter 'app.json' -File | + Where-Object { $_.FullName -notmatch '[\\/]\.buildartifacts[\\/]' } + + foreach ($file in $appJsonFiles) { + $json = Get-Content -Path $file.FullName -Raw | ConvertFrom-Json + if (-not $json.id) { continue } + + $appId = $json.id.ToLowerInvariant() + $depIds = @() + if ($json.dependencies) { + $depIds = @($json.dependencies | ForEach-Object { $_.id.ToLowerInvariant() }) + } + + $graph[$appId] = [PSCustomObject]@{ + Id = $appId + Name = $json.name + AppFolder = $file.DirectoryName + Dependencies = $depIds + Dependents = [System.Collections.Generic.List[string]]::new() + } + } + + foreach ($node in $graph.Values) { + foreach ($depId in $node.Dependencies) { + if ($graph.ContainsKey($depId)) { + $graph[$depId].Dependents.Add($node.Id) + } + } + } + + return $graph +} + +<# +.SYNOPSIS + Determines which app (if any) a file belongs to by walking up to the nearest app.json. +.PARAMETER FilePath + Path to the changed file (absolute or relative to BaseFolder). +.PARAMETER BaseFolder + Root of the repository. +.OUTPUTS + The app ID (lowercase GUID) or $null if the file is not inside any app folder. +#> +function Get-AppForFile { + [CmdletBinding()] + [OutputType([string])] + param( + [Parameter(Mandatory)] + [string] $FilePath, + [Parameter(Mandatory)] + [string] $BaseFolder + ) + + if (-not [System.IO.Path]::IsPathRooted($FilePath)) { + $FilePath = Join-Path $BaseFolder $FilePath + } + $FilePath = [System.IO.Path]::GetFullPath($FilePath) + + $dir = [System.IO.Path]::GetDirectoryName($FilePath) + $baseFolderNorm = [System.IO.Path]::GetFullPath($BaseFolder).TrimEnd([System.IO.Path]::DirectorySeparatorChar, [System.IO.Path]::AltDirectorySeparatorChar) + + while ($dir -and $dir.Length -ge $baseFolderNorm.Length) { + $candidate = Join-Path $dir 'app.json' + if (Test-Path $candidate) { + $json = Get-Content -Path $candidate -Raw | ConvertFrom-Json + if ($json.id) { return $json.id.ToLowerInvariant() } + } + $parent = [System.IO.Path]::GetDirectoryName($dir) + if ($parent -eq $dir) { break } + $dir = $parent + } + + return $null +} + +<# +.SYNOPSIS + Given changed files, computes the set of affected app IDs via downstream BFS. +.DESCRIPTION + Maps each changed file to its app, then walks dependents (BFS) to find all + apps that transitively depend on a changed app. Files under src/ that can't + be mapped to an app trigger a full build (returns all app IDs). +.PARAMETER ChangedFiles + Array of changed file paths (relative to BaseFolder or absolute). +.PARAMETER BaseFolder + Root of the repository. +.PARAMETER Graph + Pre-built dependency graph. If not provided, one is built from BaseFolder. +.OUTPUTS + String array of affected app IDs (lowercase GUIDs). +#> +function Get-AffectedApps { + [CmdletBinding()] + [OutputType([string[]])] + param( + [Parameter(Mandatory)] + [string[]] $ChangedFiles, + [Parameter(Mandatory)] + [string] $BaseFolder, + [Parameter()] + [hashtable] $Graph + ) + + if (-not $Graph) { + $Graph = Get-AppDependencyGraph -BaseFolder $BaseFolder + } + + # Map changed files to apps + $directlyChanged = [System.Collections.Generic.HashSet[string]]::new() + foreach ($file in $ChangedFiles) { + $appId = Get-AppForFile -FilePath $file -BaseFolder $BaseFolder + if ($appId) { + [void]$directlyChanged.Add($appId) + } elseif ($file.Replace('\', '/') -match '(^|/)src/') { + # Unmapped file under src/ — safety fallback to full build + return [string[]]@($Graph.Keys) + } + } + + if ($directlyChanged.Count -eq 0) { return [string[]]@() } + + # BFS downstream: changed apps + everything that depends on them + $affected = [System.Collections.Generic.HashSet[string]]::new() + $queue = [System.Collections.Generic.Queue[string]]::new() + foreach ($appId in $directlyChanged) { + if ($Graph.ContainsKey($appId)) { $queue.Enqueue($appId) } + } + + while ($queue.Count -gt 0) { + $current = $queue.Dequeue() + if ($affected.Contains($current)) { continue } + [void]$affected.Add($current) + if ($Graph.ContainsKey($current)) { + foreach ($dep in $Graph[$current].Dependents) { + if (-not $affected.Contains($dep)) { $queue.Enqueue($dep) } + } + } + } + + return [string[]]@($affected) +} + +<# +.SYNOPSIS + Detects changed files from the GitHub Actions CI environment. +.DESCRIPTION + Reads the GitHub event payload ($GITHUB_EVENT_PATH) to extract base/head commit + SHAs, then uses git diff with those SHAs. This approach works reliably with + shallow clones (unlike three-dot diffs that need the merge base). + Supports pull_request, merge_group, and push events. + Returns $null when changed files cannot be determined (local, workflow_dispatch, git failure). +.OUTPUTS + String array of changed file paths relative to repo root, or $null. +#> +function Get-ChangedFilesForCI { + [CmdletBinding()] + [OutputType([string[]])] + param() + + if (-not $env:GITHUB_ACTIONS) { + Write-Host "BUILD OPTIMIZATION: Change detection skipped - not running in GitHub Actions" + return $null + } + + if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') { + Write-Host "BUILD OPTIMIZATION: Change detection skipped - workflow_dispatch event" + return $null + } + + # Read GitHub event payload for base/head commit SHAs (works with shallow clones) + if (-not $env:GITHUB_EVENT_PATH -or -not (Test-Path $env:GITHUB_EVENT_PATH)) { + Write-Host "BUILD OPTIMIZATION: GitHub event payload not found at '$($env:GITHUB_EVENT_PATH)'" + return $null + } + + $eventPayload = Get-Content $env:GITHUB_EVENT_PATH -Raw | ConvertFrom-Json + + $baseSha = $null + $headSha = $null + + if ($env:GITHUB_EVENT_NAME -match 'pull_request') { + $baseSha = $eventPayload.pull_request.base.sha + $headSha = $eventPayload.pull_request.head.sha + } + elseif ($env:GITHUB_EVENT_NAME -eq 'merge_group') { + $baseSha = $eventPayload.merge_group.base_sha + $headSha = $eventPayload.merge_group.head_sha + } + elseif ($env:GITHUB_EVENT_NAME -eq 'push') { + $baseSha = $eventPayload.before + $headSha = $eventPayload.after + } + + if (-not $baseSha -or -not $headSha) { + Write-Host "BUILD OPTIMIZATION: Could not extract commit SHAs from event payload (event=$($env:GITHUB_EVENT_NAME))" + return $null + } + + Write-Host "BUILD OPTIMIZATION: Comparing $($baseSha.Substring(0, 8))...$($headSha.Substring(0, 8))" + + $prevErrorAction = $ErrorActionPreference + $ErrorActionPreference = 'Continue' + try { + # Best-effort fetch of base commit (may not be in shallow clone) + git fetch origin $baseSha --depth=1 2>$null + + $files = @(git diff --name-only $baseSha $headSha 2>$null) + if ($LASTEXITCODE -ne 0) { + Write-Host "BUILD OPTIMIZATION: git diff failed (exitCode=$LASTEXITCODE)" + return $null + } + + if ($files.Count -eq 0) { + Write-Host "BUILD OPTIMIZATION: No changed files detected" + return $null + } + + return [string[]]$files + } + finally { + $ErrorActionPreference = $prevErrorAction + } +} + +<# +.SYNOPSIS + Checks whether any changed files match the fullBuildPatterns from AL-Go settings. +.DESCRIPTION + Reads the fullBuildPatterns array from .github/AL-Go-Settings.json and tests + each changed file path against each pattern using -like. When AL-Go detects + matching files it forces a full compile, so the test side must also force a + full test run (i.e., skip nothing). +.PARAMETER ChangedFiles + Array of changed file paths (relative to repo root, forward-slash separated). +.PARAMETER BaseFolder + Root of the repository (used to locate .github/AL-Go-Settings.json). +.OUTPUTS + $true if any changed file matches a fullBuildPattern, $false otherwise. +#> +function Test-FullBuildPatternsMatch { + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory)] + [string[]] $ChangedFiles, + [Parameter(Mandatory)] + [string] $BaseFolder + ) + + $settingsPath = Join-Path $BaseFolder '.github/AL-Go-Settings.json' + if (-not (Test-Path $settingsPath)) { return $false } + + $settings = Get-Content -Path $settingsPath -Raw | ConvertFrom-Json + $patterns = $settings.fullBuildPatterns + if (-not $patterns -or $patterns.Count -eq 0) { return $false } + + foreach ($file in $ChangedFiles) { + $normalized = $file.Replace('\', '/') + foreach ($pattern in $patterns) { + if ($normalized -like $pattern) { + Write-Host "BUILD OPTIMIZATION: File '$normalized' matches fullBuildPattern '$pattern' - forcing full test run" + return $true + } + } + } + + return $false +} + +<# +.SYNOPSIS + Computes the set of app names affected by changed files. +.DESCRIPTION + Pure computation — no caching or side effects. Returns a PSCustomObject with + two properties: RunAll (bool) and AppNames (string[]). The caller is responsible + for persisting/reading the result to avoid recomputing across process invocations. +.PARAMETER BaseFolder + Root of the repository. +.OUTPUTS + PSCustomObject with RunAll (bool) and AppNames (string[]). +#> +function Get-AffectedAppNames { + [CmdletBinding()] + [OutputType([PSCustomObject])] + param( + [Parameter(Mandatory)] + [string] $BaseFolder + ) + + $runAll = @{ RunAll = $true; AppNames = @() } + + if ($env:BUILD_OPTIMIZATION_DISABLED -eq 'true') { + Write-Host "BUILD OPTIMIZATION: Disabled via BUILD_OPTIMIZATION_DISABLED=true" + return [PSCustomObject]$runAll + } + if (-not $env:GITHUB_ACTIONS) { + return [PSCustomObject]$runAll + } + if ($env:GITHUB_EVENT_NAME -eq 'workflow_dispatch') { + return [PSCustomObject]$runAll + } + + $changedFiles = Get-ChangedFilesForCI + if (-not $changedFiles) { + Write-Host "BUILD OPTIMIZATION: Running all tests - could not determine changed files" + return [PSCustomObject]$runAll + } + + Write-Host "BUILD OPTIMIZATION: Changed files ($($changedFiles.Count)):" + foreach ($f in $changedFiles) { Write-Host " - $f" } + + # If any changed file matches fullBuildPatterns, AL-Go compiles everything. + # We must run all tests to match — skip nothing. + if (Test-FullBuildPatternsMatch -ChangedFiles $changedFiles -BaseFolder $BaseFolder) { + Write-Host "BUILD OPTIMIZATION: fullBuildPatterns matched, full test run required" + return [PSCustomObject]$runAll + } + + $graph = Get-AppDependencyGraph -BaseFolder $BaseFolder + $affectedIds = Get-AffectedApps -ChangedFiles $changedFiles -BaseFolder $BaseFolder -Graph $graph + + # Full build triggered (unmapped src file or all apps affected) + if ($affectedIds.Count -ge $graph.Count) { + Write-Host "BUILD OPTIMIZATION: Full build triggered ($($affectedIds.Count) apps affected)" + return [PSCustomObject]$runAll + } + + $affectedNames = @() + foreach ($id in $affectedIds) { + if ($graph.ContainsKey($id)) { $affectedNames += $graph[$id].Name } + } + + $sortedNames = $affectedNames | Sort-Object + Write-Host "BUILD OPTIMIZATION: Affected apps ($($affectedNames.Count)):" + foreach ($name in $sortedNames) { Write-Host " - $name" } + + return [PSCustomObject]@{ RunAll = $false; AppNames = $affectedNames } +} + +<# +.SYNOPSIS + Determines whether tests for a given app should be skipped. +.DESCRIPTION + Reads the cached affected-apps JSON file written by a prior call in the same + CI job. If the file doesn't exist, computes the result and writes the cache. + Returns $true to skip, $false to run. +.PARAMETER AppName + The display name of the test app (from $parameters["appName"]). +.PARAMETER BaseFolder + Root of the repository. +.PARAMETER CacheFile + Path to the JSON cache file. Defaults to $TEMP/BuildOptimization_AffectedApps.json. +.OUTPUTS + $true if the test app should be skipped, $false if it should run. +#> +function Test-ShouldSkipTestApp { + [CmdletBinding()] + [OutputType([bool])] + param( + [Parameter(Mandatory)] + [string] $AppName, + [Parameter(Mandatory)] + [string] $BaseFolder, + [string] $CacheFile = (Join-Path ([System.IO.Path]::GetTempPath()) 'BuildOptimization_AffectedApps.json') + ) + + # Read or compute affected apps + if (Test-Path $CacheFile) { + Write-Host "BUILD OPTIMIZATION: Reading cached result from $CacheFile" + $result = Get-Content $CacheFile -Raw | ConvertFrom-Json + } else { + $result = Get-AffectedAppNames -BaseFolder $BaseFolder + $result | ConvertTo-Json -Compress | Set-Content -Path $CacheFile -Force + Write-Host "BUILD OPTIMIZATION: Cache written to $CacheFile" + } + + if ($result.RunAll) { + Write-Host "BUILD OPTIMIZATION: RUNNING tests for '$AppName'" + return $false + } + + if ($result.AppNames -notcontains $AppName) { + Write-Host "BUILD OPTIMIZATION: SKIPPING tests for '$AppName' - not in affected set" + return $true + } + Write-Host "BUILD OPTIMIZATION: RUNNING tests for '$AppName'" + return $false +} + +Export-ModuleMember -Function Get-AppDependencyGraph, Get-AppForFile, Get-AffectedApps, Get-ChangedFilesForCI, Test-FullBuildPatternsMatch, Get-AffectedAppNames, Test-ShouldSkipTestApp diff --git a/build/scripts/RunTestsInBcContainer.ps1 b/build/scripts/RunTestsInBcContainer.ps1 index 43bc946227..82d8003b77 100644 --- a/build/scripts/RunTestsInBcContainer.ps1 +++ b/build/scripts/RunTestsInBcContainer.ps1 @@ -76,4 +76,11 @@ if ($DisableTestIsolation) $parameters["testRunnerCodeunitId"] = "130450" # Test Runner with Codeunit test isolation } +if ($parameters["appName"] -eq "System Application Test" -or $parameters["appName"] -eq "Agent Test") +{ + Write-Host "Enabling AgentsFeature for '$($parameters["appName"])' tests" + Set-BcContainerServerConfiguration -containerName $parameters.ContainerName -keyName "AgentsFeatureEnabled" -keyValue "true" + Restart-BcContainer -containerName $parameters.ContainerName +} + return Invoke-TestsWithReruns -parameters $parameters diff --git a/build/scripts/tests/BuildOptimization.Test.ps1 b/build/scripts/tests/BuildOptimization.Test.ps1 new file mode 100644 index 0000000000..a1f8bcd2c5 --- /dev/null +++ b/build/scripts/tests/BuildOptimization.Test.ps1 @@ -0,0 +1,434 @@ +Describe "BuildOptimization" { + BeforeAll { + Import-Module "$PSScriptRoot\..\EnlistmentHelperFunctions.psm1" -Force + Import-Module "$PSScriptRoot\..\BuildOptimization.psm1" -Force + $baseFolder = Get-BaseFolder + $graph = Get-AppDependencyGraph -BaseFolder $baseFolder + } + + Context "Get-AppDependencyGraph" { + It "builds a graph with the expected number of nodes" { + $graph.Count | Should -BeGreaterOrEqual 300 + } + + It "includes System Application node" { + $sysAppId = '63ca2fa4-4f03-4f2b-a480-172fef340d3f' + $graph.ContainsKey($sysAppId) | Should -BeTrue + $graph[$sysAppId].Name | Should -Be 'System Application' + } + + It "includes E-Document Core with correct reverse edges" { + $edocId = 'e1d97edc-c239-46b4-8d84-6368bdf67c8b' + $graph[$edocId].Dependents.Count | Should -BeGreaterOrEqual 5 + $dependentNames = $graph[$edocId].Dependents | ForEach-Object { $graph[$_].Name } + $dependentNames | Should -Contain 'E-Document Core Tests' + $dependentNames | Should -Contain 'E-Document Connector - Avalara' + } + + It "builds correct forward edges for Avalara connector" { + $avalaraNode = $graph.Values | Where-Object { $_.Name -eq 'E-Document Connector - Avalara' } + $avalaraNode | Should -Not -BeNullOrEmpty + $avalaraNode.Dependencies | Should -Contain 'e1d97edc-c239-46b4-8d84-6368bdf67c8b' + } + } + + Context "Get-AppForFile" { + It "maps E-Document Core file" { + $result = Get-AppForFile -FilePath 'src/Apps/W1/EDocument/App/src/SomeFile.al' -BaseFolder $baseFolder + $result | Should -Be 'e1d97edc-c239-46b4-8d84-6368bdf67c8b' + } + + It "maps Email module file" { + $result = Get-AppForFile -FilePath 'src/System Application/App/Email/src/SomeFile.al' -BaseFolder $baseFolder + $result | Should -Be '9c4a2cf2-be3a-4aa3-833b-99a5ffd11f25' + } + + It "returns null for non-app file" { + $result = Get-AppForFile -FilePath 'build/scripts/BuildOptimization.psm1' -BaseFolder $baseFolder + $result | Should -BeNullOrEmpty + } + + It "handles absolute paths" { + $absPath = Join-Path $baseFolder 'src/Apps/W1/EDocument/App/src/SomeFile.al' + $result = Get-AppForFile -FilePath $absPath -BaseFolder $baseFolder + $result | Should -Be 'e1d97edc-c239-46b4-8d84-6368bdf67c8b' + } + } + + Context "Get-AffectedApps" { + It "returns 9 affected apps for E-Document Core change" { + $affected = Get-AffectedApps -ChangedFiles @('src/Apps/W1/EDocument/App/src/SomeFile.al') -BaseFolder $baseFolder -Graph $graph + $affected.Count | Should -Be 9 + $affected | Should -Contain 'e1d97edc-c239-46b4-8d84-6368bdf67c8b' + } + + It "includes all connectors and tests for E-Document Core change" { + $affected = Get-AffectedApps -ChangedFiles @('src/Apps/W1/EDocument/App/src/SomeFile.al') -BaseFolder $baseFolder -Graph $graph + $affectedNames = $affected | ForEach-Object { $graph[$_].Name } + $affectedNames | Should -Contain 'E-Document Core Tests' + $affectedNames | Should -Contain 'E-Document Core Demo Data' + $affectedNames | Should -Contain 'E-Document Connector - Avalara' + $affectedNames | Should -Contain 'E-Document Connector - Avalara Tests' + $affectedNames | Should -Contain 'E-Document Connector - Continia' + $affectedNames | Should -Contain 'E-Document Connector - Continia Tests' + } + + It "returns all apps when an unmapped src/ file is present" { + $affected = Get-AffectedApps -ChangedFiles @('src/rulesets/ruleset.json') -BaseFolder $baseFolder -Graph $graph + $affected.Count | Should -Be $graph.Count + } + + It "ignores non-src unmapped files" { + $affected = Get-AffectedApps -ChangedFiles @( + 'build/scripts/SomeNewScript.ps1', + 'src/Apps/W1/EDocument/App/src/SomeFile.al' + ) -BaseFolder $baseFolder -Graph $graph + $affected.Count | Should -BeLessThan $graph.Count + $affectedNames = $affected | ForEach-Object { $graph[$_].Name } + $affectedNames | Should -Contain 'E-Document Core' + } + + It "handles multiple changed files" { + $affected = Get-AffectedApps -ChangedFiles @( + 'src/Apps/W1/EDocument/App/src/SomeFile.al', + 'src/Apps/W1/EDocument/Test/src/SomeTest.al' + ) -BaseFolder $baseFolder -Graph $graph + $affectedNames = $affected | ForEach-Object { $graph[$_].Name } + $affectedNames | Should -Contain 'E-Document Core' + $affectedNames | Should -Contain 'E-Document Core Tests' + } + } + + Context "Get-ChangedFilesForCI" { + It "returns null when not in CI" { + $saved = $env:GITHUB_ACTIONS + try { + $env:GITHUB_ACTIONS = $null + Get-ChangedFilesForCI | Should -BeNullOrEmpty + } finally { + $env:GITHUB_ACTIONS = $saved + } + } + + It "returns null for workflow_dispatch" { + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'workflow_dispatch' + Get-ChangedFilesForCI | Should -BeNullOrEmpty + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + } + } + + It "returns null when event payload file is missing" { + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + $savedEventPath = $env:GITHUB_EVENT_PATH + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'pull_request' + $env:GITHUB_EVENT_PATH = 'C:\nonexistent\event.json' + Get-ChangedFilesForCI | Should -BeNullOrEmpty + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + $env:GITHUB_EVENT_PATH = $savedEventPath + } + } + + It "returns null for unsupported event type" { + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + $savedEventPath = $env:GITHUB_EVENT_PATH + $tempFile = $null + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'schedule' + $tempFile = [System.IO.Path]::GetTempFileName() + Set-Content $tempFile -Value '{}' + $env:GITHUB_EVENT_PATH = $tempFile + Get-ChangedFilesForCI | Should -BeNullOrEmpty + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + $env:GITHUB_EVENT_PATH = $savedEventPath + if ($tempFile) { Remove-Item $tempFile -ErrorAction SilentlyContinue } + } + } + + It "returns changed files using real commits from pull_request event payload" { + $baseSha = (git rev-parse --verify HEAD~1 2>$null) + if (-not $baseSha -or $LASTEXITCODE -ne 0) { + Set-ItResult -Skipped -Because 'shallow clone has no parent commit' + return + } + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + $savedEventPath = $env:GITHUB_EVENT_PATH + $tempFile = $null + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'pull_request' + + $headSha = (git rev-parse HEAD) + + $tempFile = [System.IO.Path]::GetTempFileName() + @{ + pull_request = @{ + base = @{ sha = $baseSha } + head = @{ sha = $headSha } + } + } | ConvertTo-Json -Depth 5 | Set-Content $tempFile + $env:GITHUB_EVENT_PATH = $tempFile + + $result = Get-ChangedFilesForCI + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -BeGreaterOrEqual 1 + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + $env:GITHUB_EVENT_PATH = $savedEventPath + if ($tempFile) { Remove-Item $tempFile -ErrorAction SilentlyContinue } + } + } + + It "returns changed files for push event using before/after SHAs" { + $baseSha = (git rev-parse --verify HEAD~1 2>$null) + if (-not $baseSha -or $LASTEXITCODE -ne 0) { + Set-ItResult -Skipped -Because 'shallow clone has no parent commit' + return + } + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + $savedEventPath = $env:GITHUB_EVENT_PATH + $tempFile = $null + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'push' + + $headSha = (git rev-parse HEAD) + + $tempFile = [System.IO.Path]::GetTempFileName() + @{ + before = $baseSha + after = $headSha + } | ConvertTo-Json -Depth 5 | Set-Content $tempFile + $env:GITHUB_EVENT_PATH = $tempFile + + $result = Get-ChangedFilesForCI + $result | Should -Not -BeNullOrEmpty + $result.Count | Should -BeGreaterOrEqual 1 + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + $env:GITHUB_EVENT_PATH = $savedEventPath + if ($tempFile) { Remove-Item $tempFile -ErrorAction SilentlyContinue } + } + } + } + + Context "Test-FullBuildPatternsMatch" { + It "returns true when a changed file matches build/* pattern" { + $result = Test-FullBuildPatternsMatch -ChangedFiles @('build/scripts/RunTestsInBcContainer.ps1') -BaseFolder $baseFolder + $result | Should -BeTrue + } + + It "returns true when a changed file matches src/rulesets/* pattern" { + $result = Test-FullBuildPatternsMatch -ChangedFiles @('src/rulesets/ruleset.json') -BaseFolder $baseFolder + $result | Should -BeTrue + } + + It "returns true when a changed file matches an exact workflow pattern" { + $result = Test-FullBuildPatternsMatch -ChangedFiles @('.github/workflows/PullRequestHandler.yaml') -BaseFolder $baseFolder + $result | Should -BeTrue + } + + It "returns false when no changed files match any pattern" { + $result = Test-FullBuildPatternsMatch -ChangedFiles @('src/Apps/W1/EDocument/App/src/SomeFile.al') -BaseFolder $baseFolder + $result | Should -BeFalse + } + + It "returns false for non-matching top-level files" { + $result = Test-FullBuildPatternsMatch -ChangedFiles @('README.md', '.gitignore') -BaseFolder $baseFolder + $result | Should -BeFalse + } + + It "returns true when only one of multiple files matches" { + $result = Test-FullBuildPatternsMatch -ChangedFiles @( + 'src/Apps/W1/EDocument/App/src/SomeFile.al', + 'build/scripts/SomeNewScript.ps1' + ) -BaseFolder $baseFolder + $result | Should -BeTrue + } + + It "handles backslash paths by normalizing to forward slashes" { + $result = Test-FullBuildPatternsMatch -ChangedFiles @('build\scripts\RunTestsInBcContainer.ps1') -BaseFolder $baseFolder + $result | Should -BeTrue + } + + It "returns false when settings file is missing" { + $result = Test-FullBuildPatternsMatch -ChangedFiles @('build/scripts/foo.ps1') -BaseFolder 'C:\nonexistent\path' + $result | Should -BeFalse + } + } + + Context "BaseFolder must be repo root, not build/" { + It "graph is empty when BaseFolder points to build/ instead of repo root" { + # The old RunTestsInBcContainer.ps1 code computed BaseFolder as: + # Join-Path $PSScriptRoot "../../.." from .AL-Go/ → resolves to build/ + # This is wrong because app.json files live under src/ at the repo root. + $buildFolder = (Resolve-Path "$PSScriptRoot\..\..").Path # build/ + $wrongGraph = Get-AppDependencyGraph -BaseFolder $buildFolder + $wrongGraph.Count | Should -Be 0 -Because 'build/ contains no app.json files; BaseFolder must be the repo root' + } + + It "affected apps returns empty array with wrong BaseFolder, causing silent full-build" { + $buildFolder = (Resolve-Path "$PSScriptRoot\..\..").Path # build/ + $wrongGraph = Get-AppDependencyGraph -BaseFolder $buildFolder + + $changedFiles = @( + 'src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al' + ) + $affected = Get-AffectedApps -ChangedFiles $changedFiles -BaseFolder $buildFolder -Graph $wrongGraph + + # With wrong BaseFolder: graph is empty, unmapped src/ file returns @($Graph.Keys) = @() + # PowerShell unwraps empty arrays to $null through the pipeline, so wrap in @() + # Then 0 >= 0 is true, so Get-AffectedAppNames treats this as "full build" + # even though zero apps were actually identified + @($affected).Count | Should -Be 0 -Because 'empty graph means no apps can be found' + @($affected).Count -ge @($wrongGraph.Keys).Count | Should -BeTrue -Because 'this is the condition that triggers the false full-build (0 >= 0)' + } + + It "correct BaseFolder (repo root) finds apps for the same changed files" { + $changedFiles = @( + 'src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al' + ) + $affected = Get-AffectedApps -ChangedFiles $changedFiles -BaseFolder $baseFolder -Graph $graph + $affected.Count | Should -BeGreaterThan 0 -Because 'repo root BaseFolder correctly maps EDocument files to apps' + $affectedNames = $affected | ForEach-Object { $graph[$_].Name } + $affectedNames | Should -Contain 'E-Document Core' + } + + It "RunTestsInBcContainer scripts use Get-BaseFolder, not relative path resolution" { + $scripts = Get-ChildItem -Path (Resolve-Path "$PSScriptRoot\..\..\projects").Path -Recurse -Filter 'RunTestsInBcContainer.ps1' + $scripts.Count | Should -BeGreaterOrEqual 4 + + foreach ($script in $scripts) { + $content = Get-Content $script.FullName -Raw + $content | Should -Match 'Get-BaseFolder' -Because "$($script.FullName) must use Get-BaseFolder for repo root" + $content | Should -Not -Match '\$baseFolder\s*=.*Join-Path.*\$PSScriptRoot' -Because "$($script.FullName) must not compute baseFolder via relative path" + } + } + } + + Context "Test-ShouldSkipTestApp" { + BeforeAll { + $cacheFile = Join-Path ([System.IO.Path]::GetTempPath()) "BuildOptimization_Test_$([System.Guid]::NewGuid()).json" + } + BeforeEach { + Remove-Item $cacheFile -ErrorAction SilentlyContinue + } + AfterAll { + Remove-Item $cacheFile -ErrorAction SilentlyContinue + } + + It "returns false when not in CI" { + $saved = $env:GITHUB_ACTIONS + try { + $env:GITHUB_ACTIONS = $null + Test-ShouldSkipTestApp -AppName 'Shopify' -BaseFolder $baseFolder -CacheFile $cacheFile | Should -BeFalse + } finally { + $env:GITHUB_ACTIONS = $saved + } + } + + It "returns false for workflow_dispatch" { + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'workflow_dispatch' + Test-ShouldSkipTestApp -AppName 'Shopify' -BaseFolder $baseFolder -CacheFile $cacheFile | Should -BeFalse + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + } + } + + It "returns false when BUILD_OPTIMIZATION_DISABLED is true" { + $savedDisabled = $env:BUILD_OPTIMIZATION_DISABLED + $savedActions = $env:GITHUB_ACTIONS + try { + $env:BUILD_OPTIMIZATION_DISABLED = 'true' + $env:GITHUB_ACTIONS = 'true' + Test-ShouldSkipTestApp -AppName 'Shopify' -BaseFolder $baseFolder -CacheFile $cacheFile | Should -BeFalse + } finally { + $env:BUILD_OPTIMIZATION_DISABLED = $savedDisabled + $env:GITHUB_ACTIONS = $savedActions + } + } + + It "returns false when changed files match fullBuildPatterns" { + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + $savedDisabled = $env:BUILD_OPTIMIZATION_DISABLED + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'pull_request' + $env:BUILD_OPTIMIZATION_DISABLED = $null + Mock -ModuleName BuildOptimization Get-ChangedFilesForCI { return @('build/scripts/SomeScript.ps1') } + Test-ShouldSkipTestApp -AppName 'Shopify' -BaseFolder $baseFolder -CacheFile $cacheFile | Should -BeFalse + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + $env:BUILD_OPTIMIZATION_DISABLED = $savedDisabled + } + } + + It "returns true (skip) when changed files affect only E-Document and AppName is Shopify" { + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + $savedDisabled = $env:BUILD_OPTIMIZATION_DISABLED + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'pull_request' + $env:BUILD_OPTIMIZATION_DISABLED = $null + Mock -ModuleName BuildOptimization Get-ChangedFilesForCI { + return @('src/Apps/W1/EDocument/App/src/SomeFile.al') + } + Test-ShouldSkipTestApp -AppName 'Shopify' -BaseFolder $baseFolder -CacheFile $cacheFile | Should -BeTrue + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + $env:BUILD_OPTIMIZATION_DISABLED = $savedDisabled + } + } + + It "reads from cache file on second call without recomputing" { + $savedActions = $env:GITHUB_ACTIONS + $savedEvent = $env:GITHUB_EVENT_NAME + $savedDisabled = $env:BUILD_OPTIMIZATION_DISABLED + try { + $env:GITHUB_ACTIONS = 'true' + $env:GITHUB_EVENT_NAME = 'pull_request' + $env:BUILD_OPTIMIZATION_DISABLED = $null + Mock -ModuleName BuildOptimization Get-ChangedFilesForCI { + return @('src/Apps/W1/EDocument/App/src/SomeFile.al') + } + # First call computes and writes cache file + Test-ShouldSkipTestApp -AppName 'Shopify' -BaseFolder $baseFolder -CacheFile $cacheFile | Should -BeTrue + Test-Path $cacheFile | Should -BeTrue + # Second call reads from cache — Get-ChangedFilesForCI not called again + Test-ShouldSkipTestApp -AppName 'E-Document Core' -BaseFolder $baseFolder -CacheFile $cacheFile | Should -BeFalse + Should -Invoke -ModuleName BuildOptimization Get-ChangedFilesForCI -Times 1 -Exactly + } finally { + $env:GITHUB_ACTIONS = $savedActions + $env:GITHUB_EVENT_NAME = $savedEvent + $env:BUILD_OPTIMIZATION_DISABLED = $savedDisabled + } + } + } +} diff --git a/src/Apps/W1/APIReportsFinance/App/CLAUDE.md b/src/Apps/W1/APIReportsFinance/App/CLAUDE.md new file mode 100644 index 0000000000..27f2d9eb06 --- /dev/null +++ b/src/Apps/W1/APIReportsFinance/App/CLAUDE.md @@ -0,0 +1,62 @@ +# API Reports - Finance + +Read-only API layer that exposes raw financial data (GL entries, customer/vendor +ledger entries, budgets, dimensions) through OData/REST endpoints. Unlike the +standard BC API v2.0 financial endpoints (trialBalance, balanceSheet, etc.) which +return pre-aggregated report data, this app exposes granular transactional records +that consumers aggregate themselves -- a "data warehouse" approach for building +custom financial reports. + +## Quick reference + +- **ID range**: 30300--30399 +- **Namespace**: Microsoft.API.FinancialManagement +- **API route**: `api/microsoft/reportsFinance/beta/` +- **API version**: beta (not GA -- endpoints may change) + +## How it works + +The app defines 9 API Pages and 6 API Queries, all read-only. Pages expose +master/reference data (chart of accounts, customers, vendors, dimensions, +budgets, accounting periods, business units, global settings). Queries expose +transactional data (GL entries, GL budget entries, customer ledger entries, +detailed customer ledger entries, vendor ledger entries, detailed vendor ledger +entries). + +Every object follows the same template: `PageType = API` or `QueryType = API`, +`DataAccessIntent = ReadOnly`, all insert/modify/delete disabled, `ODataKeyFields += SystemId`. The app defines no tables of its own -- it reads directly from base +app tables (G/L Entry, Cust. Ledger Entry, etc.). + +The only procedural logic lives in two page triggers: + +- **Accounting Periods** (`APIFinanceAccPeriods.Page.al`): computes + `FiscalYearStartDate`, `FiscalYearEndDate`, and `EndingDate` on each record + fetch. The ending date is derived by peeking at the next period's start date + and subtracting one day. + +- **GL Accounts** (`APIFinanceGLAccount.Page.al`): reconstructs the chart of + accounts parent-child hierarchy at runtime using a `Dictionary` keyed by indentation level. This is order-dependent -- it relies on + records arriving sorted by account number with ascending indentation. + +## Things to know + +- All 15 data endpoints are completely read-only. There is zero write logic, zero + events, zero extensibility hooks, and zero codeunits. +- The GL Account parent tracking via dictionary is fragile. It works because API + pages iterate records in sort order, but the pattern wouldn't survive + re-sorting or filtering that breaks indentation sequencing. +- `globalSettings` is a virtual page -- it reads from both the Company record and + General Ledger Setup in `OnOpenPage`, exposing just company name, LCY code, and + additional reporting currency. +- The app has several caption typos in the query objects: `entryType` is captioned + as `'Entry Number'` in both detailed ledger entry queries, and + `initialEntryGlobalDim2` is captioned as `'...Dimension 1'` instead of + `'...Dimension 2'`. +- The `reveresd` column name in `APIFinanceGLEntry.Query.al` is a typo for + `reversed`. +- GL Budget Entry query has a duplicate column: both `accountNo` and + `generalLedgerAccountNumber` map to the same `G/L Account No.` field. +- Permissions follow the standard stacking pattern: one base permission set + (`API Reports Finance - Objects`) extended into 6 D365 roles. diff --git a/src/Apps/W1/DataArchive/App/src/DataArchiveProvider.Codeunit.al b/src/Apps/W1/DataArchive/App/src/DataArchiveProvider.Codeunit.al index 206a41d08f..d6d47085c6 100644 --- a/src/Apps/W1/DataArchive/App/src/DataArchiveProvider.Codeunit.al +++ b/src/Apps/W1/DataArchive/App/src/DataArchiveProvider.Codeunit.al @@ -135,15 +135,25 @@ codeunit 605 "Data Archive Provider" implements "Data Archive Provider" if RecRef.FindSet() then repeat TableJson.Add(GetRecordJsonFromRecRef(RecRef, FieldList)); + if TableJson.Count() >= 1000 then begin + AddToCachedDataRecords(TableJson, TableIndex); + SaveTable(TableIndex, RecRef.Number); + Clear(TableJson); + end; until RecRef.Next() = 0; end else TableJson.Add(GetRecordJsonFromRecRef(RecRef, FieldList)); + AddToCachedDataRecords(TableJson, TableIndex); + if TableJson.Count() >= 1000 then + SaveTable(TableIndex, RecRef.Number); + end; + + local procedure AddToCachedDataRecords(var TableJson: JsonArray; TableIndex: Integer) + begin if CachedDataRecords.Count >= TableIndex then CachedDataRecords.Set(TableIndex, TableJson) else CachedDataRecords.Add(TableIndex, TableJson); - if TableJson.Count() >= 10000 then - SaveTable(TableIndex, RecRef.Number); end; procedure StartSubscriptionToDelete() diff --git a/src/Apps/W1/DataArchive/Test/TestDataArchiveImpl.codeunit.al b/src/Apps/W1/DataArchive/Test/TestDataArchiveImpl.codeunit.al index 11c3a15be2..090a5fa09f 100644 --- a/src/Apps/W1/DataArchive/Test/TestDataArchiveImpl.codeunit.al +++ b/src/Apps/W1/DataArchive/Test/TestDataArchiveImpl.codeunit.al @@ -5,6 +5,7 @@ namespace System.Test.DataAdministration; +using Microsoft.Foundation.Address; using Microsoft.Sales.Customer; using System.DataAdministration; using System.TestLibraries.Utilities; @@ -75,6 +76,25 @@ codeunit 139504 "Test Data Archive Impl." Assert.IsTrue(DataArchiveTable."Table Fields (json)".HasValue(), 'The table fields field is empty.'); end; + [Test] + [TransactionModel(TransactionModel::AutoRollback)] + procedure TestSaveRecordsWithSplit() + var + DataArchiveTable: Record "Data Archive Table"; // data archive app + CurrCount: Integer; + NewArchiveNo: Integer; + begin + CurrCount := DataArchiveTable.Count(); + NewArchiveNo := CreateLargeArchive(); // >1100 records + Assert.AreNotEqual(0, NewArchiveNo, 'New archive returned 0'); + Assert.AreEqual(CurrCount + 2, DataArchiveTable.Count(), 'Expected exactly two extra archive records.'); + DataArchiveTable.SetRange("Data Archive Entry No.", NewArchiveNo); + DataArchiveTable.FindFirst(); + Assert.AreEqual(1000, DataArchiveTable."No. of Records", 'Wrong no. of records.'); + Assert.IsTrue(DataArchiveTable."Table Data (json)".HasValue(), 'The table data field is empty.'); + Assert.IsTrue(DataArchiveTable."Table Fields (json)".HasValue(), 'The table fields field is empty.'); + end; + [Test] [TransactionModel(TransactionModel::AutoRollback)] procedure TestSaveSaveAsCSV() @@ -158,4 +178,24 @@ codeunit 139504 "Test Data Archive Impl." DataArchiveInterface.Save(); exit(NewArchiveNo); end; + + local procedure CreateLargeArchive(): Integer + var + CountryRegion: Record "Country/Region"; + DataArchiveInterface: Codeunit "Data Archive"; // System App + RecRef: RecordRef; + NewArchiveNo: Integer; + i: Integer; + begin + NewArchiveNo := DataArchiveInterface.Create('New Archive'); + for i := 1 to 1100 do begin + CountryRegion.Code := Format(100000 + i); + CountryRegion.Name := CountryRegion.Code; + if CountryRegion.Insert() then; + end; + RecRef.GetTable(CountryRegion); + DataArchiveInterface.SaveRecords(RecRef); // should trigger a split after the first 1000 records + DataArchiveInterface.Save(); // save the remaining + exit(NewArchiveNo); + end; } diff --git a/src/Apps/W1/DataCorrectionFA/App/CLAUDE.md b/src/Apps/W1/DataCorrectionFA/App/CLAUDE.md new file mode 100644 index 0000000000..ecdf06681b --- /dev/null +++ b/src/Apps/W1/DataCorrectionFA/App/CLAUDE.md @@ -0,0 +1,50 @@ +# Troubleshoot FA Ledger Entries + +This app detects and corrects rounding issues in Fixed Asset ledger entries. Some FA entries end up with amounts that have more decimal places than the currency's rounding precision allows (e.g., 1234.567 when precision is 0.01), which can cause downstream posting and reporting problems. The app scans for these entries, shows them to the user on the Fixed Asset Card via a notification, and lets them accept a rounded correction that writes directly back to the FA Ledger Entry table. + +## Quick reference + +| Item | Value | +|------|-------| +| ID range | 6090-6099 | +| Namespace | `Microsoft.FixedAssets.Repair` | +| Dependencies | None (base app + platform only) | +| App ID | `7961e9dc-a8e5-49b1-839b-3a78803a4cb8` | +| Object count | 1 table, 1 tableextension, 2 codeunits, 1 page, 10 permission objects | + +## How it works + +The app uses a **shadow table** pattern. Rather than flagging issues on the real FA Ledger Entry table, it copies problematic entries into its own staging table (`FA Ledg. Entry w. Issue`, table 6090). This avoids modifying production data during the detection phase and gives users a safe review step before any corrections happen. + +**Detection** is handled by codeunit 6090 `FA Ledger Entries Scan`. It iterates FA Ledger Entries starting from a high-water mark stored on FA Setup (`LastEntryNo`), compares each entry's Amount to `Round(Amount, Currency."Amount Rounding Precision")`, and copies mismatched entries into the issues table. The scan is incremental -- it only looks at entries newer than the last scan, so repeated runs are cheap. The high-water mark advances to one past the last entry checked. + +**Triggering** the scan is lazy and background-based. When a user opens a Fixed Asset Card, codeunit 6091 `FA Card Notifications` subscribes to `OnAfterGetCurrRecordEvent`. It checks a cooldown (7 days, stored as `Last time scanned` on FA Setup) and if enough time has passed, schedules the scan as a background task via `TaskScheduler.CreateTask` with a 1-second delay. This keeps the UI responsive -- the scan never blocks the page load. The notification only appears if the issues table already has uncorrected entries for that specific FA. + +**Correction** happens on page 6090 `FA Ledger Entries Issues`. The user selects entries and clicks "Accept Selected". The page action directly modifies the real `FA Ledger Entry` record: it rounds the Amount, recalculates Debit/Credit amounts respecting the Correction flag, and marks the shadow entry as corrected. This is a permanent write to posted ledger entries -- there is no reversal mechanism. + +## Structure + +- **`src/tables/`** -- The shadow table for flagged entries and a tableextension on FA Setup for scan state (high-water mark, last scan timestamp). +- **`src/codeunits/`** -- Scanner that detects rounding issues, and notification handler that wires into the Fixed Asset Card page. +- **`src/pages/`** -- Review and correction page where users inspect flagged entries and accept fixes. +- **`Permissions/`** -- Standard permission scaffolding. Extends D365 AUTOMATION, BASIC ISV, BUS FULL ACCESS, BUS PREMIUM, FA EDIT, and FULL ACCESS sets. The layered sets (Objects/Read/Edit) follow the BC permission set pattern. + +## Things to know + +- **The app modifies posted ledger entries directly.** The "Accept Selected" action writes to `FA Ledger Entry` without creating correcting entries or journal lines. This is unusual in BC where posted entries are typically immutable. The design is intentional -- these are sub-penny rounding fixes, not business corrections. + +- **The scan is incremental, not full-table.** `FASetup.LastEntryNo` acts as a checkpoint. If you need to re-scan previously checked entries (e.g., after changing currency rounding precision), you would need to reset this field manually. + +- **The 7-day cooldown is hardcoded.** `GetCacheRefreshInterval()` returns a fixed 1-week duration. There is no setup field to configure this. The cooldown prevents the scan from being scheduled on every Fixed Asset Card open. + +- **Scan runs as a background task, not inline.** `TaskScheduler.CreateTask` means the scan executes in a separate session. The notification on the FA Card shows results from the *previous* scan, not a live check. A user opening the card for the first time after install will not see a notification until after the background task completes and they revisit the card. + +- **The Amount field on the issues table stores the already-rounded value, not the original.** `TransferFields` copies from FA Ledger Entry, then the page's `OnAfterGetRecord` recalculates the original amount display and rounding difference on the fly. The `OriginalAmount` and `Rounding` columns on the page are computed, not stored. + +- **Debit/Credit recalculation respects the Correction flag.** When rounding the amount, the code checks both the sign of the amount and the `Correction` boolean to determine which side (Debit vs Credit) gets the value. This mirrors standard BC posting logic where correction entries flip the normal debit/credit assignment. + +- **No events are published.** The app subscribes to the FA Card page event but does not expose any integration or business events of its own. There are no extension points for customizing the scan logic or correction behavior. + +- **Permission set extensions grant RIMD on the shadow table to all major D365 entitlements.** This means any user with standard BC licensing can see and interact with the correction page. The page itself also declares inline `Permissions` for both the shadow table and the real FA Ledger Entry table (RIMD on both). + +- **The `Commit()` calls in both codeunits are deliberate lock-release patterns.** The scan codeunit locks FA Setup to update the timestamp, then commits to release the lock before the potentially long-running scan loop. The notification codeunit does the same before scheduling the background task. diff --git a/src/Apps/W1/DataSearch/App/DataSearch.page.al b/src/Apps/W1/DataSearch/App/DataSearch.page.al index f8294b29b7..54aec6a33d 100644 --- a/src/Apps/W1/DataSearch/App/DataSearch.page.al +++ b/src/Apps/W1/DataSearch/App/DataSearch.page.al @@ -133,9 +133,9 @@ page 2680 "Data Search" [TryFunction] local procedure ValidateFilter(FilterValue: text) var - DataSearchResultFilterTest: Record "Data Search Result"; + TempDataSearchResultFilterTest: Record "Data Search Result"; begin - DataSearchResultFilterTest.SetFilter(Description, '*' + FilterValue + '*'); // will throw an error if filter is illegal + TempDataSearchResultFilterTest.SetFilter(Description, '*' + FilterValue + '*'); // will throw an error if filter is illegal end; internal procedure LaunchSearch() diff --git a/src/Apps/W1/DataSearch/App/DataSearchResultRecords.page.al b/src/Apps/W1/DataSearch/App/DataSearchResultRecords.page.al index 7d4cca7aa1..eb4d14457a 100644 --- a/src/Apps/W1/DataSearch/App/DataSearchResultRecords.page.al +++ b/src/Apps/W1/DataSearch/App/DataSearchResultRecords.page.al @@ -394,9 +394,9 @@ page 2682 "Data Search Result Records" local procedure DrillDown() var - DataSearchResult: Record "Data Search Result"; + TempDataSearchResult: Record "Data Search Result"; begin - DataSearchResult.ShowPage(SourceRecRef); + TempDataSearchResult.ShowPage(SourceRecRef); end; local procedure AdjustColumnOffset(Delta: Integer) diff --git a/src/Apps/W1/DataSearch/App/DataSearchSetupTable.Table.al b/src/Apps/W1/DataSearch/App/DataSearchSetupTable.Table.al index be20db2555..3b0fe40d3b 100644 --- a/src/Apps/W1/DataSearch/App/DataSearchSetupTable.Table.al +++ b/src/Apps/W1/DataSearch/App/DataSearchSetupTable.Table.al @@ -142,11 +142,11 @@ table 2681 "Data Search Setup (Table)" internal procedure GetProfileID(): Code[30] var - UserSettingsRec: Record "User Settings"; + TempUserSettingsRec: Record "User Settings"; UserSettings: Codeunit "User Settings"; begin - UserSettings.GetUserSettings(UserSecurityId(), UserSettingsRec); - exit(UserSettingsRec."Profile ID"); + UserSettings.GetUserSettings(UserSecurityId(), TempUserSettingsRec); + exit(TempUserSettingsRec."Profile ID"); end; procedure GetRoleCenterID(): Integer diff --git a/src/Apps/W1/EDocument/App/.resources/AITools/ubl_example.json b/src/Apps/W1/EDocument/App/.resources/AITools/ubl_example.json new file mode 100644 index 0000000000..d96c56d613 --- /dev/null +++ b/src/Apps/W1/EDocument/App/.resources/AITools/ubl_example.json @@ -0,0 +1,186 @@ +{ + "customization_id": "urn:cen.eu:en16931:2017", + "profile_id": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0", + "id": "", + "issue_date": "", + "due_date": "", + "document_currency_code": "", + "order_reference": { + "id": "" + }, + "accounting_supplier_party": { + "party": { + "party_name": { + "name": "" + }, + "postal_address": { + "street_name": "", + "additional_street_name": "", + "city_name": "", + "postal_zone": "", + "country": { + "identification_code": "" + } + }, + "party_tax_scheme": { + "company_id": "", + "tax_scheme": { + "id": "" + } + }, + "contact": { + "name": "", + "telephone": "", + "electronic_mail": "" + } + } + }, + "accounting_customer_party": { + "party": { + "party_name": { + "name": "" + }, + "postal_address": { + "street_name": "", + "additional_street_name": "", + "city_name": "", + "postal_zone": "", + "country": { + "identification_code": "" + } + }, + "party_tax_scheme": { + "company_id": "", + "tax_scheme": { + "id": "" + } + }, + "contact": { + "name": "", + "telephone": "", + "electronic_mail": "" + } + } + }, + "delivery": { + "actual_delivery_date": "", + "delivery_location": { + "id": { + "scheme_id": "", + "value": "" + }, + "address": { + "street_name": "", + "additional_street_name": "", + "city_name": "", + "postal_zone": "", + "country": { + "identification_code": "" + } + } + }, + "delivery_party": { + "party_name": { + "name": "" + } + } + }, + "payment_means": { + "payment_means_code": "", + "payment_id": "", + "payee_financial_account": { + "id": "", + "name": "", + "financial_institution_branch": { + "id": "" + } + } + }, + "payment_terms": { + "note": "" + }, + "allowance_charge": [ + { + "charge_indicator": false, + "allowance_charge_reason_code": 0, + "allowance_charge_reason": "", + "amount": { + "currency_id": "", + "value": "0" + }, + "tax_category": { + "id": "", + "percent": "0", + "tax_scheme": { + "id": "" + } + } + } + ], + "tax_total": { + "tax_amount": "0", + "tax_subtotal": [ + { + "taxable_amount": "0", + "tax_amount": "0", + "tax_category": { + "id": "", + "percent": "0", + "tax_scheme": { + "id": "" + } + } + } + ] + }, + "legal_monetary_total": { + "line_extension_amount": "0", + "tax_exclusive_amount": "0", + "tax_inclusive_amount": "0", + "allowance_total_amount": "0", + "charge_total_amount": "0", + "payable_amount": "0" + }, + "invoice_line": [ + { + "id": "", + "invoiced_quantity": { + "unit_code": "", + "value": "0" + }, + "line_extension_amount": "0", + "allowance_charge": { + "charge_indicator": false, + "allowance_charge_reason_code": 0, + "allowance_charge_reason": "", + "amount": { + "currency_id": "", + "value": "0" + }, + "tax_category": { + "id": "", + "percent": "0", + "tax_scheme": { + "id": "" + } + } + }, + "item": { + "name": "", + "sellers_item_identification": { + "id": "" + }, + "classified_tax_category": { + "id": "", + "percent": "0", + "tax_scheme": { + "id": "" + } + } + }, + "price": { + "price_amount": "0" + } + } + ] +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/.resources/Prompts/EDocMLLMExtraction-SystemPrompt.md b/src/Apps/W1/EDocument/App/.resources/Prompts/EDocMLLMExtraction-SystemPrompt.md new file mode 100644 index 0000000000..c8ae18e094 --- /dev/null +++ b/src/Apps/W1/EDocument/App/.resources/Prompts/EDocMLLMExtraction-SystemPrompt.md @@ -0,0 +1,24 @@ +You are an data extraction system. Extract ONLY what is explicitly visible on the document into UBL (Universal Business Language) JSON format. + +EXTRACTION RULES: +1. NEVER invent, calculate, or assume values - extract only what you see +2. Use "" for missing text fields +3. Dates: YYYY-MM-DD format +4. Extract ALL invoice lines with sequential IDs starting from "1" +5. Quantity: use "1" only if no quantity column exists on the document + +CUSTOMER vs VENDOR IDENTIFICATION: +The JSON structure includes pre-filled accounting_customer_party data. This is OUR company — the buyer receiving the invoice. Use this to distinguish between customer and vendor on the document: +- The accounting_customer_party (buyer) is already filled in. Keep these values as provided unless the document clearly shows different buyer details. +- The accounting_supplier_party (vendor/seller) is the OTHER party on the invoice — the one sending the invoice and requesting payment. Extract their details from the document. + +CRITICAL FORMAT RULES: +- Country codes: Use ISO 3166-1 alpha-2 (2 letters) +- VAT IDs: Extract only the number with country prefix, no labels (e.g., "DK29399700", NOT "SE. Nr. 31 89 26 86") +- Tax scheme ID: Always use "VAT" +- Tax category ID: Use standard codes: S=Standard rate, Z=Zero rate, E=Exempt, AE=Reverse charge +- Unit codes: Use UN/ECE codes +- Allowance Charge: Leave allowance_charge section empty if no discount/charge exists on the document + + +Output ONLY valid JSON. No markdown, no explanation. \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/CLAUDE.md b/src/Apps/W1/EDocument/App/CLAUDE.md new file mode 100644 index 0000000000..64bc5420cc --- /dev/null +++ b/src/Apps/W1/EDocument/App/CLAUDE.md @@ -0,0 +1,62 @@ +# E-Document Core + +E-Document Core is the framework for electronic document exchange in Business Central. It converts outbound sales/service/purchase documents into structured formats (PEPPOL BIS 3.0, Data Exchange, or custom), transmits them through configurable service integrations, and processes inbound electronic documents into purchase invoices, credit memos, or journal lines. The framework is interface-driven: it defines the contracts, and connector apps provide the actual service communication. + +See [README.md](README.md) for detailed interface implementation examples with code samples. + +## Quick reference + +- **ID range**: 6100-6199, 6208-6209, 6231-6232, 6234 + +## How it works + +Everything starts with the **Document Sending Profile**. When a BC document is posted, `EDocumentSubscribers.Codeunit.al` catches the posting event and looks up the Document Sending Profile for the customer/vendor. If the profile specifies "Extended E-Document Service Flow", the framework follows the referenced **Workflow** to determine which E-Document Services to invoke. The workflow is a first-class citizen -- it orchestrates send, email, approval, and other steps as a sequence of workflow responses. + +An **E-Document Service** (`EDocumentService.Table.al`) combines a **Document Format** (how to serialize, e.g. PEPPOL BIS 3.0) with a **Service Integration** (where to send/receive, e.g. a connector app). Both are enum-based with interface implementations, so you extend them by adding enum values that bind to your interface implementation -- classic strategy pattern via AL enums. + +Outbound documents flow through: check on release -> create E-Document record on post -> export to blob via the "E-Document" interface -> send via IDocumentSender -> optionally poll for async response via IDocumentResponseHandler. Inbound documents have a V2.0 multi-stage pipeline: receive document list -> download each document -> structure (convert PDF to structured data via ADI or passthrough for XML) -> read into draft staging tables -> prepare draft (resolve vendors, items, accounts) -> finish draft (create actual BC purchase document). Each stage is independently reversible. + +The framework does not provide any service connector itself. The `"Service Integration"` enum ships with only `"No Integration"`. Connector apps (like E-Document Connector for Pagero or Avalara) extend this enum and implement IDocumentSender, IDocumentReceiver, and other integration interfaces. Similarly, the `"E-Document Format"` enum ships with two built-in values: `"Data Exchange"` (using BC Data Exchange Definitions) and `"PEPPOL BIS 3.0"`, but can be extended. + +Three status dimensions track document lifecycle: **E-Document Status** (In Progress / Processed / Error -- the overall state), **E-Document Service Status** (per-service granular status like Exported, Sent, Pending Response, Approved, etc.), and **Import Processing Status** (the V2.0 import pipeline stage: Unprocessed -> Readable -> Ready for draft -> Draft Ready -> Processed). + +## Structure + +``` +src/ + ClearanceModel/ -- Tax authority QR code clearance on posted invoices/credit memos + ControlAddIn/ -- PDF Viewer browser control add-in + DataExchange/ -- Data Exchange Definition format impl + PEPPOL pre-mapping + Document/ -- Core E-Document table, status model, direction/type enums, notification + Extensions/ -- Table/page extensions hooking into BC sales, purchase, service documents + Format/ -- PEPPOL BIS 3.0 export/import implementation + Helpers/ -- Utilities: error handling, JSON helpers, logging, blob processing + Integration/ -- Service integration framework: send/receive/action interfaces, runners, context + Logging/ -- E-Document Log, Integration Log, Data Storage tables + Mapping/ -- Field mapping engine for import/export transformations + Processing/ -- Core orchestration: export, import pipeline, subscribers, order matching, AI, providers + SampleInvoice/ -- Demo sample invoice generation + Service/ -- Service configuration, participants, supported document types + Setup/ -- Installation, upgrade, consent management + Workflow/ -- Workflow integration: event triggers, response handlers, setup +``` + +## Documentation + +- [docs/data-model.md](docs/data-model.md) -- How the data fits together +- [docs/business-logic.md](docs/business-logic.md) -- Processing flows and gotchas +- [docs/extensibility.md](docs/extensibility.md) -- Extension points and how to customize +- [docs/patterns.md](docs/patterns.md) -- Recurring code patterns (and legacy ones to avoid) + +## Things to know + +- The E-Document table (`table 6121`) is the central entity. It links to the source BC document via `"Document Record ID"` (a RecordId, not a foreign key). For inbound V1.0 documents, Purchase Header links back via `"E-Document Link"` (a Guid matching `SystemId`), but this is being removed in CLEAN27. +- Inbound documents have two data storage slots: `"Unstructured Data Entry No."` (the raw PDF/file) and `"Structured Data Entry No."` (the parsed XML/JSON). Both point to `"E-Doc. Data Storage"` records containing blobs. +- The import pipeline implementation fields live directly on the E-Document table: `"Structure Data Impl."`, `"Read into Draft Impl."`, and `"Process Draft Impl."`. These enums determine which interface implementations run at each stage. +- `"E-Document Service Status"` enum implements `IEDocumentStatus` interface -- each status value knows whether it means "in progress", "processed", or "error", via `EDocInProgressStatus`, `EDocProcessedStatus`, and `EDocErrorStatus` codeunits. +- Batch processing and single-document processing are distinct code paths. Batch mode uses recurrent background jobs configured on the service (fields 21-26 on `"E-Document Service"`). +- The V1.0 import process (`"Import Process" = "Version 1.0"`) collapses all pipeline stages into a single "Finish draft" step. V2.0 is the current architecture. +- `#if not CLEAN26` and `#if not CLEAN27` blocks mark deprecated code scheduled for removal. The old `"E-Document Integration"` enum and its `"Service Integration"` field on the service table are fully replaced by `"Service Integration V2"`. +- The framework uses the "if codeunit.run" pattern extensively -- interface calls are wrapped in codeunits that run with error trapping, so a connector failure produces a logged error rather than a crash. +- `"E-Document Background Jobs"` manages Job Queue Entries for recurrent import polling and batch send processing. +- The `IExportEligibilityEvaluator` interface (field `"Export Eligibility Evaluator"` on the service) lets connector apps control which documents should be exported via their service, beyond the basic document type check. diff --git a/src/Apps/W1/EDocument/App/app.json b/src/Apps/W1/EDocument/App/app.json index d5c15fcea0..4ed9bcb273 100644 --- a/src/Apps/W1/EDocument/App/app.json +++ b/src/Apps/W1/EDocument/App/app.json @@ -49,6 +49,18 @@ { "from": 6208, "to": 6209 + }, + { + "from": 6231, + "to": 6232 + }, + { + "from": 6234, + "to": 6234 + }, + { + "from": 6401, + "to": 6410 } ], "resourceExposurePolicy": { diff --git a/src/Apps/W1/EDocument/App/docs/business-logic.md b/src/Apps/W1/EDocument/App/docs/business-logic.md new file mode 100644 index 0000000000..8248f29f32 --- /dev/null +++ b/src/Apps/W1/EDocument/App/docs/business-logic.md @@ -0,0 +1,146 @@ +# Business logic + +This document covers the main processing flows in E-Document Core, the decision points, and non-obvious behavior. For how the data fits together, see [data-model.md](data-model.md). For extension points, see [extensibility.md](extensibility.md). + +## Outbound flow + +### Trigger: posting a BC document + +`EDocumentSubscribers.Codeunit.al` subscribes to posting events across Sales-Post, Purch.-Post, Service-Post, and Gen. Jnl.-Post Line. When a sales invoice is posted, for example, `OnAfterPostSalesDoc` fires. The subscriber also hooks into release events (`OnBeforeReleaseSalesDoc`, etc.) to run pre-flight checks before the user commits to posting. + +The subscriber resolves the Document Sending Profile for the document and, if it specifies `"Extended E-Document Service Flow"`, delegates to `EDocExport.Codeunit.al`. + +### Check, create, export, send + +The outbound process has four distinct phases, orchestrated by `EDocExport` and the workflow engine: + +```mermaid +flowchart TD + A[BC document posted] --> B[EDocumentSubscribers catches event] + B --> C{Document Sending Profile?} + C -->|Not E-Doc| D[Skip] + C -->|Extended E-Document Service Flow| E["EDocExport.CheckEDocument()"] + E --> F["EDocExport.CreateEDocument()"] + F --> G{Service supports doc type?} + G -->|No| H[Skip service] + G -->|Yes| I["IExportEligibilityEvaluator.ShouldExport()"] + I -->|No| H + I -->|Yes| J["ExportEDocument() -- format interface.Create()"] + J --> K{Batch processing?} + K -->|Yes| L[Queue for batch send] + K -->|No| M[Start E-Document Created workflow] + M --> N["Workflow response: Send to service"] + N --> O["EDocIntegrationManagement.Send()"] + O --> P{Async?} + P -->|No| Q[Status: Sent] + P -->|Yes| R[Status: Pending Response] + R --> S[Background job: GetResponse loop] +``` + +**CheckEDocument** runs during release (not posting). It finds the workflow, iterates over the E-Document Services in the workflow, and for each service that supports the document type, calls `interface "E-Document".Check()` on the format implementation. This lets format implementations validate that required fields are present before the user posts. + +**CreateEDocument** runs during posting. It creates the E-Document record, populates it from the source document header via `PopulateEDocument()` (which uses RecordRef field reads -- it works generically across Sales, Purchase, Service, Finance Charge, Reminder, and Transfer Shipment headers), then creates service status records for each supported service. + +**ExportEDocument** invokes the format interface's `Create()` method (wrapped in `EDocumentCreate.Codeunit.al` for error trapping). The exported blob is stored via `EDocumentLog.InsertLog()` which creates an `"E-Doc. Data Storage"` entry. Before calling the format, field mappings from `"E-Doc. Mapping"` are applied via `MapEDocument()`, producing mapped RecordRefs. + +**Send** happens when the workflow fires the "Send E-Document" response. `EDocIntegrationManagement.Send()` retrieves the exported blob from the log, wraps it in a `SendContext`, and calls `IDocumentSender.Send()` through the `SendRunner` codeunit (error-trapping wrapper). The implementation sets `IsAsync` to true if the service is asynchronous. For async services, the status becomes `"Pending Response"` and a background job polls `IDocumentResponseHandler.GetResponse()` until it returns true. + +### Batch processing + +When `"Use Batch Processing"` is enabled on the service, documents are not sent immediately. Instead they accumulate with status `"Pending Batch"`. A recurrent Job Queue Entry (configured via `"Batch Start Time"` / `"Batch Minutes between runs"`) collects pending documents and calls `ExportEDocumentBatch()` / `SendBatch()`. The batch mode can be threshold-based (send when N documents accumulate) or time-based. + +### Error handling + +Errors during export or send do not crash the posting transaction. Every interface call is wrapped in a "if codeunit.run" pattern: the framework commits before calling the interface implementation, runs it inside a codeunit, and catches runtime errors via `GetLastErrorText()`. Errors are recorded in `"E-Document Error Helper"` as error messages on the E-Document record, and the service status is set to the appropriate error state (`Export Error`, `Sending Error`, etc.). The user can then fix the issue and retry from the E-Document card. + +## Inbound flow (V2.0 pipeline) + +### Receive + +`EDocImport.ReceiveAndProcessAutomatically()` is the entry point, typically triggered by a recurrent import Job Queue Entry (configured via `"Auto Import"` on the service). It calls `EDocIntegrationManagement.ReceiveDocuments()`, which invokes `IDocumentReceiver.ReceiveDocuments()` to get a list of document metadata blobs, then for each document calls `DownloadDocument()` to fetch the actual content. If the receiver also implements `IReceivedDocumentMarker`, the framework calls `MarkFetched()` after download so the service knows the document was consumed. + +Each successfully downloaded document gets an E-Document record (Direction = Incoming, Status = Imported), with the raw content stored in `"E-Doc. Data Storage"` and linked via `"Unstructured Data Entry No."`. + +### Import pipeline stages + +After receiving, each document goes through the V2.0 pipeline managed by `ImportEDocumentProcess.Codeunit.al`. The pipeline uses `EDocImport.GetEDocumentToDesiredStatus()` to advance or revert the document to a target state. This is a bidirectional state machine -- it can undo steps to go backward. + +```mermaid +flowchart TD + A[Unprocessed] -->|"Structure received data"| B[Readable] + B -->|"Read into Draft"| C["Ready for draft"] + C -->|"Prepare draft"| D["Draft Ready"] + D -->|"Finish draft"| E[Processed] + E -.->|Undo| D + D -.->|Undo| C + C -.->|Undo| B + B -.->|Undo| A +``` + +**Structure received data** (`ImportEDocumentProcess.StructureReceivedData()`): Converts the raw blob into a structured format. The implementation is determined by `"Structure Data Impl."` on the E-Document, which defaults to the preferred implementation for the file format (e.g., PDFs default to Azure Document Intelligence processing). The `IStructureReceivedEDocument` interface returns an `IStructuredDataType` that contains the structured content and specifies how to read it. For already-structured documents (XML), this is a passthrough -- the structured entry number just points to the same storage as the unstructured one. + +**Read into Draft** (`ImportEDocumentProcess.ReadIntoDraft()`): The `IStructuredFormatReader.ReadIntoDraft()` method parses the structured data and populates the E-Document Purchase Header/Line staging tables. It returns the `"E-Doc. Process Draft"` enum value that determines which `IProcessStructuredData` implementation runs next. + +**Prepare draft** (`ImportEDocumentProcess.PrepareDraft()`): `IProcessStructuredData.PrepareDraft()` resolves BC entities from the parsed data -- finding the vendor, matching items, resolving units of measure, assigning GL accounts. This uses a chain of provider interfaces (`IVendorProvider`, `IItemProvider`, `IUnitOfMeasureProvider`, `IPurchaseLineProvider`, `IPurchaseOrderProvider`). The result is a draft with BC-specific field values filled in. + +**Finish draft** (`ImportEDocumentProcess.FinishDraft()`): `IEDocumentFinishDraft.ApplyDraftToBC()` creates the actual BC document (Purchase Invoice, Purchase Credit Memo, etc.) from the staging tables. It returns the RecordId of the created document, which is stored in `"Document Record ID"` on the E-Document. + +### Reversibility + +Each step can be undone via `UndoProcessingStep()`. Undoing "Finish draft" calls `IEDocumentFinishDraft.RevertDraftActions()`, which de-links the BC document from the E-Document (clears the `"E-Document Link"` field), transfers PO matches and attachments back to the E-Document, but does **not** delete the BC document itself -- it must be handled separately. Undoing "Prepare draft" clears the BC-resolved fields and resets the document type. Undoing "Structure received data" clears the structured data reference. This lets users go back to an earlier stage, correct data, and re-process. + +### Automatic vs. manual processing + +The `"Automatic Import Processing"` field on the service controls whether received documents are automatically processed through the full pipeline or stop at the `Unprocessed` state for manual review. The `GetDefaultImportParameters()` method on the service table produces the appropriate parameters. + +## Clearance model + +The clearance model handles tax authority pre-approval workflows. After an outbound document is sent and approved, some jurisdictions require a clearance step where the tax authority returns a QR code to embed on the printed invoice. The clearance status is tracked via `"E-Document Service Status"` values `"Not Cleared"` and `"Cleared"`, and QR code data is managed by `EDocumentQRCodeManagement.Codeunit.al`. Report extensions on posted sales/service invoices and credit memos render the QR code. + +## Actions framework + +Beyond send and receive, the framework supports arbitrary actions on documents via `EDocIntegrationManagement.InvokeAction()`. The `"Integration Action Type"` enum (extensible) dispatches to `IDocumentAction.InvokeAction()` implementations. Built-in action types are `"Sent Document Approval"` and `"Sent Document Cancellation"`, which delegate to `ISentDocumentActions.GetApprovalStatus()` and `GetCancellationStatus()` on the service integration. Connector apps can add custom action types. + +## Gotchas + +- **Commit before interface calls**: The framework commits before every interface call (format Create, service Send, receiver ReceiveDocuments, etc.) to support error trapping. This means if the interface fails, the E-Document record and logs are already persisted. The trade-off is that you cannot roll back the E-Document creation if the format export fails. +- **Re-reading after interface calls**: After every interface call, the framework re-reads the E-Document and service records from the database (`EDocument.Get(EDocument."Entry No")`). This is because interface implementations may modify these records, and the framework needs the latest values. Note: this defensive re-read is most important when the interface parameter is **not** passed by `var`. The original intent of non-`var` parameters was to prevent implementations from modifying records through the parameter, but since AL code can always call `.Modify()` directly on any record, the protection is incomplete. When adding new interface methods, only apply this re-read pattern to procedures where the record is not passed by `var` -- if the signature already uses `var`, the caller expects modifications and re-reading is redundant. +- **E-Document Status is derived**: The overall `E-Document Status` is derived from the service statuses. `EDocumentProcessing.ModifyEDocumentStatus()` computes it after every service status change using the `IEDocumentStatus` interface on the enum values. +- **Import Processing Status is a FlowField**: On the E-Document table, `"Import Processing Status"` is a FlowField that reads from `"E-Document Service Status"`. You must call `CalcFields` before reading it. +- **V1.0 and V2.0 coexistence**: The `"Import Process"` field on the service determines which path runs. V1.0 collapses everything into a single "Finish draft" step that calls the old `V1_ProcessEDocument` logic. The pipeline state machine still runs, but only the last step does anything for V1.0 documents. + +## Inbound flow (V1.0 pipeline) + +The V1.0 import path is the original single-pass process. It is still active when a service has `"Import Process" = "Version 1.0"`. The main logic lives in `EDocImport.Codeunit.al` (codeunit 6140). + +### Flow + +```mermaid +flowchart TD + A[E-Document received with raw blob] --> B["V1_ProcessEDocument()"] + B --> C["GetDocumentBasicInfo() -- extract vendor, invoice no., dates via format interface"] + C --> D["ParseDocumentLines() -- EDocGetFullInfo.Run() parses full content into RecordRef staging"] + D --> E["MapEDocument() -- apply field mappings"] + E --> F{Auto-process?} + F -->|No| G["Status = Imported, wait for manual action"] + F -->|Yes| H{Purchase Order linked?} + H -->|Yes| I["ReceiveEDocumentToPurchaseOrder() -- link to existing PO"] + H -->|No| J{Create journal line?} + J -->|Yes| K["CreateJournalLineFromImportedDocument()"] + J -->|No| L["ReceiveEDocumentToPurchaseDoc() -- create Purchase Invoice/Credit Memo"] + I --> M["Status = Processed"] + K --> M + L --> M +``` + +### Key methods + +- **`GetDocumentBasicInfo()`** -- calls the format interface's `GetBasicInfoFromReceivedDocument()` to extract vendor, invoice number, dates, and currency from the raw blob. +- **`ParseDocumentLines()`** -- runs `EDocGetFullInfo` (a runner codeunit that calls the format interface's `GetCompleteInfoFromReceivedDocument()`) to parse the full document into Purchase Header/Line RecordRef staging tables, then applies field mappings. +- **`ReceiveEDocumentToPurchaseDoc()`** -- calls `CreatePurchaseDocumentFromImportedDocument()`, which resolves items, units of measure, and G/L accounts, then creates a Purchase Invoice or Credit Memo via `EDocumentCreatePurchase`. +- **`CreateJournalLineFromImportedDocument()`** -- alternative path that creates a Gen. Journal Line instead of a purchase document, controlled by the `CreateJournalLineV1` import parameter. + +### Why V1.0 is deprecated + +V1.0 has no staging tables, no user review step, and no reversibility. Format implementations must know how to produce complete purchase documents, violating separation of concerns. The V2.0 pipeline splits this into discrete, undoable stages with provider interfaces for entity resolution. +- **Duplicate detection**: `E-Document.IsDuplicate()` checks for matching `"Incoming E-Document No."`, `"Bill-to/Pay-to No."`, and `"Document Date"`. Duplicates can be deleted without confirmation; unique documents require explicit user confirmation. diff --git a/src/Apps/W1/EDocument/App/docs/data-model.md b/src/Apps/W1/EDocument/App/docs/data-model.md new file mode 100644 index 0000000000..4733b23fe8 --- /dev/null +++ b/src/Apps/W1/EDocument/App/docs/data-model.md @@ -0,0 +1,79 @@ +# Data model + +This document covers how the E-Document Core data model is organized, what the key relationships mean, and design decisions worth knowing about. For interface contracts and code examples, see [README.md](../README.md). For processing flows, see [business-logic.md](business-logic.md). + +## Core document lifecycle + +The central entity is the **E-Document** table (`table 6121 "E-Document"`). Every electronic document -- inbound or outbound -- gets exactly one E-Document record. It stores header-level data extracted from the source BC document (amounts, dates, customer/vendor info) plus metadata about how the document was processed. + +An E-Document is always associated with at least one **E-Document Service Status** record (`table 6138`), which tracks the per-service state. The composite key is `(E-Document Entry No, E-Document Service Code)`. In practice, most documents have exactly one service status record, but the model supports multiple services per document (e.g., send via PEPPOL and also via email through different workflow steps). + +Every state transition produces an **E-Document Log** entry (`table 6124`), which captures the service status at that moment and optionally links to a **E-Doc. Data Storage** record (`table 6125`) containing the document blob. The separation between log and storage is intentional: multiple log entries can reference the same storage entry (for batch imports where many documents share one downloaded blob). + +The **E-Document Integration Log** (`table 6126`, in the Logging folder) stores HTTP request/response pairs from service communication. It links to both the E-Document and the service, providing a full communication audit trail. + +```mermaid +erDiagram + E-DOCUMENT ||--o{ E-DOCUMENT-SERVICE-STATUS : "has statuses" + E-DOCUMENT ||--o{ E-DOCUMENT-LOG : "has log entries" + E-DOCUMENT-LOG }o--o| E-DOC-DATA-STORAGE : "references blob" + E-DOCUMENT ||--o{ E-DOCUMENT-INTEGRATION-LOG : "has comms log" +``` + +## Service configuration + +An **E-Document Service** (`table 6103`) defines a processing endpoint. It combines a Document Format (the `"E-Document Format"` enum, which implements the `"E-Document"` interface for serialization) with a Service Integration (the `"Service Integration"` enum, which implements `IDocumentSender` and `IDocumentReceiver`). Both are extensible enums that connector apps extend. + +The service also holds import pipeline configuration: `"Import Process"` (Version 1.0 or 2.0), `"Read into Draft Impl."` (how to parse structured data), and a set of boolean flags that control import behavior (Validate Receiving Company, Resolve Unit Of Measure, Lookup Item Reference, Lookup Item GTIN, Lookup Account Mapping, etc.). + +**E-Doc. Service Supported Type** links services to the document types they handle. This is a simple junction table keyed by `(Service Code, E-Document Type)`. A service that handles Sales Invoices and Sales Credit Memos has two records here. + +**Service Participant** links a service to specific business partners (customers or vendors), identified by participant identifiers and schemes. This is used for participant-based routing -- knowing which electronic identifier to use for a given trading partner on a given service. + +**E-Doc. Mapping** (`table 6119`) provides field-level transformations: renaming field values on export or import per service. For example, mapping a BC field value to a PEPPOL code. **E-Doc. Mapping Log** records which mappings were applied to each document. + +```mermaid +erDiagram + E-DOCUMENT-SERVICE ||--o{ E-DOC-SERVICE-SUPPORTED-TYPE : "supports types" + E-DOCUMENT-SERVICE ||--o{ SERVICE-PARTICIPANT : "has participants" + E-DOCUMENT-SERVICE ||--o{ E-DOC-MAPPING : "defines mappings" +``` + +## Inbound staging tables (V2.0 import pipeline) + +V2.0 inbound processing uses dedicated staging tables to hold parsed document data before creating actual BC purchase documents. This three-layer approach (raw blob -> staging tables -> BC documents) isolates parsing from business logic and allows users to review and correct data before committing. + +**E-Document Purchase Header** (`table 6100`) and **E-Document Purchase Line** (`table 6101`, in `Processing/Import/Purchase/`) hold the parsed invoice data in a vendor-neutral format. The purchase header has two kinds of fields: external data (fields 2-100, prefixed with vendor/customer names, addresses, amounts as raw text) and BC-resolved fields (fields 101+, prefixed with `[BC]`, containing resolved vendor numbers, posting groups, etc.). The purchase line follows the same pattern. Both are keyed by E-Document Entry No. + +**E-Document Header Mapping** and **E-Document Line Mapping** (`Processing/Import/`) track how staging table fields were mapped to BC entities during the "Prepare draft" step. + +For order matching scenarios (linking inbound invoices to existing purchase orders), the **E-Doc. Imported Line** table (`Processing/OrderMatching/`) stores lines from the imported document alongside references to matched purchase order lines. + +```mermaid +erDiagram + E-DOCUMENT ||--o| E-DOCUMENT-PURCHASE-HEADER : "parsed into" + E-DOCUMENT-PURCHASE-HEADER ||--o{ E-DOCUMENT-PURCHASE-LINE : "has lines" + E-DOCUMENT ||--o{ E-DOC-IMPORTED-LINE : "for order matching" +``` + +## Dual data storage + +Every inbound E-Document can have two blob references: `"Unstructured Data Entry No."` and `"Structured Data Entry No."`, both pointing to `"E-Doc. Data Storage"` records. The unstructured slot holds the original file as received (e.g., a PDF). The structured slot holds the machine-readable version (e.g., XML extracted by Azure Document Intelligence, or the original XML if the document was already structured). + +When a PDF is received and processed through the "Structure received data" step, the original PDF is moved to a Document Attachment for reference, and the structured output replaces the working data. If the received document is already structured (XML), both slots point to the same storage entry. + +Each `"E-Doc. Data Storage"` record has a `"File Format"` enum field that implements `IEDocFileFormat` and `IBlobType` interfaces. These determine how to preview the content and what structuring methods are available. + +## Linking to BC documents + +Outbound E-Documents link to their source BC document via `"Document Record ID"` (a RecordId). This is a BC runtime reference, not a traditional foreign key, which means it works across any source table (Sales Invoice Header, Service Cr.Memo Header, etc.) without explicit table relations. + +Inbound E-Documents, once the import pipeline completes, store the created BC document's RecordId in the same `"Document Record ID"` field. The E-Document table's `OnDelete` trigger prevents deletion of linked or processed documents. + +Extensions on BC tables add E-Document awareness: `"E-Document Link"` (a Guid) on `Purchase Header` links back to the E-Document's SystemId for V1.0 processing (marked for removal in CLEAN27). Page extensions on Posted Sales Invoice, Posted Purchase Invoice, etc. add factboxes showing E-Document status. + +## Status model + +The E-Document has three independent status dimensions. The **E-Document Status** enum (`enum 6108`) is the coarsest: `In Progress`, `Processed`, or `Error`. The **E-Document Service Status** enum (`enum 6106`) is the granular per-service status with values like `Created`, `Exported`, `Sent`, `Pending Response`, `Approved`, `Rejected`, `Imported`, `Imported Document Created`, `Not Cleared`, `Cleared`, and various error states. Notably, this enum implements `IEDocumentStatus` -- each value is bound to one of three status codeunits (`EDocInProgressStatus`, `EDocProcessedStatus`, `EDocErrorStatus`) that know how to derive the overall E-Document Status. + +The **Import E-Doc. Proc. Status** enum (`enum 6100`) tracks the V2.0 import pipeline position: `Unprocessed` -> `Readable` -> `Ready for draft` -> `Draft Ready` -> `Processed`. This is stored on the E-Document Service Status table (field 4) and exposed as a FlowField on the E-Document table. Transitions between these states are defined by the `"Import E-Document Steps"` enum. diff --git a/src/Apps/W1/EDocument/App/docs/extensibility.md b/src/Apps/W1/EDocument/App/docs/extensibility.md new file mode 100644 index 0000000000..4f97012b84 --- /dev/null +++ b/src/Apps/W1/EDocument/App/docs/extensibility.md @@ -0,0 +1,226 @@ +# Extensibility + +This document is organized by developer intent -- what you want to customize, not which codeunit to look at. For the processing flows that call these extension points, see [business-logic.md](business-logic.md). For code examples of interface implementations, see [README.md](../README.md). + +## Core pattern: enum extension binds interface implementation + +All major extension points follow the same pattern. The framework defines an extensible enum that implements one or more interfaces. You extend the enum with a new value and bind it to your codeunit that implements the interface. At runtime, the framework reads the enum value from configuration and dispatches to your implementation. + +Example: to add a custom document format, extend `enum 6101 "E-Document Format"` with a new value that implements the `"E-Document"` interface. Then set that format on the E-Document Service. No event subscriptions needed. + +## Document format + +**Goal**: Define how BC documents are serialized to/from electronic formats. + +**Interface**: `"E-Document"` (in `Document/Interfaces/EDocument.Interface.al`) + +**Methods**: + +- `Check(SourceDocumentHeader, EDocumentService, EDocumentProcessingPhase)` -- validate on release/post +- `Create(EDocumentService, EDocument, SourceDocumentHeader, SourceDocumentLines, TempBlob)` -- serialize to blob +- `CreateBatch(EDocumentService, EDocuments, SourceDocumentHeaders, SourceDocumentsLines, TempBlob)` -- batch serialize +- `GetBasicInfoFromReceivedDocument(EDocument, TempBlob)` -- extract header info from received blob (V1.0 import) +- `GetCompleteInfoFromReceivedDocument(EDocument, CreatedDocumentHeader, CreatedDocumentLines, TempBlob)` -- parse into BC records (V1.0 import) + +**Binding enum**: `"E-Document Format"` (`enum 6101`). Built-in values: `"Data Exchange"` and `"PEPPOL BIS 3.0"`. + +**Where configured**: `"Document Format"` field on `"E-Document Service"` table. + +## Service integration (send and receive) + +**Goal**: Connect to an external service endpoint for sending and receiving electronic documents. + +### Sending + +**Interface**: `IDocumentSender` (in `Integration/Interfaces/IDocumentSender.Interface.al`) + +``` +procedure Send(var EDocument, var EDocumentService, SendContext) +``` + +The `SendContext` provides the exported blob via `GetTempBlob()`, HTTP request/response objects via `Http()`, and a status object via `Status()`. Set the status to control the resulting service status (default is `Sent`). + +For **async sending**, your sender implementation must also implement `IDocumentResponseHandler`: + +``` +procedure GetResponse(var EDocument, var EDocumentService, SendContext): Boolean +``` + +Return `true` when the service confirms receipt (status becomes `Sent`), `false` to keep polling (status stays `Pending Response`). A runtime error or logged error message sets `Sending Error`. + +### Receiving + +**Interface**: `IDocumentReceiver` (in `Integration/Interfaces/IDocumentReceiver.Interface.al`) + +Two methods: + +- `ReceiveDocuments(EDocumentService, DocumentsMetadata, ReceiveContext)` -- query the service for available documents, add one TempBlob per document to the list +- `DownloadDocument(EDocument, EDocumentService, DocumentMetadata, ReceiveContext)` -- download a single document's content into `ReceiveContext.GetTempBlob()` + +Optionally implement `IReceivedDocumentMarker` to mark documents as fetched on the service after download: + +``` +procedure MarkFetched(EDocument, EDocumentService, DocumentBlob, ReceiveContext) +``` + +### Consent + +**Interface**: `IConsentManager` (in `Integration/Interfaces/IConsentManager.Interface.al`) + +Invoked when a user selects a service integration for the first time. Return `true` to allow, `false` to block. + +### Binding enum + +`"Service Integration"` (`enum 6151`). Implements `IDocumentSender`, `IDocumentReceiver`, and `IConsentManager`. Built-in value: `"No Integration"`. + +**Where configured**: `"Service Integration V2"` field on `"E-Document Service"` table. + +## Actions on sent documents + +**Goal**: Define custom actions that can be performed on E-Documents after sending (approval checks, cancellation, etc.). + +**Interface**: `IDocumentAction` (in `Integration/Interfaces/IDocumentAction.Interface.al`) + +``` +procedure InvokeAction(var EDocument, var EDocumentService, ActionContext): Boolean +``` + +Return `true` to update the E-Document status to `ActionContext.Status().GetStatus()`, `false` to leave it unchanged. + +**Binding enum**: `"Integration Action Type"` (`enum 6170`). Built-in: `"Sent Document Approval"` and `"Sent Document Cancellation"`. + +For the two built-in actions, the framework also checks if the service integration implements `ISentDocumentActions`, which provides `GetApprovalStatus()` and `GetCancellationStatus()` methods. + +## Import pipeline (V2.0) + +### Structuring received documents + +**Goal**: Convert a raw blob (e.g., PDF) into a structured, machine-readable format. + +**Interface**: `IStructureReceivedEDocument` (in `Processing/Interfaces/IStructureReceivedEDocument.Interface.al`) + +``` +procedure StructureReceivedEDocument(EDocumentDataStorage): Interface IStructuredDataType +``` + +Returns an `IStructuredDataType` that wraps the structured content, its file format, and which `"E-Doc. Read into Draft"` implementation should read it. + +**Binding enum**: `"Structure Received E-Doc."` (`enum 6120`). + +### Reading structured data into staging tables + +**Goal**: Parse structured data (XML, JSON, ADI output) into the E-Document Purchase Header/Line staging tables. + +**Interface**: `IStructuredFormatReader` (in `Processing/Interfaces/IStructuredFormatReader.Interface.al`) + +``` +procedure ReadIntoDraft(EDocument, TempBlob): Enum "E-Doc. Process Draft" +procedure View(EDocument, TempBlob) +``` + +`ReadIntoDraft` populates the staging tables and returns the `"E-Doc. Process Draft"` enum value that determines which `IProcessStructuredData` runs next. + +**Binding enum**: `"E-Doc. Read into Draft"` (`enum 6113`). + +### Processing the draft + +**Goal**: Resolve BC entities (vendors, items, accounts) from the parsed data and prepare for document creation. + +**Interface**: `IProcessStructuredData` (in `Processing/Interfaces/IProcessStructuredData.Interface.al`) + +``` +procedure PrepareDraft(EDocument, EDocImportParameters): Enum "E-Document Type" +procedure GetVendor(EDocument, Customizations): Record Vendor +procedure OpenDraftPage(var EDocument) +procedure CleanUpDraft(EDocument) +``` + +`PrepareDraft` returns the resolved document type. `GetVendor` is called separately to populate the E-Document's vendor fields. `CleanUpDraft` is called when an E-Document is deleted. + +**Binding enum**: `"E-Doc. Process Draft"` (`enum 6112`). + +### Finishing the draft (creating BC documents) + +**Goal**: Create the actual BC purchase document from the staging tables. + +**Interface**: `IEDocumentFinishDraft` (in `Processing/Interfaces/IEDocumentFinishDraft.Interface.al`) + +``` +procedure ApplyDraftToBC(EDocument, EDocImportParameters): RecordId +procedure RevertDraftActions(EDocument) +``` + +`ApplyDraftToBC` creates the Purchase Invoice/Credit Memo and returns its RecordId. `RevertDraftActions` undoes the creation (deletes the BC document). + +**Binding enum**: `"E-Document Type"` (`enum 6105`). Each document type value implements this interface to handle its specific creation logic. + +## Provider interfaces + +These interfaces allow customization of specific resolution steps during the "Prepare draft" stage. They are invoked by the `IProcessStructuredData` implementation. + +- **IVendorProvider** -- resolve vendor from E-Document data +- **IItemProvider** -- resolve item from E-Document line, vendor, and unit of measure +- **IUnitOfMeasureProvider** -- resolve unit of measure from external code +- **IPurchaseLineProvider** -- determine purchase line type and account (replaces deprecated `IPurchaseLineAccountProvider`) +- **IPurchaseOrderProvider** -- match E-Document to an existing purchase order + +## Export eligibility + +**Goal**: Control which documents should be exported via a given service, beyond document type matching. + +**Interface**: `IExportEligibilityEvaluator` (in `Processing/Interfaces/IExportEligibilityEvaluator.Interface.al`) + +``` +procedure ShouldExport(EDocumentService, SourceDocumentHeader, DocumentType): Boolean +``` + +**Binding enum**: `"Export Eligibility Evaluator"`. Configured on the service's `"Export Eligibility Evaluator"` field. + +## AI tools + +**Goal**: Register AI-powered processing capabilities (PDF classification, GL account matching, etc.). + +**Interface**: `IEDocAISystem` (in `Processing/Interfaces/IEDocAISystem.Interface.al`) + +``` +procedure GetSystemPrompt(UserLanguage): SecretText +procedure GetTools(): List of [Interface "AOAI Function"] +procedure GetFeatureName(): Text +``` + +Implementations provide a system prompt, a list of AOAI Function tools, and a feature name for telemetry. The framework invokes these via the E-Document AI Processor during Copilot-assisted processing. + +## Key integration events + +Beyond interfaces, the framework publishes integration events for finer-grained customization. Major ones, organized by area: + +**Export/create** (in `EDocExport.Codeunit.al`): + +- `OnBeforeEDocumentCheck` / `OnAfterEDocumentCheck` -- override or extend document validation +- `OnBeforeCreateEDocument` / `OnAfterCreateEDocument` -- modify E-Document before/after creation + +**Send** (in `EDocIntegrationManagement.Codeunit.al`): + +- `OnBeforeSendDocument` / `OnAfterSendDocument` -- hook into send process +- `OnBeforeIsEDocumentInStateToSend` -- override send eligibility check + +**Import pipeline** (in `ImportEDocumentProcess.Codeunit.al`): + +- `OnADIProcessingCompleted` -- react to Azure Document Intelligence processing completion +- `OnFoundVendorNo` -- react to vendor resolution during draft preparation + +**Service configuration** (in `EDocumentService.Table.al`): + +- `OnAfterGetDefaultFileExtension` -- override the default file extension for the service + +**Subscribers** (in `EDocumentSubscribers.Codeunit.al`): + +- Events on draft page field validations for reacting to user edits + +**Import processing** (in `EDocImport.Codeunit.al`): + +- `OnAfterProcessIncomingEDocument` -- react after the import pipeline completes or advances a step + +## Processing customizations + +The `"E-Doc. Proc. Customizations"` enum on the service (field 61) provides a secondary customization axis. It is passed to `IProcessStructuredData.GetVendor()` as a parameter, allowing different vendor resolution strategies per service. diff --git a/src/Apps/W1/EDocument/App/docs/patterns.md b/src/Apps/W1/EDocument/App/docs/patterns.md new file mode 100644 index 0000000000..df5383f781 --- /dev/null +++ b/src/Apps/W1/EDocument/App/docs/patterns.md @@ -0,0 +1,194 @@ +# Patterns + +This document covers the recurring code patterns used in E-Document Core and, just as importantly, the legacy patterns you should avoid. For the full processing flows, see [business-logic.md](business-logic.md). For extension contracts, see [extensibility.md](extensibility.md). + +## Interface polymorphism via enum dispatch + +The most pervasive pattern in the codebase. Rather than using abstract codeunits or event-based dispatch, the framework uses AL's enum-implements-interface feature as a strategy + factory pattern. An extensible enum value binds to a codeunit that implements the interface. Configuration stores the enum value; runtime reads it and dispatches. + +This pattern appears in: + +- `"E-Document Format"` (`enum 6101`) implements `"E-Document"` interface -- format serialization +- `"Service Integration"` (`enum 6151`) implements `IDocumentSender`, `IDocumentReceiver`, `IConsentManager` -- service communication +- `"Integration Action Type"` (`enum 6170`) implements `IDocumentAction` -- extensible actions +- `"E-Document Service Status"` (`enum 6106`) implements `IEDocumentStatus` -- status-dependent behavior +- `"Structure Received E-Doc."` (`enum 6120`) implements `IStructureReceivedEDocument` -- structuring raw data +- `"E-Doc. Read into Draft"` (`enum 6113`) implements `IStructuredFormatReader` -- parsing structured data +- `"E-Doc. Process Draft"` (`enum 6112`) implements `IProcessStructuredData` -- draft preparation +- `"E-Document Type"` (`enum 6105`) implements `IEDocumentFinishDraft` -- document creation per type +- `"Export Eligibility Evaluator"` implements `IExportEligibilityEvaluator` -- export gating +- `"E-Doc. File Format"` implements `IEDocFileFormat`, `IBlobType` -- file type behavior + +The enum value is stored on the E-Document or Service table and read at the dispatch site. For example, in `ImportEDocumentProcess.ReadIntoDraft()`: + +```al +IStructuredFormatReader := EDocument."Read into Draft Impl."; +EDocument."Process Draft Impl." := IStructuredFormatReader.ReadIntoDraft(EDocument, FromBlob); +``` + +This is clean but has a consequence: the dispatch decision is locked to a single field value. You cannot chain implementations or compose behaviors without building that into the interface contract. + +## Error-trapping codeunit wrapper ("if codeunit.run") + +Every call to an external interface implementation is wrapped in a dedicated codeunit that runs inside a `if not Codeunit.Run()` pattern. The framework commits before the call, runs the wrapper codeunit, and catches runtime errors. This isolates interface failures from the calling transaction. + +Concrete wrappers include `EDocumentCreate.Codeunit.al` (for format `Create`), `SendRunner.Codeunit.al` (for `IDocumentSender.Send`), `ReceiveDocuments.Codeunit.al`, `DownloadDocument.Codeunit.al`, `MarkFetched.Codeunit.al`, and `EDocumentActionRunner.Codeunit.al`. + +The pattern in `EDocIntegrationManagement.RunSend()`: + +```al +Commit(); +SendRunner.SetDocumentAndService(EDocument, EDocumentService); +SendRunner.SetContext(SendContext); +if not SendRunner.Run() then + EDocumentErrorHelper.LogSimpleErrorMessage(EDocument, GetLastErrorText()); +EDocument.Get(EDocument."Entry No"); // re-read after interface call +``` + +The `Commit()` before the call is essential -- without it, a runtime error in the interface implementation would roll back the E-Document record itself. + +## Error accumulation (collecting parameter) + +Rather than failing on the first error, the framework counts errors before and after an operation to determine success: + +```al +ErrorCount := EDocumentErrorHelper.ErrorMessageCount(EDocument); +RunSend(EDocumentService, EDocument, SendContext, IsAsync); +Success := EDocumentErrorHelper.ErrorMessageCount(EDocument) = ErrorCount; +``` + +This pattern appears throughout `EDocExport`, `EDocIntegrationManagement`, and `ImportEDocumentProcess`. It allows interface implementations to log multiple error messages (via `EDocumentErrorHelper.LogSimpleErrorMessage`) and the framework to detect whether any new errors were added. + +## Context objects for integration calls + +Send and receive operations use context codeunits (`SendContext`, `ReceiveContext`, `ActionContext`) that bundle HTTP request/response, TempBlob, status, and metadata. This replaces the older pattern of passing `HttpRequestMessage` and `HttpResponseMessage` as separate parameters. + +The context objects provide a fluent API: `SendContext.Http().GetHttpRequestMessage()`, `SendContext.Status().SetStatus()`, `SendContext.GetTempBlob()`. After the interface call, the framework reads the HTTP objects from the context to log them in the integration log. + +## Manual event subscribers as stateful context carriers + +The AI tool codeunits in `Processing/AI/Tools/` use a non-standard pattern: `EventSubscriberInstance = Manual` combined with instance variables to carry context across an execution lifecycle. This differs from the typical BC event subscriber pattern where subscribers are stateless singletons. + +In this pattern, a codeunit declares `EventSubscriberInstance = Manual` and `TableNo = "E-Document Purchase Line"` (or similar). The orchestrator (`EDocAIToolProcessor`) binds the instance, then calls it with row-level context via `OnRun()`. Instance variables like `EDocumentNo: Integer` persist between method calls on the same instance, so the `Execute()` callback (invoked later by the AI function-calling loop) can access state that was set during initialization. + +```al +codeunit 6177 "E-Doc. Historical Matching" implements "AOAI Function", IEDocAISystem +{ + EventSubscriberInstance = Manual; + TableNo = "E-Document Purchase Line"; + + var + EDocumentNo: Integer; // context set in OnRun, used in Execute + + trigger OnRun() + begin + EDocumentNo := Rec."E-Document Entry No."; // capture context + EDocumentAIProcessor.Setup(this); // bind instance + EDocumentAIProcessor.Process(...); // AI loop calls Execute() + end; + + procedure Execute(Arguments: JsonObject): Variant + begin + // EDocumentNo is available here from the OnRun context + end; +} +``` + +This pattern appears in `EDocHistoricalMatching`, `EDocGLAccountMatching`, `EDocDeferralMatching`, and `EDocSimilarDescriptions`. The key advantage is that state flows through the instance rather than through event parameters, keeping the AI tool interface clean while preserving row-level context across asynchronous function calls. + +## Bidirectional state machine (import pipeline) + +The V2.0 import pipeline in `ImportEDocumentProcess.Codeunit.al` is a bidirectional state machine. Given a current status and a desired status, `GetEDocumentToDesiredStatus()` calculates the path: + +1. If going backward: undo steps from current down to desired + 1 +2. If going forward: run steps from current up to desired - 1 + +The `StatusStepIndex()` function maps each `"Import E-Doc. Proc. Status"` to a numeric index (0-4), and `GetNextStep()` / `GetPreviousStep()` map statuses to the `"Import E-Document Steps"` enum values that transition between them. + +This design makes it possible to revert a processed document back to any earlier stage. For example, if a user notices the vendor was resolved incorrectly, they can revert from "Draft Ready" to "Ready for draft", change vendor data, and re-run "Prepare draft". + +## Workflow as orchestration layer + +The framework delegates flow control to BC's Workflow engine rather than hardcoding the sequence of operations. The E-Document workflow setup (`EDocumentWorkFlowSetup.Codeunit.al`) registers workflow events and responses. The key events are "E-Document Created" and "E-Document Sent". Responses include "Send E-Document to Service", "Send E-Document via Email". + +This means the sequence of operations (export -> send -> email -> approval) is configured in the workflow, not in code. `EDocumentCreatedFlow.Codeunit.al` triggers the workflow after document creation, and each workflow step response delegates to the appropriate framework method. + +## Blob management (TempBlob and Data Storage) + +Blobs flow through the system in two forms: in-memory `TempBlob` codeunits during processing, and persisted `"E-Doc. Data Storage"` records for permanent storage. The conversion happens in `EDocumentLog.InsertLog()`, which writes the TempBlob to a new Data Storage record and links it via `"E-Doc. Data Storage Entry No."` on the log entry. + +Inbound documents can accumulate multiple blobs: the original unstructured content, the structured conversion, and document attachments. The E-Document tracks two primary references: `"Unstructured Data Entry No."` and `"Structured Data Entry No."`. + +## RecordRef-based generic processing + +Export operations work with RecordRef rather than specific table types, enabling a single code path for Sales, Purchase, Service, and other document types. `PopulateEDocument()` in `EDocExport.Codeunit.al` uses `SourceDocumentHeader.Number` to determine the table and then reads fields via `SourceDocumentHeader.Field(FieldNo).Value`. The mapping engine (`EDocMapping.Codeunit.al`) also operates on RecordRefs. + +The downside is that field access is by field number, making the code harder to follow and susceptible to breaking if field numbers change. + +## IEDocumentStatus: behavior per enum value + +The `"E-Document Service Status"` enum uniquely implements `IEDocumentStatus`. Each enum value declares which of three status codeunits handles it: + +- Values like `Created`, `Imported`, `Pending Response` use the `DefaultImplementation = "E-Doc In Progress Status"` (they represent in-progress states) +- Values like `Exported`, `Sent`, `Approved`, `Cleared` explicitly set `Implementation = IEDocumentStatus = "E-Doc Processed Status"` +- Error values like `Sending Error`, `Export Error` set `Implementation = IEDocumentStatus = "E-Doc Error Status"` + +This lets `EDocumentProcessing.ModifyEDocumentStatus()` call the service status enum value's interface to determine the overall E-Document status without a giant case statement. + +--- + +## Legacy patterns + +These patterns exist in the codebase but are deprecated. Understanding them helps when reading code inside `#if not CLEAN26` or `#if not CLEAN27` blocks. + +### V1.0 import process + +**What**: The original single-stage import where the format interface's `GetBasicInfoFromReceivedDocument()` and `GetCompleteInfoFromReceivedDocument()` methods directly create BC purchase documents in one shot. + +**Where**: `EDocImport.V1_ProcessEDocument()`, `EDocImport.V1_BeforeInsertImportedEdocument()`, and the `"E-Document"` interface methods `GetBasicInfoFromReceivedDocument` / `GetCompleteInfoFromReceivedDocument`. + +**Why deprecated**: No staging tables, no user review, no reversibility. Format implementations had to know how to create Purchase Invoices, violating separation of concerns. + +**What to do instead**: Use V2.0 pipeline with `"Import Process" = "Version 2.0"`. Implement `IStructuredFormatReader` to populate staging tables and let the framework handle BC document creation. + +### Old "E-Document Integration" enum and interface + +**What**: The original `"E-Document Integration"` enum (`enum 6132`) and its `"E-Document Integration"` interface, which combined send, receive, and action methods into a single interface. Methods took raw `HttpRequestMessage`/`HttpResponseMessage` parameters. + +**Where**: `EDocumentIntegration.Interface.al`, `EDocumentIntegration.Enum.al`, and the `"Service Integration"` field (field 4) on `"E-Document Service"`. + +**Why deprecated**: Too coarse-grained (one interface for all operations), raw HTTP parameters instead of context objects, no support for the V2.0 receive flow. + +**What to do instead**: Extend `"Service Integration"` (`enum 6151`) and implement the granular interfaces: `IDocumentSender`, `IDocumentReceiver`, `IDocumentResponseHandler`, `ISentDocumentActions`, `IReceivedDocumentMarker`. + +### GetDocumentCountInBatch() + +**What**: V1.0 receive flow called `EDocIntegration.GetDocumentCountInBatch()` to determine how many documents were in a received blob, then called `ReceiveDocument()` to get the blob. + +**Where**: `EDocIntegrationManagement.ReceiveDocument()` (inside `#if not CLEAN26`). + +**Why deprecated**: Awkward two-step receive pattern that bundled all documents in a single blob. V2.0 uses `ReceiveDocuments()` which returns a `"Temp Blob List"` with one entry per document, and `DownloadDocument()` fetches each individually. + +### Manual GetIntegrationSetup() + +**What**: Connector apps had to implement a method that returned a setup page ID, called by the service card to open integration-specific settings. + +**Why deprecated**: Replaced by the `OnBeforeOpenServiceIntegrationSetupPage` event pattern, which is more flexible and doesn't require interface changes. + +### IPurchaseLineAccountProvider + +**What**: Earlier interface for determining purchase line account type and number during import. + +**Where**: `Processing/Interfaces/IPurchaseLineAccountProvider.Interface.al` (marked `ObsoleteState = Pending`, tag `27.0`). + +**Why deprecated**: Too narrow -- only sets account type and number. Replaced by `IPurchaseLineProvider`, which operates on the full `"E-Document Purchase Line"` record and can set any field. + +### CLEANSCHEMA and CLEAN version markers + +The codebase uses compiler directives to manage deprecation: + +- `#if not CLEAN26` -- code to be removed when BC version 26 cleanup happens +- `#if not CLEAN27` -- code to be removed when BC version 27 cleanup happens +- `#if not CLEANSCHEMA26` / `#if not CLEANSCHEMA29` -- table schema changes (field removals) that need separate cleanup due to schema migration constraints + +When reading the code, content inside these blocks is legacy. The code outside (or in the `#else` branch) is the current implementation. diff --git a/src/Apps/W1/EDocument/App/src/ClearanceModel/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/ClearanceModel/docs/CLAUDE.md new file mode 100644 index 0000000000..8d117b47f8 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/ClearanceModel/docs/CLAUDE.md @@ -0,0 +1,19 @@ +# Clearance model + +QR code storage and display for tax authority clearance scenarios. In countries with clearance models (e.g., Spain, India, Saudi Arabia), invoices must be submitted to a tax authority that stamps them with a QR code or similar token. This module provides the infrastructure to store those QR codes on posted documents and display them to users. It does not perform the clearance itself -- that is handled by country-specific connectors that write QR data back to these fields. + +## How it works + +The module adds two fields to each of four posted document tables (Sales Invoice Header, Sales Cr.Memo Header, Service Invoice Header, Service Cr.Memo Header) via table extensions: `"QR Code Image"` (MediaSet for display) and `"QR Code Base64"` (Blob for raw data). Matching page extensions add a "View QR Code" action to each posted document page, and report extensions include the QR image in printed documents. + +`EDocumentQRCodeManagement` is the central codeunit. `InitializeAndRunQRCodeViewer` takes a RecordRef, extracts the QR Code Base64 from the appropriate table, copies it to a temporary `EDocQRBuffer` record, and opens the `E-Document QR Viewer` page as modal. The buffer table exists because the viewer needs a temporary, table-agnostic record to display. `ExportQRCodeToFile` decodes the base64 to a PNG and triggers a file download. `SetQRCodeImageFromBase64` converts the stored base64 into a MediaSet for inline display on document pages. + +## Things to know + +- The QR data is stored as base64 text in a Blob field, not as a MediaSet directly. `SetQRCodeImageFromBase64` must be called to populate the MediaSet field for inline rendering. This two-field approach allows both raw export and rendered display. +- The module is purely passive storage -- connectors write the QR data after clearance, and this module reads it. There are no events or triggers that fire during the clearance process. +- All four document types (Sales Invoice, Sales Credit Memo, Service Invoice, Service Credit Memo) follow an identical pattern -- the table/page/report extensions are near-copies of each other. +- The `EDocQRBuffer` table is declared as `TableType = Temporary` and `Access = Internal` -- it never persists data. It exists solely as a transport between the management codeunit and the viewer page. +- If no QR data exists for a document, the viewer shows a user-friendly message ("No QR Base64 content available for...") and exits without opening the page. + +See the [app-level CLAUDE.md](../../docs/CLAUDE.md) for broader architecture context. diff --git a/src/Apps/W1/EDocument/App/src/DataExchange/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/DataExchange/docs/CLAUDE.md new file mode 100644 index 0000000000..10fb4c46e1 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/DataExchange/docs/CLAUDE.md @@ -0,0 +1,23 @@ +# Data exchange + +An alternative to the interface-based Format module -- this implements the `"E-Document"` interface by delegating to BC's built-in Data Exchange Definition framework for field-by-field XML mapping. Rather than writing custom XML generation code, you configure Data Exchange Definitions and column mappings in the UI. The `PEPPOL Data Exchange Definition/` subfolder provides the pre-mapping codeunits that prepare data before Data Exchange processes it. + +## How it works + +`EDocDataExchangeImpl` implements the same `"E-Document"` interface as the Format module's PEPPOL codeunit, but its `Create` method works differently. It looks up the `E-Doc. Service Data Exch. Def.` table to find the export Data Exchange Definition for the document type, creates a `Data Exch.` record with line filters, and calls `DataExch.ExportFromDataExch` to run the configured mapping. The resulting XML blob is extracted from the Data Exch record's field 3. For import, `GetCompleteInfoFromReceivedDocument` uses the import Data Exchange Definition to parse incoming XML into `Intermediate Data Import` records, which are then processed by `EDocDEDPEPPOLPreMapping`. + +The `E-Doc. Service Data Exch. Def.` table links an E-Document Service code and document type to both an import and export Data Exchange Definition code, displayed via the `E-Doc. Service Data Exch. Sub` subpage on the service card. + +**Pre-mapping codeunits** run before Data Exchange processing to transform raw PEPPOL data into BC-compatible values. `EDocDEDPEPPOLPreMapping` is the main import pre-mapper -- it validates currencies, resolves buy-from/pay-to vendors, finds related invoices for credit memos, processes line items, and applies invoice charges. The `PreMapSalesInvLine`, `PreMapSalesCrMemoLine`, `PreMapServiceInvLine`, and `PreMapServiceCrMemoLine` codeunits filter out rounding lines before export to avoid PEPPOL schema violations. + +`EDocDEDPEPPOLSubscribers` is a `SingleInstance` codeunit that manages state across the Data Exchange export process. It subscribes to events on `EDocDataExchangeImpl` and `Export Generic XML`, injecting UBL namespace declarations and tracking loop counters for tax subtotals and allowance charges. + +## Things to know + +- `EDocDEDPEPPOLExternal` is a dummy codeunit with an empty `OnRun` -- it exists solely to be referenced as the "External Data Handling Codeunit" in Data Exchange Definitions, satisfying a BC framework requirement. +- The Data Exchange approach is more configurable but less flexible than the XMLport-based Format approach. Localizations that need complex XML structures often use the Format interface directly. +- The pre-mapping import path validates that referenced purchase invoices are posted before allowing credit memo creation (`YouMustFirstPostTheRelatedInvoiceErr`). +- `EDocDEDPEPPOLSubscribers` uses `SingleInstance` because the Data Exchange framework processes records one at a time through event subscribers, and state (loop counters, VAT amounts) must persist across those calls. +- On export, the `OnAfterDataExchangeInsert` and `OnBeforeDataExchangeExport` integration events let subscribers customize behavior per document type. + +See the [app-level CLAUDE.md](../../docs/CLAUDE.md) for broader architecture context. diff --git a/src/Apps/W1/EDocument/App/src/Document/EDocument.Page.al b/src/Apps/W1/EDocument/App/src/Document/EDocument.Page.al index 2de7f2a557..3e2f8ea79b 100644 --- a/src/Apps/W1/EDocument/App/src/Document/EDocument.Page.al +++ b/src/Apps/W1/EDocument/App/src/Document/EDocument.Page.al @@ -342,12 +342,12 @@ page 6121 "E-Document" trigger OnAction() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; begin - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImportParameters."Purch. Journal V1 Behavior" := EDocImportParameters."Purch. Journal V1 Behavior"::"Create purchase document"; - EDocImportParameters."Create Document V1 Behavior" := true; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + TempEDocImportParameters."Purch. Journal V1 Behavior" := TempEDocImportParameters."Purch. Journal V1 Behavior"::"Create purchase document"; + TempEDocImportParameters."Create Document V1 Behavior" := true; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); if EDocumentErrorHelper.HasErrors(Rec) then Message(DocNotCreatedMsg, Rec."Document Type"); end; @@ -361,12 +361,12 @@ page 6121 "E-Document" trigger OnAction() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; begin - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImportParameters."Purch. Journal V1 Behavior" := EDocImportParameters."Purch. Journal V1 Behavior"::"Create journal line"; - EDocImportParameters."Create Document V1 Behavior" := true; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + TempEDocImportParameters."Purch. Journal V1 Behavior" := TempEDocImportParameters."Purch. Journal V1 Behavior"::"Create journal line"; + TempEDocImportParameters."Create Document V1 Behavior" := true; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); if EDocumentErrorHelper.HasErrors(Rec) then Message(DocNotCreatedMsg, Rec."Document Type"); end; diff --git a/src/Apps/W1/EDocument/App/src/Document/EDocumentType.Enum.al b/src/Apps/W1/EDocument/App/src/Document/EDocumentType.Enum.al index 08b282174e..72a79fb517 100644 --- a/src/Apps/W1/EDocument/App/src/Document/EDocumentType.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Document/EDocumentType.Enum.al @@ -56,6 +56,7 @@ enum 6121 "E-Document Type" implements IEDocumentFinishDraft value(10; "Purchase Credit Memo") { Caption = 'Purchase Credit Memo'; + Implementation = IEDocumentFinishDraft = "E-Doc. Create Purch. Cr. Memo"; } value(11; "Service Order") { diff --git a/src/Apps/W1/EDocument/App/src/Document/Inbound/InboundEDocuments.Page.al b/src/Apps/W1/EDocument/App/src/Document/Inbound/InboundEDocuments.Page.al index c870f71fe0..6f5a4f6539 100644 --- a/src/Apps/W1/EDocument/App/src/Document/Inbound/InboundEDocuments.Page.al +++ b/src/Apps/W1/EDocument/App/src/Document/Inbound/InboundEDocuments.Page.al @@ -265,11 +265,11 @@ page 6105 "Inbound E-Documents" trigger OnAction() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; begin - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Read into Draft"; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Read into Draft"; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); end; } action(PrepareDraftDocument) @@ -282,13 +282,13 @@ page 6105 "Inbound E-Documents" trigger OnAction() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; ImportEDocumentProcess: Codeunit "Import E-Document Process"; begin - EDocImportParameters := Rec.GetEDocumentService().GetDefaultImportParameters(); - EDocImportParameters."Desired E-Document Status" := EDocImportParameters."Desired E-Document Status"::"Draft Ready"; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); + TempEDocImportParameters := Rec.GetEDocumentService().GetDefaultImportParameters(); + TempEDocImportParameters."Desired E-Document Status" := TempEDocImportParameters."Desired E-Document Status"::"Draft Ready"; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); if ImportEDocumentProcess.IsEDocumentInStateGE(Rec, Enum::"Import E-Doc. Proc. Status"::"Ready for draft") then EDocumentHelper.OpenDraftPage(Rec) end; @@ -303,15 +303,15 @@ page 6105 "Inbound E-Documents" trigger OnAction() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; ImportEDocumentProcess: Codeunit "Import E-Document Process"; begin if ImportEDocumentProcess.IsEDocumentInStateGE(Rec, Enum::"Import E-Doc. Proc. Status"::"Ready for draft") then EDocumentHelper.OpenDraftPage(Rec) else begin - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); end; end; } diff --git a/src/Apps/W1/EDocument/App/src/Document/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Document/docs/CLAUDE.md new file mode 100644 index 0000000000..1aaa6db1d0 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Document/docs/CLAUDE.md @@ -0,0 +1,27 @@ +# Document + +The E-Document table (table 6121 in `EDocument.Table.al`) is the aggregate root of the entire framework. Every electronic document -- whether an outbound sales invoice or an inbound purchase credit memo -- lives as a single row here. This module also owns the status model, direction semantics, document type taxonomy, and user notification infrastructure. + +## How it works + +When a BC document is posted or an external document arrives from a service endpoint, the framework creates an E-Document record via the `Create` procedure, stamping it with a direction (Incoming/Outgoing from `EDocumentDirection.Enum.al`), a document type from the extensive `E-Document Type` enum (22 values covering sales, purchase, service, finance charge, reminders, journals, and shipments), and the originating service code. + +The E-Document carries three independent status dimensions that evolve separately. The top-level `Status` field (enum 6108: In Progress, Processed, Error) is derived automatically from the per-service `E-Document Service Status` via the strategy pattern -- each service status value implements `IEDocumentStatus` (defined in `Interfaces/IEDocumentStatus.Interface.al`), and the three codeunits in `Status/` (`EDocErrorStatus`, `EDocInProgressStatus`, `EDocProcessedStatus`) return the corresponding top-level status. Most service statuses default to "In Progress" unless explicitly mapped to Error or Processed in the enum implementation declarations. The third dimension, `Import Processing Status`, is a FlowField that reads from the `E-Document Service Status` table, tracking inbound documents through a five-step pipeline: Unprocessed, Readable, Ready for draft, Draft Ready, Processed. + +The `E-Document` interface (`Interfaces/EDocument.Interface.al`) defines the format contract that document format implementations must satisfy -- `Check`, `Create`, `CreateBatch` for outbound, and `GetBasicInfoFromReceivedDocument` / `GetCompleteInfoFromReceivedDocument` for inbound. + +## Things to know + +- Duplicate detection uses `IsDuplicate()` which checks the composite `(Incoming E-Document No., Bill-to/Pay-to No., Document Date)` with `ReadIsolation::ReadUncommitted` -- this means it can see uncommitted records from other sessions, avoiding race conditions during batch imports. + +- Deletion is heavily guarded: you cannot delete a Processed document or one linked to a source document (`Document Record ID`). Non-duplicate documents require explicit user confirmation, and non-GUI contexts block outright. + +- The `CleanupDocument` procedure cascades deletes to logs, integration logs, service statuses, mapping logs, imported lines, and document attachments. It also invokes `IProcessStructuredData.CleanUpDraft` for version 2 processing cleanup. + +- The `E-Documents Setup` table (table 6107) is marked `ObsoleteState = Pending` with tag '28.0'. It controls the "new E-Document experience" feature gate, which is activated per-tenant via AAD tenant ID allowlist, environment setting, or country code list. + +- The `E-Document` interface's `CreateBatch` method receives a record set of E-Documents rather than a single record -- format implementations must handle multi-document serialization into a single blob. + +- Fields 42-44 (`Structure Data Impl.`, `Read into Draft Impl.`, `Process Draft Impl.`) are enum-based strategy selectors for the import processing pipeline, allowing different implementations per document. + +- Notification infrastructure (`Notification/`) currently handles a single scenario -- alerting users when an inbound vendor was matched by name but not address. Notifications are per-user, dismissable, and backed by the `My Notifications` framework. diff --git a/src/Apps/W1/EDocument/App/src/Document/docs/data-model.md b/src/Apps/W1/EDocument/App/src/Document/docs/data-model.md new file mode 100644 index 0000000000..c765affc7f --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Document/docs/data-model.md @@ -0,0 +1,44 @@ +# Document data model + +This describes the data model for the E-Document aggregate root and its immediate relationships. For the full cross-module data model, see [../../docs/data-model.md](../../docs/data-model.md). + +## Core entity and status tracking + +The `E-Document` table (6121) is the central record. Each E-Document points to its originating BC document via `Document Record ID` (a RecordId field) and to its content via `Structured Data Entry No.` and `Unstructured Data Entry No.`, both foreign keys to `E-Doc. Data Storage`. The `E-Document Service Status` table (6138) tracks per-service processing state for each document, creating a one-to-many relationship between documents and services. + +```mermaid +erDiagram + E-Document ||--o{ E-Document-Service-Status : "has per-service status" + E-Document ||--o| E-Doc-Data-Storage-Structured : "structured content" + E-Document ||--o| E-Doc-Data-Storage-Unstructured : "unstructured content" + E-Document-Service-Status }o--|| E-Document-Service : "belongs to service" +``` + +The `E-Document Service Status` table uses a composite primary key of `(E-Document Entry No, E-Document Service Code)`. Its `Import Processing Status` field has a validate trigger that automatically synchronizes the `Status` field -- when import processing reaches Processed, the service status flips to "Imported Document Created"; otherwise it stays at "Imported". This coupling means you cannot set import processing status without side-effecting the service status. + +## Three status dimensions + +The status model is the most important design decision in this module. Rather than a single linear state machine, the framework uses three orthogonal dimensions. + +**E-Document Status** (enum 6108) is the top-level rollup with only three values: In Progress, Processed, Error. It is never set directly -- it is derived from the service status via the `IEDocumentStatus` interface. Each `E-Document Service Status` enum value declares which `IEDocumentStatus` implementation it uses. For example, "Exported", "Sent", "Canceled", "Approved", "Rejected", "Cleared", and "Imported Document Created" all map to Processed; "Sending Error", "Cancel Error", "Export Error", "Imported Document Processing Error", and "Approval Error" map to Error; everything else defaults to In Progress. + +**E-Document Service Status** (enum 6106) is the fine-grained operational status with 20+ values spanning the full lifecycle: Created, Exported, Sent, Imported, Canceled, Pending Batch, Pending Response, Order Linked, Cleared, and various error states. The clearance model values (30-31: Not Cleared, Cleared) are reserved in a separate range for tax authority clearance workflows. + +**Import Processing Status** (enum 6100) is a five-step inbound pipeline: Unprocessed, Readable, Ready for draft, Draft Ready, Processed. Each step corresponds to a processing action (structure received data, read into intermediate representation, prepare draft, finish draft). This enum is not extensible. + +## Notification model + +```mermaid +erDiagram + E-Document ||--o{ E-Document-Notification : "has notifications" +``` + +The `E-Document Notification` table (6126) uses a composite key of `(E-Document Entry No., ID, User Id)` where ID is a well-known Guid identifying the notification type. This design allows multiple notification types per document per user. Currently only one type exists ("Vendor Matched By Name Not Address"), but the structure supports adding more notification scenarios without schema changes. Notifications integrate with BC's `My Notifications` framework for user-level opt-out. + +## Design decisions and gotchas + +- The `Document Record ID` field stores a RecordId, which is a BC-specific composite reference encoding table number and primary key. This means the E-Document can point to any source document table without a fixed foreign key, but it also means the reference breaks if the source record is renumbered or the table ID changes. + +- Key3 on the E-Document table `(Incoming E-Document No., Bill-to/Pay-to No., Document Date, Entry No)` exists specifically for the `IsDuplicate()` check. The inclusion of `Entry No` in the key allows efficient exclusion of the current record during the duplicate scan. + +- The `E-Documents Setup` table is obsolete (pending removal in v28). Its feature gating logic checks three sources in priority order: explicit table flag, AAD tenant ID allowlist, environment setting, then country code list. The country list is hardcoded to 14 specific localizations plus W1. diff --git a/src/Apps/W1/EDocument/App/src/Extensions/EDocCompanyInformation.PageExt.al b/src/Apps/W1/EDocument/App/src/Extensions/EDocCompanyInformation.PageExt.al index 897c2f6f22..d763495079 100644 --- a/src/Apps/W1/EDocument/App/src/Extensions/EDocCompanyInformation.PageExt.al +++ b/src/Apps/W1/EDocument/App/src/Extensions/EDocCompanyInformation.PageExt.al @@ -18,19 +18,24 @@ pageextension 6165 "E-Doc. Company Information" extends "Company Information" { addafter(GLN) { - field("E-Document Service Participation Ids"; ParticipantIdCount) + group(ElectronicDocumentServiceGroup) { - ApplicationArea = All; - Caption = 'E-Document Service Participation'; - DrillDown = true; - Editable = false; - ToolTip = 'Specifies the company participation for the E-Document services.'; + ShowCaption = false; Visible = EDocumentServiceExists; - trigger OnDrillDown() - begin - ServiceParticipant.RunServiceParticipantPage(Enum::"E-Document Source Type"::Company, ''); - end; + field("E-Document Service Participation Ids"; ParticipantIdCount) + { + ApplicationArea = All; + Caption = 'E-Document Service Participation'; + DrillDown = true; + Editable = false; + ToolTip = 'Specifies the company participation for the E-Document services.'; + + trigger OnDrillDown() + begin + ServiceParticipant.RunServiceParticipantPage(Enum::"E-Document Source Type"::Company, ''); + end; + } } } } diff --git a/src/Apps/W1/EDocument/App/src/Extensions/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Extensions/docs/CLAUDE.md new file mode 100644 index 0000000000..cb6fee5cc1 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Extensions/docs/CLAUDE.md @@ -0,0 +1,25 @@ +# Extensions + +Base app integration surface -- table extensions, page extensions, and enum extensions that embed E-Document capabilities into existing BC entities. This module does not contain business logic; it adds fields, actions, and factboxes so that E-Documents are visible and actionable from the pages users already work in. + +## How it works + +The module operates at three levels: + +**Document Sending Profile integration** (`Sending/` subfolder): The `EDocSendProfileElecDoc` enum extension adds the `"Extended E-Document Service Flow"` option to the Electronic Document field. The `EDocumentSendingProfile` table extension adds `"Electronic Service Flow"` (a workflow code reference). The `EDocSendingProfAttType` enum extension adds `"E-Document"` and `"PDF & E-Document"` as email attachment types. Together, these connect BC's existing document sending infrastructure to the E-Document workflow engine. + +**Purchase-side fields**: `EDocPurchaseHeader` adds `"E-Document Link"` (a Guid matching the E-Document's SystemId for linking incoming documents to purchase orders) and `"Amount Incl. VAT To Inv."` (a FlowField summing line amounts for partial invoicing). `EDocPurchaseLine` adds matching `"Amount Incl. VAT To Inv."` with rounding logic and an `HasEDocMatch` helper for order matching. The `EDocPurchPayablesSetup` table extension adds `"E-Document Matching Difference"` (tolerance percentage) and a Copilot learning flag. + +**Vendor/Location/Attachment fields**: Vendor and Vendor Template get `"Receive E-Document To"` (controls whether incoming e-docs create Purchase Orders or Purchase Invoices, defaulting to Purchase Order). Location gets `"Transfer Doc. Sending Profile"` for transfer shipment routing. Document Attachment gets `"E-Document Attachment"` and `"E-Document Entry No."` to link attachments back to their source E-Document. + +**Page extensions** add E-Document action groups (Open/Create) and factboxes to posted sales invoices, credit memos, service documents, purchase documents, and shipments. Role center extensions (`RoleCenter/` subfolder) add E-Document activities/cues to Accountant, Business Manager, Inventory Manager, and other standard role centers. + +## Things to know + +- The `"E-Document Link"` Guid on Purchase Header is indexed (secondary key) for fast lookup during incoming document matching. It stores the E-Document's `SystemId`, not its `"Entry No"`. +- The `"Receive E-Document To"` field on Vendor only allows `"Purchase Order"` or `"Purchase Invoice"` -- it uses `ValuesAllowed` to restrict the enum. This determines the default document type created when an incoming e-document is received from that vendor. +- Page extensions follow a consistent pattern: an "E-Document" action group with "Open" (enabled when an E-Document exists for the record) and "Create" (enabled when none exists). The `EDocumentExists` boolean is computed on page load. +- The `"Electronic Service Flow"` on Document Sending Profile has a table relation filtered to `Category = 'EDOC'` and `Template = false`, ensuring only enabled E-Document workflows can be selected. +- `EDocOrderMapActivities` is a standalone page (not an extension) providing the order mapping activities cue for role centers. + +See the [app-level CLAUDE.md](../../docs/CLAUDE.md) for broader architecture context. diff --git a/src/Apps/W1/EDocument/App/src/Format/EDocImportPEPPOLBIS30.Codeunit.al b/src/Apps/W1/EDocument/App/src/Format/EDocImportPEPPOLBIS30.Codeunit.al index 804516ec5f..a12f24e3bf 100644 --- a/src/Apps/W1/EDocument/App/src/Format/EDocImportPEPPOLBIS30.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Format/EDocImportPEPPOLBIS30.Codeunit.al @@ -14,9 +14,10 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" procedure ParseBasicInfo(var EDocument: Record "E-Document"; var TempBlob: Codeunit "Temp Blob") var - TempXMLBuffer: Record "XML Buffer" temporary; GLSetup: Record "General Ledger Setup"; + TempXMLBuffer: Record "XML Buffer" temporary; DocStream: InStream; + RootPath: Text; begin TempXMLBuffer.DeleteAll(); TempBlob.CreateInStream(DocStream, TextEncoding::UTF8); @@ -27,11 +28,11 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" EDocument.Direction := EDocument.Direction::Incoming; - case UpperCase(GetDocumentType(TempXMLBuffer)) of - 'INVOICE': - ParseInvoiceBasicInfo(EDocument, TempXMLBuffer); - 'CREDITNOTE': - ParseCreditMemoBasicInfo(EDocument, TempXMLBuffer); + case UpperCase(GetDocumentType(TempXMLBuffer, RootPath)) of + InvoiceTok: + ParseInvoiceBasicInfo(EDocument, TempXMLBuffer, RootPath); + CreditNoteTok: + ParseCreditMemoBasicInfo(EDocument, TempXMLBuffer, RootPath); end; end; @@ -39,6 +40,7 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" var TempXMLBuffer: Record "XML Buffer" temporary; DocStream: InStream; + RootPath: Text; begin TempXMLBuffer.DeleteAll(); TempBlob.CreateInStream(DocStream, TextEncoding::UTF8); @@ -47,88 +49,88 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" PurchaseHeader."Buy-from Vendor No." := EDocument."Bill-to/Pay-to No."; PurchaseHeader."Currency Code" := EDocument."Currency Code"; - case UpperCase(GetDocumentType(TempXMLBuffer)) of - 'INVOICE': - CreateInvoice(EDocument, PurchaseHeader, PurchaseLine, TempXMLBuffer); - 'CREDITNOTE': - CreateCreditMemo(EDocument, PurchaseHeader, PurchaseLine, TempXMLBuffer); + case UpperCase(GetDocumentType(TempXMLBuffer, RootPath)) of + InvoiceTok: + CreateInvoice(EDocument, PurchaseHeader, PurchaseLine, TempXMLBuffer, RootPath); + CreditNoteTok: + CreateCreditMemo(EDocument, PurchaseHeader, PurchaseLine, TempXMLBuffer, RootPath); end; end; - local procedure ParseInvoiceBasicInfo(var EDocument: Record "E-Document"; var TempXMLBuffer: Record "XML Buffer" temporary) + local procedure ParseInvoiceBasicInfo(var EDocument: Record "E-Document"; var TempXMLBuffer: Record "XML Buffer" temporary; RootPath: Text) var DueDate, IssueDate : Text; Currency: Text[10]; begin EDocument."Document Type" := EDocument."Document Type"::"Purchase Invoice"; - EDocument."Incoming E-Document No." := CopyStr(GetNodeByPath(TempXMLBuffer, '/Invoice/cbc:ID'), 1, MaxStrLen(EDocument."Document No.")); - ParseAccountingSupplierParty(EDocument, TempXMLBuffer, 'Invoice'); - ParseAccountingCustomerParty(EDocument, TempXMLBuffer, 'Invoice'); + EDocument."Incoming E-Document No." := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:ID'), 1, MaxStrLen(EDocument."Document No.")); + ParseAccountingSupplierParty(EDocument, TempXMLBuffer, RootPath); + ParseAccountingCustomerParty(EDocument, TempXMLBuffer, RootPath); - DueDate := GetNodeByPath(TempXMLBuffer, '/Invoice/cbc:DueDate'); + DueDate := GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:DueDate'); if DueDate <> '' then Evaluate(EDocument."Due Date", DueDate, 9); - IssueDate := GetNodeByPath(TempXMLBuffer, '/Invoice/cbc:IssueDate'); + IssueDate := GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:IssueDate'); if IssueDate <> '' then Evaluate(EDocument."Document Date", IssueDate, 9); - Evaluate(EDocument."Amount Excl. VAT", GetNodeByPath(TempXMLBuffer, '/Invoice/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount'), 9); - Evaluate(EDocument."Amount Incl. VAT", GetNodeByPath(TempXMLBuffer, '/Invoice/cac:LegalMonetaryTotal/cbc:PayableAmount'), 9); + Evaluate(EDocument."Amount Excl. VAT", GetNodeByPath(TempXMLBuffer, RootPath + '/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount'), 9); + Evaluate(EDocument."Amount Incl. VAT", GetNodeByPath(TempXMLBuffer, RootPath + '/cac:LegalMonetaryTotal/cbc:PayableAmount'), 9); - EDocument."Order No." := CopyStr(GetNodeByPath(TempXMLBuffer, '/Invoice/cac:OrderReference/cbc:ID'), 1, MaxStrLen(EDocument."Order No.")); + EDocument."Order No." := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cac:OrderReference/cbc:ID'), 1, MaxStrLen(EDocument."Order No.")); - Currency := CopyStr(GetNodeByPath(TempXMLBuffer, '/Invoice/cbc:DocumentCurrencyCode'), 1, MaxStrLen(EDocument."Currency Code")); + Currency := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:DocumentCurrencyCode'), 1, MaxStrLen(EDocument."Currency Code")); if LCYCode <> Currency then EDocument."Currency Code" := Currency; end; - local procedure ParseCreditMemoBasicInfo(var EDocument: Record "E-Document"; var TempXMLBuffer: Record "XML Buffer" temporary) + local procedure ParseCreditMemoBasicInfo(var EDocument: Record "E-Document"; var TempXMLBuffer: Record "XML Buffer" temporary; RootPath: Text) var DueDate, IssueDate : Text; Currency: Text[10]; begin EDocument."Document Type" := EDocument."Document Type"::"Purchase Credit Memo"; - EDocument."Incoming E-Document No." := CopyStr(GetNodeByPath(TempXMLBuffer, '/CreditNote/cbc:ID'), 1, MaxStrLen(EDocument."Document No.")); - ParseAccountingSupplierParty(EDocument, TempXMLBuffer, 'CreditNote'); - ParseAccountingCustomerParty(EDocument, TempXMLBuffer, 'CreditNote'); + EDocument."Incoming E-Document No." := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:ID'), 1, MaxStrLen(EDocument."Document No.")); + ParseAccountingSupplierParty(EDocument, TempXMLBuffer, RootPath); + ParseAccountingCustomerParty(EDocument, TempXMLBuffer, RootPath); - DueDate := GetNodeByPath(TempXMLBuffer, '/CreditNote/cac:PaymentMeans/cbc:PaymentDueDate'); + DueDate := GetNodeByPath(TempXMLBuffer, RootPath + '/cac:PaymentMeans/cbc:PaymentDueDate'); if DueDate <> '' then Evaluate(EDocument."Due Date", DueDate, 9); - IssueDate := GetNodeByPath(TempXMLBuffer, '/CreditNote/cbc:IssueDate'); + IssueDate := GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:IssueDate'); if IssueDate <> '' then Evaluate(EDocument."Document Date", IssueDate, 9); - Evaluate(EDocument."Amount Excl. VAT", GetNodeByPath(TempXMLBuffer, '/CreditNote/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount'), 9); - Evaluate(EDocument."Amount Incl. VAT", GetNodeByPath(TempXMLBuffer, '/CreditNote/cac:LegalMonetaryTotal/cbc:PayableAmount'), 9); + Evaluate(EDocument."Amount Excl. VAT", GetNodeByPath(TempXMLBuffer, RootPath + '/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount'), 9); + Evaluate(EDocument."Amount Incl. VAT", GetNodeByPath(TempXMLBuffer, RootPath + '/cac:LegalMonetaryTotal/cbc:PayableAmount'), 9); - Currency := CopyStr(GetNodeByPath(TempXMLBuffer, '/CreditNote/cbc:DocumentCurrencyCode'), 1, MaxStrLen(EDocument."Currency Code")); + Currency := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:DocumentCurrencyCode'), 1, MaxStrLen(EDocument."Currency Code")); if LCYCode <> Currency then EDocument."Currency Code" := Currency; end; - local procedure ParseAccountingSupplierParty(var EDocument: Record "E-Document"; var TempXMLBuffer: Record "XML Buffer" temporary; DocumentType: Text) + local procedure ParseAccountingSupplierParty(var EDocument: Record "E-Document"; var TempXMLBuffer: Record "XML Buffer" temporary; RootPath: Text) var - Vendor: Record Vendor; EDocumentService: Record "E-Document Service"; + Vendor: Record Vendor; EDocumentHelper: Codeunit "E-Document Helper"; - VendorName, VendorAddress : Text; - VATRegistrationNo: Text[20]; - VendorID: Text; - VendorNo: Code[20]; GLN: Code[13]; + VendorNo: Code[20]; + VendorAddress, VendorName : Text; + VendorID: Text; + VATRegistrationNo: Text[20]; begin // Read GLN or VAT Registration No based on the scheme ID. - if GetNodeAttributeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID') = GLNSchemeId() then - GLN := CopyStr(GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), 1, MaxStrLen(GLN)); + if GetNodeAttributeByPath(TempXMLBuffer, RootPath + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID') = GLNSchemeId() then + GLN := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'), 1, MaxStrLen(GLN)); - VATRegistrationNo := CopyStr(GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID'), 1, MaxStrLen(VATRegistrationNo)); + VATRegistrationNo := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID'), 1, MaxStrLen(VATRegistrationNo)); VendorNo := EDocumentImportHelper.FindVendor('', GLN, VATRegistrationNo); // If vendor not found, try to find by Service Participant. if VendorNo = '' then begin - VendorID := GetNodeAttributeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID') + ':'; - VendorID += this.GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'); + VendorID := GetNodeAttributeByPath(TempXMLBuffer, RootPath + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID') + ':'; + VendorID += this.GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID'); EDocumentHelper.GetEdocumentService(EDocument, EDocumentService); VendorNo := EDocumentImportHelper.FindVendorByServiceParticipant(CopyStr(VendorID, 1, 200), EDocumentService.Code); @@ -136,8 +138,8 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" // If vendor not found, try to find by name and address. if VendorNo = '' then begin - VendorName := GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name'); - VendorAddress := GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName'); + VendorName := GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name'); + VendorAddress := GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName'); VendorNo := EDocumentImportHelper.FindVendorByNameAndAddress(VendorName, VendorAddress); EDocument."Bill-to/Pay-to Name" := CopyStr(VendorName, 1, MaxStrLen(EDocument."Bill-to/Pay-to Name")); end; @@ -149,10 +151,10 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" end; end; - local procedure ParseAccountingCustomerParty(var EDocument: Record "E-Document"; var TempXMLBuffer: Record "XML Buffer" temporary; DocumentType: Text) + local procedure ParseAccountingCustomerParty(var EDocument: Record "E-Document"; var TempXMLBuffer: Record "XML Buffer" temporary; RootPath: Text) var + CompanyIdentifierValue, SchemaId : Text; ReceivingId: Text[250]; - SchemaId, CompanyIdentifierValue : Text; begin Clear(EDocument."Receiving Company Name"); Clear(EDocument."Receiving Company Address"); @@ -160,49 +162,50 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" Clear(EDocument."Receiving Company GLN"); Clear(EDocument."Receiving Company Id"); - EDocument."Receiving Company Name" := CopyStr(GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name'), 1, MaxStrLen(EDocument."Receiving Company Name")); - EDocument."Receiving Company Address" := CopyStr(GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cac:PostalAddress/cbc:StreetName'), 1, MaxStrLen(EDocument."Receiving Company Address")); - SchemaId := GetNodeAttributeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'); - CompanyIdentifierValue := GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'); + EDocument."Receiving Company Name" := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name'), 1, MaxStrLen(EDocument."Receiving Company Name")); + EDocument."Receiving Company Address" := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PostalAddress/cbc:StreetName'), 1, MaxStrLen(EDocument."Receiving Company Address")); + SchemaId := GetNodeAttributeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'); + CompanyIdentifierValue := GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'); - EDocument."Receiving Company VAT Reg. No." := CopyStr(GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID'), 1, MaxStrLen(EDocument."Receiving Company VAT Reg. No.")); + EDocument."Receiving Company VAT Reg. No." := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID'), 1, MaxStrLen(EDocument."Receiving Company VAT Reg. No.")); if SchemaId = '0088' then EDocument."Receiving Company GLN" := CopyStr(CompanyIdentifierValue, 1, MaxStrLen(EDocument."Receiving Company GLN")); if (EDocument."Receiving Company VAT Reg. No." = '') and (SchemaId <> '0088') then EDocument."Receiving Company VAT Reg. No." := CopyStr(CompanyIdentifierValue, 1, MaxStrLen(EDocument."Receiving Company VAT Reg. No.")); - SchemaId := GetNodeAttributeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID/@schemeID'); - CompanyIdentifierValue := GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID'); + SchemaId := GetNodeAttributeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID/@schemeID'); + CompanyIdentifierValue := GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PartyIdentification/cbc:ID'); if (EDocument."Receiving Company GLN" = '') and (SchemaId = '0088') then EDocument."Receiving Company GLN" := CopyStr(CompanyIdentifierValue, 1, MaxStrLen(EDocument."Receiving Company GLN")); - ReceivingId := CopyStr(this.GetNodeAttributeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), 1, (MaxStrLen(EDocument."Receiving Company Id") - 1)) + ':'; - ReceivingId += CopyStr(this.GetNodeByPath(TempXMLBuffer, '/' + DocumentType + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'), 1, MaxStrLen(EDocument."Receiving Company Id") - StrLen(ReceivingId)); + ReceivingId := CopyStr(this.GetNodeAttributeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID'), 1, (MaxStrLen(EDocument."Receiving Company Id") - 1)) + ':'; + ReceivingId += CopyStr(this.GetNodeByPath(TempXMLBuffer, RootPath + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID'), 1, MaxStrLen(EDocument."Receiving Company Id") - StrLen(ReceivingId)); EDocument."Receiving Company Id" := ReceivingId; end; - local procedure CreateInvoice(var EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: record "Purchase Line" temporary; var TempXMLBuffer: Record "XML Buffer" temporary) + local procedure CreateInvoice(var EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: record "Purchase Line" temporary; var TempXMLBuffer: Record "XML Buffer" temporary; RootPath: Text) var DocumentAttachment: Record "Document Attachment"; DocumentAttachmentData: Codeunit "Temp Blob"; InStream: InStream; + LineNo: Integer; begin PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::Invoice; - PurchaseHeader."No." := CopyStr(GetNodeByPath(TempXMLBuffer, '/Invoice/cbc:ID'), 1, MaxStrLen(PurchaseHeader."No.")); - PurchaseHeader."Vendor Order No." := CopyStr(GetNodeByPath(TempXMLBuffer, '/Invoice/cac:OrderReference/cbc:ID'), 1, MaxStrLen(PurchaseHeader."Vendor Order No.")); + PurchaseHeader."No." := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:ID'), 1, MaxStrLen(PurchaseHeader."No.")); + PurchaseHeader."Vendor Order No." := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cac:OrderReference/cbc:ID'), 1, MaxStrLen(PurchaseHeader."Vendor Order No.")); // Currency PurchaseHeader.Insert(); TempXMLBuffer.Reset(); if TempXMLBuffer.FindSet() then repeat - ParseInvoice(EDocument, PurchaseHeader, PurchaseLine, DocumentAttachment, DocumentAttachmentData, TempXMLBuffer); + ParseInvoice(EDocument, PurchaseHeader, PurchaseLine, DocumentAttachment, DocumentAttachmentData, TempXMLBuffer, LineNo, RootPath); until TempXMLBuffer.Next() = 0; // Insert last document attachment - if DocumentAttachment."No." <> '' then begin + if (DocumentAttachment."No." <> '') and (DocumentAttachment."File Name" <> '') then begin DocumentAttachmentData.CreateInStream(InStream, TextEncoding::UTF8); EDocumentAttachmentGen.Insert(EDocument, InStream, DocumentAttachment.FindUniqueFileName(DocumentAttachment."File Name", DocumentAttachment."File Extension")); Clear(DocumentAttachment); @@ -214,27 +217,28 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" PurchaseHeader.Modify(); // Allowance charge - CreateInvoiceAllowanceChargeLines(EDocument, PurchaseHeader, PurchaseLine, TempXMLBuffer); + CreateInvoiceAllowanceChargeLines(EDocument, PurchaseHeader, PurchaseLine, TempXMLBuffer, RootPath); end; - local procedure CreateCreditMemo(var EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: record "Purchase Line" temporary; var TempXMLBuffer: Record "XML Buffer" temporary) + local procedure CreateCreditMemo(var EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: record "Purchase Line" temporary; var TempXMLBuffer: Record "XML Buffer" temporary; RootPath: Text) var DocumentAttachment: Record "Document Attachment"; DocumentAttachmentData: Codeunit "Temp Blob"; InStream: InStream; + LineNo: Integer; begin PurchaseHeader."Document Type" := PurchaseHeader."Document Type"::"Credit Memo"; - PurchaseHeader."No." := CopyStr(GetNodeByPath(TempXMLBuffer, '/CreditNote/cbc:ID'), 1, MaxStrLen(PurchaseHeader."No.")); + PurchaseHeader."No." := CopyStr(GetNodeByPath(TempXMLBuffer, RootPath + '/cbc:ID'), 1, MaxStrLen(PurchaseHeader."No.")); PurchaseHeader.Insert(); TempXMLBuffer.Reset(); if TempXMLBuffer.FindSet() then repeat - ParseCreditMemo(EDocument, PurchaseHeader, PurchaseLine, DocumentAttachment, DocumentAttachmentData, TempXMLBuffer); + ParseCreditMemo(EDocument, PurchaseHeader, PurchaseLine, DocumentAttachment, DocumentAttachmentData, TempXMLBuffer, LineNo, RootPath); until TempXMLBuffer.Next() = 0; // Insert last document attachment - if DocumentAttachment."No." <> '' then begin + if (DocumentAttachment."No." <> '') and (DocumentAttachment."File Name" <> '') then begin DocumentAttachmentData.CreateInStream(InStream, TextEncoding::UTF8); EDocumentAttachmentGen.Insert(EDocument, InStream, DocumentAttachment.FindUniqueFileName(DocumentAttachment."File Name", DocumentAttachment."File Extension")); Clear(DocumentAttachment); @@ -245,23 +249,15 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" PurchaseHeader.Modify(); // Allowance charge - CreateInvoiceAllowanceChargeLines(EDocument, PurchaseHeader, PurchaseLine, TempXMLBuffer); + CreateInvoiceAllowanceChargeLines(EDocument, PurchaseHeader, PurchaseLine, TempXMLBuffer, RootPath); end; - local procedure CreateInvoiceAllowanceChargeLines(var EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: record "Purchase Line" temporary; var TempXMLBuffer: Record "XML Buffer" temporary) + local procedure CreateInvoiceAllowanceChargeLines(var EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: record "Purchase Line" temporary; var TempXMLBuffer: Record "XML Buffer" temporary; RootPath: Text) var LineNo: Integer; - DocumentText: Text; begin - case EDocument."Document Type" of - EDocument."Document Type"::"Purchase Invoice": - DocumentText := '/Invoice'; - EDocument."Document Type"::"Purchase Credit Memo": - DocumentText := '/CreditNote'; - end; - TempXMLBuffer.Reset(); - TempXMLBuffer.SetFilter(Path, DocumentText + '/cac:AllowanceCharge*'); + TempXMLBuffer.SetFilter(Path, RootPath + '/cac:AllowanceCharge*'); PurchaseLine.FindLast(); LineNo := PurchaseLine."Line No." + 10000; @@ -269,7 +265,7 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" if TempXMLBuffer.FindSet() then repeat case TempXMLBuffer.Path of - DocumentText + '/cac:AllowanceCharge/cbc:ChargeIndicator': + RootPath + '/cac:AllowanceCharge/cbc:ChargeIndicator': if TempXMLBuffer.Value = 'true' then begin SetGLAccountAndInsertLine(EDocument, PurchaseLine, LineNo); @@ -280,12 +276,12 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" PurchaseLine.Quantity := 1; PurchaseLine.Type := PurchaseLine.Type::"G/L Account"; end; - DocumentText + '/cac:AllowanceCharge/cbc:Amount': + RootPath + '/cac:AllowanceCharge/cbc:Amount': if TempXMLBuffer.Value <> '' then begin Evaluate(PurchaseLine."Direct Unit Cost", TempXMLBuffer.Value, 9); Evaluate(PurchaseLine.Amount, TempXMLBuffer.Value, 9); end; - DocumentText + '/cac:AllowanceCharge/cbc:AllowanceChargeReason': + RootPath + '/cac:AllowanceCharge/cbc:AllowanceChargeReason': PurchaseLine.Description := CopyStr(TempXMLBuffer.Value, 1, MaxStrLen(PurchaseLine.Description)); end; @@ -311,48 +307,48 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" /// Parses credit memo information line by line from TempXMLBuffer. /// We handle the insert of Purchase Order Line and Document Attachment after the call to this function. /// - local procedure ParseCreditMemo(EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: record "Purchase Line" temporary; var DocumentAttachment: Record "Document Attachment"; DocumentAttachmentData: Codeunit "Temp Blob"; var TempXMLBuffer: Record "XML Buffer" temporary) + local procedure ParseCreditMemo(EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: record "Purchase Line" temporary; var DocumentAttachment: Record "Document Attachment"; DocumentAttachmentData: Codeunit "Temp Blob"; var TempXMLBuffer: Record "XML Buffer" temporary; var LineNo: Integer; RootPath: Text) var Base64Convert: Codeunit "Base64 Convert"; - OutStream: OutStream; InStream: InStream; + OutStream: OutStream; Path, Value : Text; begin Path := TempXMLBuffer.Path; Value := TempXMLBuffer.Value; case Path of - '/CreditNote/cbc:ID': + RootPath + '/cbc:ID': PurchaseHeader."Vendor Invoice No." := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Vendor Invoice No.")); - '/CreditNote/cbc:IssueDate': + RootPath + '/cbc:IssueDate': if Value <> '' then Evaluate(PurchaseHeader."Document Date", Value, 9); - '/CreditNote/cac:OrderReference/cbc:ID': + RootPath + '/cac:OrderReference/cbc:ID': PurchaseHeader."Vendor Order No." := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Vendor Order No.")); - '/CreditNote/cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID': + RootPath + '/cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID': PurchaseHeader."Applies-to Doc. No." := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Applies-to Doc. No.")); - '/CreditNote/cac:PayeeParty/cac:PartyName/cbc:Name': + RootPath + '/cac:PayeeParty/cac:PartyName/cbc:Name': PurchaseHeader."Pay-to Name" := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Pay-to Name")); - '/CreditNote/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount': + RootPath + '/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount': if Value <> '' then Evaluate(PurchaseHeader."Invoice Discount Value", Value, 9); - '/CreditNote/cac:LegalMonetaryTotal/cbc:PayableAmount': + RootPath + '/cac:LegalMonetaryTotal/cbc:PayableAmount': if Value <> '' then Evaluate(PurchaseHeader."Amount Including VAT", Value, 9); - '/CreditNote/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ID': + RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ID': PurchaseHeader."Your Reference" := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Your Reference")); - '/CreditNote/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName': + RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName': PurchaseHeader."Buy-from Address" := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Buy-from Address")); - '/CreditNote/cac:PayeeParty/cac:PartyLegalEntity/cbc:CompanyID', '/CreditNote/cac:PayeeParty/cac:PartyIdentification/cbc:ID': + RootPath + '/cac:PayeeParty/cac:PartyLegalEntity/cbc:CompanyID', RootPath + '/cac:PayeeParty/cac:PartyIdentification/cbc:ID': PurchaseHeader."VAT Registration No." := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."VAT Registration No.")); - '/CreditNote/cac:PaymentMeans/cbc:PaymentDueDate': + RootPath + '/cac:PaymentMeans/cbc:PaymentDueDate': if Value <> '' then Evaluate(PurchaseHeader."Due Date", Value, 9); - '/CreditNote/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount': + RootPath + '/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount': if Value <> '' then Evaluate(PurchaseHeader.Amount, Value, 9); - '/CreditNote/cac:AdditionalDocumentReference/cbc:ID': + RootPath + '/cac:AdditionalDocumentReference/cbc:ID': begin - if DocumentAttachment."No." <> '' then begin + if (DocumentAttachment."No." <> '') and (DocumentAttachment."File Name" <> '') then begin DocumentAttachmentData.CreateInStream(InStream, TextEncoding::UTF8); EDocumentAttachmentGen.Insert(EDocument, InStream, DocumentAttachment.FindUniqueFileName(DocumentAttachment."File Name", DocumentAttachment."File Extension")); Clear(DocumentAttachment); @@ -361,7 +357,7 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" DocumentAttachment.Init(); DocumentAttachment."No." := CopyStr(PurchaseHeader."Vendor Invoice No.", 1, MaxStrLen(DocumentAttachment."No.")); end; - '/CreditNote/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject': + RootPath + '/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject': begin DocumentAttachmentData.CreateOutStream(OutStream, TextEncoding::UTF8); TempXMLBuffer.CalcFields("Value BLOB"); @@ -369,11 +365,11 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" InStream.Read(Value, InStream.Length); Base64Convert.FromBase64(Value, OutStream); end; - '/CreditNote/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@mimeCode': + RootPath + '/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@mimeCode': DocumentAttachment.Validate("File Extension", DetermineFileType(Value)); - '/CreditNote/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@filename': + RootPath + '/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@filename': DocumentAttachment."File Name" := CopyStr(Value.Split('.').Get(1), 1, MaxStrLen(DocumentAttachment."File Name")); - '/CreditNote/cac:CreditNoteLine': + RootPath + '/cac:CreditNoteLine': begin if PurchaseLine."Document No." <> '' then PurchaseLine.Insert(); @@ -381,41 +377,41 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" PurchaseLine.Init(); PurchaseLine."Document Type" := PurchaseHeader."Document Type"; PurchaseLine."Document No." := PurchaseHeader."No."; + LineNo += 10000; + PurchaseLine."Line No." := LineNo; end; - '/CreditNote/cac:CreditNoteLine/cbc:CreditedQuantity': + RootPath + '/cac:CreditNoteLine/cbc:CreditedQuantity': if Value <> '' then Evaluate(PurchaseLine.Quantity, Value, 9); - '/CreditNote/cac:CreditNoteLine/cbc:CreditedQuantity/@unitCode': + RootPath + '/cac:CreditNoteLine/cbc:CreditedQuantity/@unitCode': PurchaseLine."Unit of Measure Code" := CopyStr(Value, 1, MaxStrLen(PurchaseLine."Unit of Measure Code")); - '/CreditNote/cac:CreditNoteLine/cbc:LineExtensionAmount': + RootPath + '/cac:CreditNoteLine/cbc:LineExtensionAmount': if Value <> '' then Evaluate(PurchaseLine.Amount, Value, 9); - '/CreditNote/cac:CreditNoteLine/cac:AllowanceCharge/cbc:Amount': + RootPath + '/cac:CreditNoteLine/cac:AllowanceCharge/cbc:Amount': if Value <> '' then Evaluate(PurchaseLine."Line Discount Amount", Value, 9); - '/CreditNote/cac:CreditNoteLine/cac:TaxTotal/cbc:TaxAmount': + RootPath + '/cac:CreditNoteLine/cac:TaxTotal/cbc:TaxAmount': if Value <> '' then Evaluate(PurchaseLine."Amount Including VAT", Value, 9); - '/CreditNote/cac:CreditNoteLine/cac:Item/cbc:Description': + RootPath + '/cac:CreditNoteLine/cac:Item/cbc:Description': PurchaseLine."Description 2" := CopyStr(Value, 1, MaxStrLen(PurchaseLine."Description 2")); - '/CreditNote/cac:CreditNoteLine/cac:Item/cbc:Name': + RootPath + '/cac:CreditNoteLine/cac:Item/cbc:Name': PurchaseLine.Description := CopyStr(Value, 1, MaxStrLen(PurchaseLine.Description)); - '/CreditNote/cac:CreditNoteLine/cac:Item/cac:SellersItemIdentification/cbc:ID': + RootPath + '/cac:CreditNoteLine/cac:Item/cac:SellersItemIdentification/cbc:ID': PurchaseLine."Item Reference No." := CopyStr(Value, 1, MaxStrLen(PurchaseLine."Item Reference No.")); - '/CreditNote/cac:CreditNoteLine/cac:Item/cac:StandardItemIdentification/cbc:ID': + RootPath + '/cac:CreditNoteLine/cac:Item/cac:StandardItemIdentification/cbc:ID': PurchaseLine."No." := CopyStr(Value, 1, MaxStrLen(PurchaseLine."No.")); - '/CreditNote/cac:CreditNoteLine/cbc:ID': - Evaluate(PurchaseLine."Line No.", Value, 9); - '/CreditNote/cac:CreditNoteLine/cac:Item/cac:ClassifiedTaxCategory/cbc:Percent': + RootPath + '/cac:CreditNoteLine/cac:Item/cac:ClassifiedTaxCategory/cbc:Percent': if Value <> '' then Evaluate(PurchaseLine."VAT %", Value, 9); - '/CreditNote/cac:CreditNoteLine/cac:Price/cbc:PriceAmount': + RootPath + '/cac:CreditNoteLine/cac:Price/cbc:PriceAmount': if Value <> '' then Evaluate(PurchaseLine."Direct Unit Cost", Value, 9); - '/CreditNote/cac:CreditNoteLine/cac:Price/cbc:BaseQuantity': + RootPath + '/cac:CreditNoteLine/cac:Price/cbc:BaseQuantity': if Value <> '' then Evaluate(PurchaseLine."Quantity (Base)", Value, 9); - '/CreditNote/cac:CreditNoteLine/cbc:Note': + RootPath + '/cac:CreditNoteLine/cbc:Note': setlineType(PurchaseLine, Value); end; OnAfterParseCreditMemo(EDocument, PurchaseHeader, PurchaseLine, DocumentAttachment, DocumentAttachmentData, TempXMLBuffer, Path, Value); @@ -425,46 +421,46 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" /// Parses invoice information line by line from TempXMLBuffer. /// We handle the insert of Purchase Order Line and Document Attachment after the call to this function. /// - local procedure ParseInvoice(EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: Record "Purchase Line" temporary; var DocumentAttachment: Record "Document Attachment"; DocumentAttachmentData: Codeunit "Temp Blob"; var TempXMLBuffer: Record "XML Buffer" temporary) + local procedure ParseInvoice(EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header" temporary; var PurchaseLine: Record "Purchase Line" temporary; var DocumentAttachment: Record "Document Attachment"; DocumentAttachmentData: Codeunit "Temp Blob"; var TempXMLBuffer: Record "XML Buffer" temporary; var LineNo: Integer; RootPath: Text) var Base64Convert: Codeunit "Base64 Convert"; - OutStream: OutStream; InStream: InStream; + OutStream: OutStream; Path, Value : Text; begin Path := TempXMLBuffer.Path; Value := TempXMLBuffer.Value; case Path of - '/Invoice/cbc:ID': + RootPath + '/cbc:ID': PurchaseHeader."Vendor Invoice No." := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Vendor Invoice No.")); - '/Invoice/cac:OrderReference/cbc:ID': + RootPath + '/cac:OrderReference/cbc:ID': PurchaseHeader."Vendor Order No." := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Vendor Order No.")); - '/Invoice/cac:PayeeParty/cac:PartyName/cbc:Name': + RootPath + '/cac:PayeeParty/cac:PartyName/cbc:Name': PurchaseHeader."Pay-to Name" := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Pay-to Name")); - '/Invoice/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount': + RootPath + '/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount': if Value <> '' then Evaluate(PurchaseHeader."Invoice Discount Value", Value, 9); - '/Invoice/cac:LegalMonetaryTotal/cbc:PayableAmount': + RootPath + '/cac:LegalMonetaryTotal/cbc:PayableAmount': if Value <> '' then Evaluate(PurchaseHeader."Amount Including VAT", Value, 9); - '/Invoice/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ID': + RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:ID': PurchaseHeader."Your Reference" := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Your Reference")); - '/Invoice/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName': + RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName': PurchaseHeader."Buy-from Address" := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."Buy-from Address")); - '/Invoice/cac:PayeeParty/cac:PartyLegalEntity/cbc:CompanyID': + RootPath + '/cac:PayeeParty/cac:PartyLegalEntity/cbc:CompanyID': PurchaseHeader."VAT Registration No." := CopyStr(Value, 1, MaxStrLen(PurchaseHeader."VAT Registration No.")); - '/Invoice/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount': + RootPath + '/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount': if Value <> '' then Evaluate(PurchaseHeader.Amount, Value, 9); - '/Invoice/cbc:DueDate': + RootPath + '/cbc:DueDate': if Value <> '' then Evaluate(PurchaseHeader."Due Date", Value, 9); - '/Invoice/cbc:IssueDate': + RootPath + '/cbc:IssueDate': if Value <> '' then Evaluate(PurchaseHeader."Document Date", Value, 9); - '/Invoice/cac:AdditionalDocumentReference/cbc:ID': + RootPath + '/cac:AdditionalDocumentReference/cbc:ID': begin - if DocumentAttachment."No." <> '' then begin + if (DocumentAttachment."No." <> '') and (DocumentAttachment."File Name" <> '') then begin DocumentAttachmentData.CreateInStream(InStream, TextEncoding::UTF8); EDocumentAttachmentGen.Insert(EDocument, InStream, DocumentAttachment.FindUniqueFileName(DocumentAttachment."File Name", DocumentAttachment."File Extension")); Clear(DocumentAttachment); @@ -473,7 +469,7 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" DocumentAttachment.Init(); DocumentAttachment."No." := CopyStr(Value, 1, MaxStrLen(DocumentAttachment."No.")); end; - '/Invoice/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject': + RootPath + '/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject': begin DocumentAttachmentData.CreateOutStream(OutStream, TextEncoding::UTF8); TempXMLBuffer.CalcFields("Value BLOB"); @@ -481,11 +477,11 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" InStream.Read(Value, InStream.Length); Base64Convert.FromBase64(Value, OutStream); end; - '/Invoice/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@mimeCode': + RootPath + '/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@mimeCode': DocumentAttachment.Validate("File Extension", DetermineFileType(Value)); - '/Invoice/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@filename': + RootPath + '/cac:AdditionalDocumentReference/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@filename': DocumentAttachment."File Name" := CopyStr(Value.Split('.').Get(1), 1, MaxStrLen(DocumentAttachment."File Name")); - '/Invoice/cac:InvoiceLine': + RootPath + '/cac:InvoiceLine': begin if PurchaseLine."Document No." <> '' then PurchaseLine.Insert(); @@ -493,42 +489,42 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" PurchaseLine.Init(); PurchaseLine."Document Type" := PurchaseHeader."Document Type"; PurchaseLine."Document No." := PurchaseHeader."No."; + LineNo += 10000; + PurchaseLine."Line No." := LineNo; end; - '/Invoice/cac:InvoiceLine/cbc:InvoicedQuantity': + RootPath + '/cac:InvoiceLine/cbc:InvoicedQuantity': if Value <> '' then Evaluate(PurchaseLine.Quantity, Value, 9); - '/Invoice/cac:InvoiceLine/cbc:InvoicedQuantity/@unitCode': + RootPath + '/cac:InvoiceLine/cbc:InvoicedQuantity/@unitCode': PurchaseLine."Unit of Measure Code" := CopyStr(Value, 1, MaxStrLen(PurchaseLine."Unit of Measure Code")); - '/Invoice/cac:InvoiceLine/cbc:LineExtensionAmount': + RootPath + '/cac:InvoiceLine/cbc:LineExtensionAmount': if Value <> '' then Evaluate(PurchaseLine.Amount, Value, 9); - '/Invoice/cac:InvoiceLine/cac:AllowanceCharge/cbc:Amount': + RootPath + '/cac:InvoiceLine/cac:AllowanceCharge/cbc:Amount': if Value <> '' then Evaluate(PurchaseLine."Line Discount Amount", Value, 9); - '/Invoice/cac:InvoiceLine/cac:TaxTotal/cbc:TaxAmount': + RootPath + '/cac:InvoiceLine/cac:TaxTotal/cbc:TaxAmount': if Value <> '' then Evaluate(PurchaseLine."Amount Including VAT", Value, 9); - '/Invoice/cac:InvoiceLine/cac:Item/cbc:Description': + RootPath + '/cac:InvoiceLine/cac:Item/cbc:Description': PurchaseLine."Description 2" := CopyStr(Value, 1, MaxStrLen(PurchaseLine."Description 2")); - '/Invoice/cac:InvoiceLine/cac:Item/cbc:Name': + RootPath + '/cac:InvoiceLine/cac:Item/cbc:Name': PurchaseLine.Description := CopyStr(Value, 1, MaxStrLen(PurchaseLine.Description)); - '/Invoice/cac:InvoiceLine/cac:Item/cac:SellersItemIdentification/cbc:ID': + RootPath + '/cac:InvoiceLine/cac:Item/cac:SellersItemIdentification/cbc:ID': PurchaseLine."Item Reference No." := CopyStr(Value, 1, MaxStrLen(PurchaseLine."Item Reference No.")); - '/Invoice/cac:InvoiceLine/cac:Item/cac:StandardItemIdentification/cbc:ID': + RootPath + '/cac:InvoiceLine/cac:Item/cac:StandardItemIdentification/cbc:ID': PurchaseLine."No." := CopyStr(Value, 1, MaxStrLen(PurchaseLine."No.")); - '/Invoice/cac:InvoiceLine/cbc:ID': - Evaluate(PurchaseLine."Line No.", Value, 9); - '/Invoice/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:Percent': + RootPath + '/cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:Percent': if Value <> '' then Evaluate(PurchaseLine."VAT %", Value, 9); - '/Invoice/cac:InvoiceLine/cac:Price/cbc:PriceAmount': + RootPath + '/cac:InvoiceLine/cac:Price/cbc:PriceAmount': if Value <> '' then Evaluate(PurchaseLine."Direct Unit Cost", Value, 9); - '/Invoice/cac:InvoiceLine/cac:Price/cbc:BaseQuantity': + RootPath + '/cac:InvoiceLine/cac:Price/cbc:BaseQuantity': if Value <> '' then Evaluate(PurchaseLine."Quantity (Base)", Value, 9); - '/Invoice/cac:InvoiceLine/cbc:Note': + RootPath + '/cac:InvoiceLine/cbc:Note': setlineType(PurchaseLine, Value); end; OnAfterParseInvoice(EDocument, PurchaseHeader, PurchaseLine, DocumentAttachment, DocumentAttachmentData, TempXMLBuffer, Path, Value); @@ -576,31 +572,37 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" exit('0088'); end; - local procedure GetDocumentType(var TempXMLBuffer: Record "XML Buffer" temporary): Text + local procedure GetDocumentType(var TempXMLBuffer: Record "XML Buffer" temporary; var RootPath: Text): Text var + InvalidXMLFileErr: Label 'Invalid XML file.'; begin TempXMLBuffer.Reset(); TempXMLBuffer.SetRange(Type, TempXMLBuffer.Type::Element); TempXMLBuffer.SetRange("Parent Entry No.", 0); if not TempXMLBuffer.FindFirst() then - Error('Invalid XML file'); + Error(InvalidXMLFileErr); + RootPath := TempXMLBuffer.Path; TempXMLBuffer.Reset(); exit(TempXMLBuffer.Name); end; local procedure SetLineType(var PurchaseLine: record "Purchase Line" temporary; Value: Text): Text var + ItemTok: Label 'ITEM', Locked = true; + ChargeItemTok: Label 'CHARGE (ITEM)', Locked = true; + ResourceTok: Label 'RESOURCE', Locked = true; + GLAccountTok: Label 'G/L ACCOUNT', Locked = true; begin case UpperCase(Value) of - 'ITEM': + ItemTok: PurchaseLine.Type := PurchaseLine.Type::Item; - 'CHARGE (ITEM)': + ChargeItemTok: PurchaseLine.Type := PurchaseLine.Type::"Charge (Item)"; - 'RESOURCE': + ResourceTok: PurchaseLine.Type := PurchaseLine.Type::Resource; - 'G/L ACCOUNT': + GLAccountTok: PurchaseLine.Type := PurchaseLine.Type::"G/L Account"; end; end; @@ -614,6 +616,8 @@ codeunit 6166 "EDoc Import PEPPOL BIS 3.0" var EDocumentAttachmentGen: Codeunit "E-Doc. Attachment Processor"; EDocumentImportHelper: Codeunit "E-Document Import Helper"; + InvoiceTok: Label 'INVOICE', Locked = true; + CreditNoteTok: Label 'CREDITNOTE', Locked = true; LCYCode: Code[10]; [IntegrationEvent(false, false)] diff --git a/src/Apps/W1/EDocument/App/src/Format/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Format/docs/CLAUDE.md new file mode 100644 index 0000000000..0703d77331 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Format/docs/CLAUDE.md @@ -0,0 +1,24 @@ +# Format + +PEPPOL BIS 3.0 export and import implementation -- the built-in document format that ships with E-Document Core. This module owns the serialization/deserialization of UBL 2.1 XML for invoices, credit memos, reminders, finance charge memos, and shipments. Localizations add their own formats via the extensible `E-Document` enum; this module provides the W1 baseline. + +## How it works + +The main entry point is `EDocPEPPOLBIS30.Codeunit.al`, which implements the `"E-Document"` interface with five methods: `Check`, `Create`, `CreateBatch`, `GetBasicInfoFromReceivedDocument`, and `GetCompleteInfoFromReceivedDocument`. + +**Export path:** `Create` dispatches by document type to dedicated XML generators. Invoices and credit memos use base app XMLports (`Sales Invoice - PEPPOL BIS 3.0`, `Sales Cr.Memo - PEPPOL BIS 3.0`) which produce UBL 2.1 XML with proper `cac`/`cbc` namespaces. Shipments and transfer shipments use custom codeunits (`EDocShipmentExportToXml`, `EDocTransferShptToXML`) that build XML via `XML DOM Management`. Reminders and finance charge memos share the `FinResultsPEPPOLBIS30` XMLport, which wraps them as UBL Invoice documents with special type codes. After generation, `OnAfterCreatePEPPOLXMLDocument` fires as an integration event, letting subscribers modify the XML blob before it leaves the format layer. + +**Import path:** `EDocImportPEPPOLBIS30` parses incoming UBL XML into temporary `Purchase Header` / `Purchase Line` records. It uses `XML Buffer` (not DOM) for XPath-style traversal. Vendor resolution cascades through three strategies: GLN/VAT number lookup, then service participant matching, then name+address fuzzy matching. + +**Validation:** `Check` delegates to three separate validators depending on source document type: base app `PEPPOL Validation` for sales documents, `PEPPOL Service Validation` for service documents, and `EDocPEPPOLValidation` (in this module) for reminders and finance charge memos. The in-module validator checks company info completeness, country/region codes, currency codes, and customer identification. + +## Things to know + +- `CreateBatch` is intentionally empty -- PEPPOL BIS 3.0 does not support batch export. The interface method exists only to satisfy the contract. +- The `"Embed PDF in export"` flag on E-Document Service controls whether a base64-encoded PDF is embedded inside the XML. This applies to invoices, credit memos, and shipments. +- Currency on import uses a subtle convention: if the document currency matches `GLSetup."LCY Code"`, it is left blank on the E-Document (BC convention for local currency). Only foreign currencies are stored explicitly. +- The `EDocumentStructuredFormat` enum is marked `ObsoleteState = Pending` for removal in v26 -- it bridges an older structured-format reader pattern that is being replaced by newer processing interfaces. +- When PEPPOL BIS 3.0 is selected as document format on a service, the `OnAfterValidateDocumentFormat` subscriber auto-populates supported document types (Sales Invoice, Sales Credit Memo, Service Invoice, Service Credit Memo). +- Shipment exports use a custom XML schema (not standard UBL Despatch Advice) -- they are simpler, flat structures with supplier/customer/delivery sections rather than full PEPPOL Despatch. + +See the [app-level CLAUDE.md](../../docs/CLAUDE.md) for broader architecture context. diff --git a/src/Apps/W1/EDocument/App/src/Helpers/EDocumentImportHelper.Codeunit.al b/src/Apps/W1/EDocument/App/src/Helpers/EDocumentImportHelper.Codeunit.al index bfe18bcc38..ccd2996294 100644 --- a/src/Apps/W1/EDocument/App/src/Helpers/EDocumentImportHelper.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Helpers/EDocumentImportHelper.Codeunit.al @@ -667,10 +667,10 @@ codeunit 6109 "E-Document Import Helper" [Obsolete('Use codeunit 6140 "E-Doc. Import"''s method ProcessIncomingEDocument', '26.0')] procedure ProcessDocument(var EDocument: Record "E-Document"; CreateJnlLine: Boolean) var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; begin - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocumentImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + EDocumentImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); end; #endif diff --git a/src/Apps/W1/EDocument/App/src/Helpers/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Helpers/docs/CLAUDE.md new file mode 100644 index 0000000000..cd5dc626f5 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Helpers/docs/CLAUDE.md @@ -0,0 +1,25 @@ +# Helpers + +Utility codeunits used across the E-Document framework. These are not standalone features -- they provide shared services (error handling, import resolution, logging, JSON parsing) that other modules depend on. The boundary is that helpers never initiate processing; they are called by the Processing, Format, and Integration layers. + +## How it works + +**`EDocumentErrorHelper`** implements the collecting-parameter pattern for errors. Instead of throwing on first failure, callers use `LogSimpleErrorMessage` or `LogErrorMessage` to accumulate errors against an E-Document's context. The errors are stored in BC's `"Error Message"` table, scoped by the E-Document's `RecordId`. After processing completes, the framework checks `HasErrors` to decide whether to proceed or mark the document as failed. This pattern is essential because a single E-Document can fail validation for multiple independent reasons, and the user needs to see all of them at once. All errors are also forwarded to Feature Telemetry for monitoring. + +**`EDocumentImportHelper`** handles the resolution of incoming document data to BC master data. Its main responsibilities are: resolving units of measure (by code, then by International Standard Code, then by description), finding items (by Item Reference, GTIN, vendor item number, or item description), and finding vendors (by GLN, VAT registration number, service participant, or name+address). Each resolution method follows a cascade pattern -- try the most specific match first, fall back to less specific. Errors are logged (not thrown) when no match is found. + +**`EDocumentHelper`** provides document-level utilities: checking if a RecordRef is an E-Document (`IsElectronicDocument`), retrieving the E-Document Service for a document (checking both live and archived workflow step instances), getting the E-Document blob from logs, and enabling HTTP client requests for the core extension. For outbound documents, service resolution walks the workflow step instance chain; for inbound, it uses the service status table. + +**`EDocumentLogHelper`** is a thin public facade over the internal `"E-Document Log"` codeunit, exposing `InsertLog` and `InsertIntegrationLog` for connector implementations that need to record HTTP request/response pairs or status transitions. + +**`EDocumentJsonHelper`** is internal, used for Azure Document Intelligence integration. It parses a specific JSON structure (`outputs.1.result.fields`/`items`) and extracts typed values (text, date, number, currency) into AL variables. + +## Things to know + +- `LogSimpleErrorMessage` vs `LogErrorMessage`: the simple version only takes a message string. The full version also takes a related record and field number, which enables drill-down navigation from the error message to the source record. +- The error helper's `ClearErrorMessages` is called before re-processing to avoid stacking duplicate errors from retry attempts. +- `EDocumentImportHelper` UOM resolution tries three paths: exact Code match, International Standard Code match, then Description match. If all fail, the error is logged but import continues -- the line will just have no UOM set. +- `EDocumentHelper.AllowEDocumentCoreHttpCalls` directly manipulates the `"NAV App Setting"` table using the E-Document Core extension's hardcoded app ID (`e1d97edc-c239-46b4-8d84-6368bdf67c8b`). +- `EDocumentJsonHelper` is tightly coupled to Azure Document Intelligence's output schema -- it is not a general-purpose JSON utility. + +See the [app-level CLAUDE.md](../../docs/CLAUDE.md) for broader architecture context. diff --git a/src/Apps/W1/EDocument/App/src/Integration/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Integration/docs/CLAUDE.md new file mode 100644 index 0000000000..6330f04f0e --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Integration/docs/CLAUDE.md @@ -0,0 +1,27 @@ +# Integration + +The Integration module defines the contracts between the E-Document Core framework and external document exchange services. It owns every interface, context codeunit, and runner that touches an external API -- sending, receiving, polling for async responses, and post-send actions like approval and cancellation. The framework never calls a service directly; it resolves an interface implementation through the extensible `"Service Integration"` enum and delegates through a runner codeunit wrapped in the `Commit(); if not Codeunit.Run()` error-isolation pattern. + +## How it works + +A service integration is wired by extending the `"Service Integration"` enum (`ServiceIntegration.Enum.al`) with a new value that maps to implementations of `IDocumentSender` and `IDocumentReceiver`. At runtime, `"E-Doc. Integration Management"` reads the service record's `"Service Integration V2"` field, resolves the enum to the matching interface implementation, and delegates through a runner codeunit (`SendRunner`, `"Get Response Runner"`, `"E-Document Action Runner"`, etc.). Each runner is a separate codeunit so it can be invoked with `Codeunit.Run()` -- if it throws, the framework catches the error via `GetLastErrorText()` and logs it without crashing the caller. + +Every outbound or inbound HTTP call flows through a **context codeunit** -- `SendContext`, `ReceiveContext`, or `ActionContext`. These contexts bundle an `"Http Message State"` (request + response pair), a `"Temp Blob"` for document content, and an `"Integration Action Status"` for the resulting service status. After the interface call returns, the framework reads `context.Http()` and writes the request/response to the integration log automatically. This is the key difference from the deprecated V1 interface, where the caller had to pass raw `HttpRequestMessage`/`HttpResponseMessage` parameters. + +Async sending is detected automatically: after `SendRunner` calls `IDocumentSender.Send()`, it checks whether the implementation also implements `IDocumentResponseHandler` (via `IDocumentSender is IDocumentResponseHandler`). If so, `IsAsync` is set to true, the service status becomes `"Pending Response"`, and `"E-Document Background Jobs"` schedules a job queue entry running `"E-Document Get Response"` every 5 minutes. That job iterates all pending-response documents, calls `IDocumentResponseHandler.GetResponse()`, and either advances to `Sent` or stays at `"Pending Response"` for the next poll. + +## Things to know + +- The V1 interface `"E-Document Integration"` (7 methods, one enum) is fully deprecated at `CLEAN26`. All new integrations must use the V2 interfaces in `Interfaces/`. The `#if not CLEAN26` blocks in runners and management codeunit handle dual-mode dispatch during the transition. + +- `"Service Integration"` enum implements `IDocumentSender`, `IDocumentReceiver`, and `IConsentManager`. A service only needs to implement the interfaces it uses -- the `"No Integration"` default value maps to the null object `"E-Document No Integration"`. + +- Actions are a separate extensibility axis. The `"Integration Action Type"` enum implements `IDocumentAction`; built-in values are `"Sent Document Approval"` and `"Sent Document Cancellation"`. The approval action (`SentDocumentApproval.Codeunit.al`) casts the sender to `ISentDocumentActions` and calls `GetApprovalStatus()`, setting default statuses of `Approved` / `"Approval Error"`. + +- Batch sending (V2) reuses the same `IDocumentSender.Send()` -- the `EDocument` record parameter contains multiple records via filters. V1 had a separate `SendBatch()` method. + +- The receive flow is two-phase: `IDocumentReceiver.ReceiveDocuments()` returns a `"Temp Blob List"` of metadata (one blob per document), then `DownloadDocument()` is called per-document to fetch the actual content. If the receiver also implements `IReceivedDocumentMarker`, the framework calls `MarkFetched()` to acknowledge the download on the external service before committing the import. + +- Every `Run*` method in `"E-Doc. Integration Management"` re-reads the `EDocument` and `EDocumentService` records after the interface call (`EDocument.Get(...)`). This is intentional -- the interface implementation may have modified fields during execution. + +- `IConsentManager` gates service activation behind a privacy consent dialog. The default implementation delegates to `"Consent Manager Default Impl."`, but integrations can override to show a custom notice. diff --git a/src/Apps/W1/EDocument/App/src/Integration/docs/extensibility.md b/src/Apps/W1/EDocument/App/src/Integration/docs/extensibility.md new file mode 100644 index 0000000000..e81338dad4 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Integration/docs/extensibility.md @@ -0,0 +1,121 @@ +# Extensibility + +## Overview + +The Integration module exposes two independent extension axes: the **service integration enum** (for send/receive implementations) and the **action type enum** (for post-send operations). Both are extensible AL enums that bind interface implementations at runtime. A third-party app extends the enum, provides the implementation codeunit, and the framework calls it automatically when the service is configured. + +All V2 interfaces receive a context codeunit rather than raw HTTP types. This means the framework handles HTTP logging, status tracking, and error isolation -- the implementation just needs to populate the context's `Http()` and `TempBlob`. + +## Send documents to a new service + +Implement `IDocumentSender` (in `Interfaces/IDocumentSender.Interface.al`). This is the only required interface for outbound documents. + +```al +codeunit 50100 "My Service Sender" implements IDocumentSender +{ + procedure Send(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext) + var + Request: HttpRequestMessage; + Client: HttpClient; + begin + Request := SendContext.Http().GetHttpRequestMessage(); + // Build and send request using SendContext.GetTempBlob() for content + Client.Send(Request, SendContext.Http().GetHttpResponseMessage()); + end; +} +``` + +Register by extending the `"Service Integration"` enum (`ServiceIntegration.Enum.al`): + +```al +enumextension 50100 "My Integration" extends "Service Integration" +{ + value(50100; "My Service") + { + Implementation = IDocumentSender = "My Service Sender", + IDocumentReceiver = "E-Document No Integration"; + } +} +``` + +When batch mode is enabled on the service, the framework passes an `EDocument` record with multiple entries (set by filters). The `Send()` method receives all of them at once. + +## Support async sending + +Implement `IDocumentResponseHandler` (in `Interfaces/IDocumentResponseHandler.Interface.al`) on the **same codeunit** that implements `IDocumentSender`. The framework detects this at runtime via `IDocumentSender is IDocumentResponseHandler` -- no additional registration needed. + +```al +codeunit 50100 "My Service Sender" implements IDocumentSender, IDocumentResponseHandler +{ + // ... Send() as above ... + + procedure GetResponse(var EDocument: Record "E-Document"; var EDocumentService: Record "E-Document Service"; SendContext: Codeunit SendContext): Boolean + begin + // Return true when the external service confirms receipt; false to keep polling. + // Set SendContext.Status().SetStatus() to control the final service status. + end; +} +``` + +A background job (`"E-Document Get Response"`) polls every 5 minutes. Returning `true` advances to the status set on `SendContext.Status()`; returning `false` leaves the document at `"Pending Response"`. A runtime error or logged error message sets `"Sending Error"` and stops polling. + +## Receive documents from a service + +Implement `IDocumentReceiver` (in `Interfaces/IDocumentReceiver.Interface.al`). The receive flow is two-phase: + +1. `ReceiveDocuments()` -- query the API for available documents; add one `"Temp Blob"` per document to the `DocumentsMetadata` list (contents are opaque metadata, not the document itself). +2. `DownloadDocument()` -- called once per metadata blob; fetch the actual document content and store it in `ReceiveContext.GetTempBlob()`. Set filename via `ReceiveContext.SetName()` and format via `ReceiveContext.SetFileFormat()`. + +This two-phase design lets the framework create E-Document records between the calls, so `DownloadDocument()` receives a fully initialized `EDocument` record. + +## Mark received documents as fetched + +Optionally implement `IReceivedDocumentMarker` (in `Interfaces/IReceivedDocumentMarker.Interface.al`) on the same codeunit as `IDocumentReceiver`. The framework checks `IDocumentReceiver is IReceivedDocumentMarker` after downloading each document. If present, it calls `MarkFetched()` to acknowledge the download on the external service. If `MarkFetched()` fails, the document is not imported. + +## Check approval and cancellation status + +Implement `ISentDocumentActions` (in `Interfaces/ISentDocumentActions.Interface.al`) on the same codeunit as `IDocumentSender`. The framework casts to this interface when the `"Sent Document Approval"` or `"Sent Document Cancellation"` action runs. + +- `GetApprovalStatus()` -- return `true` to update the document to `Approved`; `false` to leave it unchanged. +- `GetCancellationStatus()` -- return `true` to update to `Canceled`; `false` to leave it unchanged. + +Use `ActionContext.Status().SetStatus()` to override the default target status if needed. + +## Add custom post-send actions + +Extend the `"Integration Action Type"` enum (`IntegrationActionType.Enum.al`) and implement `IDocumentAction` (in `Interfaces/IDocumentAction.Interface.al`): + +```al +enumextension 50100 "My Actions" extends "Integration Action Type" +{ + value(50100; "My Custom Action") + { + Implementation = IDocumentAction = "My Custom Action Impl"; + } +} +``` + +The `InvokeAction()` method returns `true` if the framework should update the E-Document service status to whatever is set on `ActionContext.Status()`, or `false` to leave the status unchanged. + +## Gate service activation behind privacy consent + +Implement `IConsentManager` (in `Interfaces/IConsentManager.Interface.al`) alongside the service integration enum. The default implementation delegates to `"Consent Manager Default Impl."`, which shows BC's standard privacy notice. Override to show a custom consent dialog or enforce region-specific requirements. + +## Filter which documents get exported + +This interface lives in `Processing/Interfaces/`, not here, but is relevant to integration developers. Extend the `"Export Eligibility Evaluator"` enum and implement `IExportEligibilityEvaluator` to control which source documents are eligible for export through a given service. The default implementation (`DefaultExportEligibility.Codeunit.al`) allows all documents. + +## V1 to V2 migration + +The V1 interface `"E-Document Integration"` (8 methods including `GetIntegrationSetup`) is deprecated at version 26.0. Key differences: + +| V1 | V2 | +|---|---| +| Single monolithic interface (6+ methods) | Granular interfaces -- implement only what you need | +| Raw `HttpRequestMessage` / `HttpResponseMessage` parameters | Context codeunits (`SendContext`, `ReceiveContext`, `ActionContext`) | +| Separate `Send()` and `SendBatch()` methods | Single `Send()` -- batch mode uses record filters | +| `GetDocumentCountInBatch()` for receive | `"Temp Blob List"` count determines document count | +| `GetIntegrationSetup()` for setup page | Replaced by `OnBeforeOpenServiceIntegrationSetupPage` event | +| `"E-Document Integration"` enum | `"Service Integration"` enum (field `"Service Integration V2"`) | + +To migrate: move your implementation from `"E-Document Integration"` to the new interfaces, extend `"Service Integration"` instead, and remove the V1 enum extension. The `#if not CLEAN26` blocks in the framework handle dual-mode dispatch during the transition period. diff --git a/src/Apps/W1/EDocument/App/src/Logging/EDocumentLog.Codeunit.al b/src/Apps/W1/EDocument/App/src/Logging/EDocumentLog.Codeunit.al index fa021d1d41..2b2af2b975 100644 --- a/src/Apps/W1/EDocument/App/src/Logging/EDocumentLog.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Logging/EDocumentLog.Codeunit.al @@ -293,7 +293,7 @@ codeunit 6132 "E-Document Log" until Changes.Next() = 0; end; - internal procedure GetDocumentBlobFromLog(EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; var TempBlob: Codeunit "Temp Blob"; EDocumentServiceStatus: Enum "E-Document Service Status"; var EDocumentLog: Record "E-Document Log"): Boolean + procedure GetDocumentBlobFromLog(EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; var TempBlob: Codeunit "Temp Blob"; EDocumentServiceStatus: Enum "E-Document Service Status"; var EDocumentLog: Record "E-Document Log"): Boolean var EDocDataStorage: Record "E-Doc. Data Storage"; EDocumentHelper: Codeunit "E-Document Processing"; diff --git a/src/Apps/W1/EDocument/App/src/Logging/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Logging/docs/CLAUDE.md new file mode 100644 index 0000000000..01e3616a8f --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Logging/docs/CLAUDE.md @@ -0,0 +1,29 @@ +# Logging + +The Logging module provides the audit trail and binary content storage for the E-Document framework. It consists of three tables -- `E-Document Log` for state change history, `E-Document Integration Log` for HTTP request/response capture, and `E-Doc. Data Storage` for binary content -- plus the `E-Document Log` codeunit that orchestrates their creation and linkage. + +## How it works + +Every significant state transition in the E-Document lifecycle produces an `E-Document Log` entry (table 6124, `EDocumentLog.Table.al`). The log records the E-Document entry number, the service code, the service status at the time of the event, document format, integration type, and optionally a reference to a `E-Doc. Data Storage` entry containing the document content at that point. Log entries use AutoIncrement for their primary key, creating a strictly chronological sequence. + +When the framework communicates with an external service, it creates an `E-Document Integration Log` entry (table 6127, `EDocumentIntegrationLog.Table.al`). This captures the HTTP method, request URL (up to 2048 characters), request body, response body, and response status code. The request and response bodies are stored as BLOB fields directly on the log record, not in the shared Data Storage table. The `InsertIntegrationLog` procedure in the codeunit populates these by reading from `HttpRequestMessage` and `HttpResponseMessage` objects, skipping the entry entirely if the request URI is empty or the service has no integration configured. + +The `E-Doc. Data Storage` table (table 6125, `EDocDataStorage.Table.al`) is a shared binary store. Each entry holds a BLOB field, a size integer, a name, and a file format enum (`E-Doc. File Format`). The E-Document table itself points to two Data Storage entries -- one for structured content (e.g., XML) and one for unstructured content (e.g., PDF). Log entries point to a single Data Storage entry representing the document content at that processing stage. + +The `E-Document Log` codeunit (codeunit 6132, `EDocumentLog.Codeunit.al`) is the central orchestrator with 30+ procedures. It manages a two-phase pattern for log creation: first call `SetFields` to configure the log template with E-Document and service context, optionally call `SetBlob` to stage binary content in a temporary Data Storage record, then call `InsertLog` to persist both the Data Storage entry and the log entry in one operation. This avoids orphaned Data Storage records if the log insert fails. + +## Things to know + +- The blob lifecycle follows a specific pattern: processing code works with `TempBlob` (in-memory codeunit), the log codeunit's `InsertDataStorage` persists it to a `E-Doc. Data Storage` record, and the resulting entry number is stamped on the log entry. The `SetBlob` overloads accept Text, TempBlob, or InStream, converting all to the same storage format. + +- Deleting an `E-Document Log` entry cascades to its referenced Data Storage entry via the `OnDelete` trigger. Deleting the parent E-Document cascades to all its log entries, integration log entries, and related records via `CleanupDocument`. + +- The `GetDocumentBlobFromLog` procedure finds the latest log entry matching specific criteria (E-Document, service, integration, format, status) and extracts its Data Storage content. It filters to `Processing Status = Unprocessed` to avoid retrieving content from already-processed stages. If no matching log is found, it emits a telemetry error ('0000LCE'). + +- Integration log entries are only created when the service has a non-"No Integration" integration configured -- the codeunit explicitly checks this and exits early otherwise. + +- The `InsertMappingLog` procedure on this codeunit writes mapping audit records (from the Mapping module) by iterating a temporary record set of applied mappings and inserting `E-Doc. Mapping Log` entries linked to both the document log entry and the E-Document. + +- The `E-Document Log` table's `GetDataStorage` procedure enforces that the caller passes an empty TempBlob (errors on non-empty), preventing accidental data overwrites during content retrieval. + +- The `Step Undone` boolean on log entries marks entries created as part of an undo/rollback operation, distinguishing forward progress from corrections in the audit trail. diff --git a/src/Apps/W1/EDocument/App/src/Mapping/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Mapping/docs/CLAUDE.md new file mode 100644 index 0000000000..b67607c48b --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Mapping/docs/CLAUDE.md @@ -0,0 +1,27 @@ +# Mapping + +The Mapping module provides an optional field-level transformation engine that rewrites values on document records before export or after import. It operates on Text and Code fields only, applying find-and-replace rules or BC Transformation Rules per service configuration. If no mapping records exist for a service, the engine is completely bypassed. + +## How it works + +Mapping rules are stored in the `E-Doc. Mapping` table (table 6118, `EDocMapping.Table.al`), keyed by service code and auto-increment entry number. Each rule targets a specific table and field (via `Table ID` and `Field ID`), with a `Find Value` / `Replace Value` pair or a reference to a `Transformation Rule` (BC's built-in text transformation framework). The `For Import` boolean flag distinguishes rules applied during inbound processing from those applied during outbound export. + +The `E-Doc. Mapping` codeunit (codeunit 6118, `EDocMapping.Codeunit.al`) applies transformations through its `MapRecord` procedure. It works exclusively on temporary RecordRef targets -- passing a non-temporary record raises an error immediately. The mapping runs in three passes with decreasing specificity: first, rules targeting a specific table and specific field; second, rules targeting a specific table but any field (Field ID = 0), which iterates all fields on the record; third, fully generic rules (Table ID = 0, Field ID = 0) that scan every Text/Code field on any record. This layered approach means you can define precise overrides for individual fields while also having broad catch-all substitutions. + +Each applied transformation is tracked: the rule is marked as `Used = true` on the mapping record, and a temporary record set of changes is built. The `E-Document Log` codeunit's `InsertMappingLog` procedure then persists these changes to the `E-Doc. Mapping Log` table (table 6123, `EDocMappingLog.Table.al`), creating a permanent audit trail linked to both the E-Document and the specific E-Document Log entry. + +The codeunit also provides `PreviewMapping`, which lets users select a service and see what transformations would be applied to a given document without actually modifying anything. This opens the `E-Doc. Changes Preview` page showing header and line changes side by side. + +## Things to know + +- Only `FieldClass::Normal` fields of type Text or Code are eligible for mapping. FlowFields, FlowFilters, and non-text fields are silently skipped by `ValidateFieldRef`. + +- The three-pass specificity order matters: a field-specific rule always runs before a table-wide or generic rule. However, all matching rules at each level are applied -- there is no short-circuit after the first match. + +- The `Transformation Rule` field takes precedence over `Find Value` / `Replace Value` when both are present. If a transformation rule is set, the find/replace pair is ignored and `TransformationRule.TransformText` is used instead. + +- Mapping rules are marked `Used = true` during processing by modifying the actual mapping record (not a temporary copy). The `PreviewMapping` procedure resets all `Used` flags to false before running, so preview operations do not leave stale state. + +- The mapping log's composite key `(E-Doc Log Entry No., E-Doc Entry No.)` allows querying all mappings applied to a specific document or to a specific processing step, supporting both document-level and step-level audit views. + +- The `E-Doc. Mapping` table is `Public` but not `Extensible` -- ISVs can read and create mapping records programmatically but cannot add fields to the table. diff --git a/src/Apps/W1/EDocument/App/src/Processing/AI/EDocAIToolProcessor.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/AI/EDocAIToolProcessor.Codeunit.al index a423e0f205..9b66532b97 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/AI/EDocAIToolProcessor.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/AI/EDocAIToolProcessor.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.eServices.EDocument.Processing.AI; using System.AI; +using System.Environment; using System.Environment.Configuration; using System.Globalization; using System.Telemetry; @@ -40,6 +41,8 @@ codeunit 6195 "E-Doc. AI Tool Processor" begin Clear(TelemetryDimensions); + RegisterCapabilityIfNeeded(); + if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"E-Document Matching Assistance") then exit(false); if not CopilotCapability.IsCapabilityActive(Enum::"Copilot Capability"::"E-Document Matching Assistance") then @@ -211,6 +214,18 @@ codeunit 6195 "E-Doc. AI Tool Processor" FeatureTelemetry.LogError('0000PUI', AISystem.GetFeatureName(), EventName, ErrorMessage, '', TelemetryDimensions); end; + local procedure RegisterCapabilityIfNeeded() + var + CopilotCapability: Codeunit "Copilot Capability"; + EnvironmentInformation: Codeunit "Environment Information"; + LearnMoreUrlTxt: Label 'https://go.microsoft.com/fwlink/?linkid=2262630', Locked = true; + begin + if not EnvironmentInformation.IsSaaSInfrastructure() then + exit; + if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"E-Document Matching Assistance") then + CopilotCapability.RegisterCapability(Enum::"Copilot Capability"::"E-Document Matching Assistance", LearnMoreUrlTxt); + end; + local procedure GetDefaultMaxInputTokens(): Integer begin exit(125000); // 125k token limit diff --git a/src/Apps/W1/EDocument/App/src/Processing/AI/Tools/EDocGLAccountMatching.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/AI/Tools/EDocGLAccountMatching.Codeunit.al index 7300712173..aee3d82884 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/AI/Tools/EDocGLAccountMatching.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/AI/Tools/EDocGLAccountMatching.Codeunit.al @@ -237,13 +237,13 @@ codeunit 6126 "E-Doc. GL Account Matching" implements "AOAI Function", IEDocAISy procedure Execute(Arguments: JsonObject): Variant var - EDocMatchLineBuffer: Record "EDoc Line Match Buffer"; + TempEDocMatchLineBuffer: Record "EDoc Line Match Buffer"; begin - EDocMatchLineBuffer."Line No." := Arguments.GetInteger('lineId'); - EDocMatchLineBuffer."GL Account No." := CopyStr(Arguments.GetText('accountId'), 1, MaxStrLen(EDocMatchLineBuffer."GL Account No.")); - EDocMatchLineBuffer."GL Account Reason" := CopyStr(Arguments.GetText('reasoning'), 1, MaxStrLen(EDocMatchLineBuffer."GL Account Reason")); - EDocMatchLineBuffer."GL Account Candidate Count" := Arguments.GetInteger('totalNumberOfPotentialAccounts'); - exit(EDocMatchLineBuffer); + TempEDocMatchLineBuffer."Line No." := Arguments.GetInteger('lineId'); + TempEDocMatchLineBuffer."GL Account No." := CopyStr(Arguments.GetText('accountId'), 1, MaxStrLen(TempEDocMatchLineBuffer."GL Account No.")); + TempEDocMatchLineBuffer."GL Account Reason" := CopyStr(Arguments.GetText('reasoning'), 1, MaxStrLen(TempEDocMatchLineBuffer."GL Account Reason")); + TempEDocMatchLineBuffer."GL Account Candidate Count" := Arguments.GetInteger('totalNumberOfPotentialAccounts'); + exit(TempEDocMatchLineBuffer); end; procedure GetName(): Text diff --git a/src/Apps/W1/EDocument/App/src/Processing/AI/Tools/EDocHistoricalMatching.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/AI/Tools/EDocHistoricalMatching.Codeunit.al index 9531cafeb0..5454c42d24 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/AI/Tools/EDocHistoricalMatching.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/AI/Tools/EDocHistoricalMatching.Codeunit.al @@ -51,6 +51,9 @@ codeunit 6177 "E-Doc. Historical Matching" implements "AOAI Function", IEDocAISy HistoricalMatchingExperimentTok: Label 'EDocHistoricalMatchingExperiment', Locked = true; HistoricalMatchingConfig: Text; begin + if DirectHistoricalMatch(Rec) then + exit; + // Get experiment configuration HistoricalMatchingConfig := FeatureConfiguration.GetConfiguration(HistoricalMatchingExperimentTok); @@ -90,6 +93,44 @@ codeunit 6177 "E-Doc. Historical Matching" implements "AOAI Function", IEDocAISy FeatureTelemetry.LogUsage('0000PUP', EDocumentAIProcessor.GetEDocumentMatchingAssistanceName(), GetFeatureName(), TelemetryDimensions); end; + local procedure DirectHistoricalMatch(var SourceEDocumentPurchaseLine: Record "E-Document Purchase Line"): Boolean + var + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocPurchaseLineHistory: Record "E-Doc. Purchase Line History"; + PurchInvLine: Record "Purch. Inv. Line"; + EDocPurchaseHistMapping: Codeunit "E-Doc. Purchase Hist. Mapping"; + EDocImpSessionTelemetry: Codeunit "E-Doc. Imp. Session Telemetry"; + VendorNo: Code[20]; + DirectHistoricalMatchEventTok: Label 'Direct Historical Match', Locked = true; + begin + EDocumentPurchaseLine.Copy(SourceEDocumentPurchaseLine); + if not EDocumentPurchaseLine.FindFirst() then + exit(false); + + EDocumentPurchaseHeader.SetRange("E-Document Entry No.", EDocumentPurchaseLine."E-Document Entry No."); + if EDocumentPurchaseHeader.FindFirst() then + VendorNo := EDocumentPurchaseHeader."[BC] Vendor No."; + if VendorNo = '' then + exit(false); + + if EDocumentPurchaseLine.FindSet() then + repeat + if EDocumentPurchaseLine."[BC] Purchase Type No." = '' then + if EDocPurchaseHistMapping.FindRelatedPurchaseLineInHistory(VendorNo, EDocumentPurchaseLine, EDocPurchaseLineHistory) then + if PurchInvLine.GetBySystemId(EDocPurchaseLineHistory."Purch. Inv. Line SystemId") then begin + EDocPurchaseHistMapping.UpdateMissingLineValuesFromHistory(PurchInvLine, EDocumentPurchaseLine, '', 'High'); + EDocumentPurchaseLine."E-Doc. Purch. Line History Id" := EDocPurchaseLineHistory."Entry No."; + EDocumentPurchaseLine.Modify(true); + EDocImpSessionTelemetry.SetLineBool(EDocumentPurchaseLine.SystemId, DirectHistoricalMatchEventTok, true); + end; + until EDocumentPurchaseLine.Next() = 0; + + // If no unmatched lines remain, all lines were resolved directly + EDocumentPurchaseLine.SetRange("[BC] Purchase Type No.", ''); + exit(EDocumentPurchaseLine.IsEmpty()); + end; + local procedure GetConfidenceScore(ExperimentConfig: Text): Text begin // When in control group we match exact vendor, hence score baseline is high confidence, else medium confidence @@ -179,6 +220,7 @@ codeunit 6177 "E-Doc. Historical Matching" implements "AOAI Function", IEDocAISy var PurchInvLine: Record "Purch. Inv. Line"; AllocationAccount: Record "Allocation Account"; + EDocPurchaseLineHistory: Record "E-Doc. Purchase Line History"; FeatureTelemetry: Codeunit "Feature Telemetry"; OneYearAgoDate: Date; RecordCount: Integer; @@ -212,6 +254,18 @@ codeunit 6177 "E-Doc. Historical Matching" implements "AOAI Function", IEDocAISy until (PurchInvLine.Next() = 0) or (RecordCount >= MaxHistoricalRecords); end; + // Enrich temp records with product codes from e-document history + EDocPurchaseLineHistory.SetCurrentKey("Vendor No.", "Product Code"); + EDocPurchaseLineHistory.SetRange("Vendor No.", VendorNo); + EDocPurchaseLineHistory.SetFilter("Product Code", '<>%1', ''); + if EDocPurchaseLineHistory.FindSet() then + repeat + if TempPurchInvLine.GetBySystemId(EDocPurchaseLineHistory."Purch. Inv. Line SystemId") then begin + TempPurchInvLine."Vendor Item No." := CopyStr(EDocPurchaseLineHistory."Product Code", 1, MaxStrLen(TempPurchInvLine."Vendor Item No.")); + TempPurchInvLine.Modify(); + end; + until EDocPurchaseLineHistory.Next() = 0; + ElapsedTime := CurrentDateTime() - StartTime; TelemetryDimensions.Add('Records loaded', Format(RecordCount)); TelemetryDimensions.Add('Duration', Format(ElapsedTime)); @@ -279,7 +333,7 @@ codeunit 6177 "E-Doc. Historical Matching" implements "AOAI Function", IEDocAISy TempPurchInvLine.Reset(); case MatchType of ProductCodeTok: - TempPurchInvLine.SetRange("No.", SearchValue); + TempPurchInvLine.SetRange("Vendor Item No.", CopyStr(SearchValue, 1, MaxStrLen(TempPurchInvLine."Vendor Item No."))); DescriptionTok: TempPurchInvLine.SetRange(Description, SearchValue); 'Similar Description': diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocAttachmentProcessor.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocAttachmentProcessor.Codeunit.al index 471f6d2eba..501e5d358b 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocAttachmentProcessor.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocAttachmentProcessor.Codeunit.al @@ -181,6 +181,38 @@ codeunit 6169 "E-Doc. Attachment Processor" DocumentAttachment.FilterGroup(0); end; + [EventSubscriber(ObjectType::Page, Page::"Doc. Attachment List Factbox", OnAfterGetRecRefFail, '', false, false)] + local procedure OnAfterGetRecRefFailForEDocs(var DocumentAttachment: Record "Document Attachment"; var RecRef: RecordRef) + var + EDocument: Record "E-Document"; + EDocumentEntryNo: Integer; + EDocumentEntryNoText: Text; + begin + DocumentAttachment.FilterGroup(4); + EDocumentEntryNoText := DocumentAttachment.GetFilter("E-Document Entry No."); + DocumentAttachment.FilterGroup(0); + if EDocumentEntryNoText = '' then + exit; + + Evaluate(EDocumentEntryNo, EDocumentEntryNoText); + if not EDocument.Get(EDocumentEntryNo) then + exit; + + RecRef.GetTable(EDocument); + end; + + [EventSubscriber(ObjectType::Table, Database::"Document Attachment", OnBeforeInsertAttachment, '', false, false)] + local procedure OnBeforeInsertAttachmentForEDocs(var DocumentAttachment: Record "Document Attachment"; var RecRef: RecordRef) + var + EDocument: Record "E-Document"; + begin + if RecRef.Number() <> Database::"E-Document" then + exit; + + DocumentAttachment.Validate("E-Document Attachment", true); + DocumentAttachment.Validate("E-Document Entry No.", RecRef.Field(EDocument.FieldNo("Entry No")).Value()); + end; + var MissingEDocumentTypeErr: Label 'E-Document type %1 is not supported for attachments', Comment = '%1 - E-Document document type'; diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocImport.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocImport.Codeunit.al index c5d9bf88eb..eb974e0b5f 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocImport.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocImport.Codeunit.al @@ -26,7 +26,7 @@ codeunit 6140 "E-Doc. Import" procedure ReceiveAndProcessAutomatically(EDocumentService: Record "E-Document Service"): Boolean var EDocumentServiceStatus: Record "E-Document Service Status"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocument: Record "E-Document"; EDocIntegrationMgt: Codeunit "E-Doc. Integration Management"; ReceiveContext: Codeunit ReceiveContext; @@ -40,7 +40,7 @@ codeunit 6140 "E-Doc. Import" #endif EDocIntegrationMgt.ReceiveDocuments(EDocumentService, ReceiveContext); - EDocImportParameters := EDocumentService.GetDefaultImportParameters(); + TempEDocImportParameters := EDocumentService.GetDefaultImportParameters(); AllEDocumentsProcessed := true; EDocumentServiceStatus.SetRange("E-Document Service Code", EDocumentService.Code); @@ -49,7 +49,7 @@ codeunit 6140 "E-Doc. Import" if EDocumentServiceStatus.FindSet() then repeat EDocument.Get(EDocumentServiceStatus."E-Document Entry No"); - AllEDocumentsProcessed := AllEDocumentsProcessed and ProcessIncomingEDocument(EDocument, EDocumentService, EDocImportParameters); + AllEDocumentsProcessed := AllEDocumentsProcessed and ProcessIncomingEDocument(EDocument, EDocumentService, TempEDocImportParameters); until EDocumentServiceStatus.Next() = 0; exit(AllEDocumentsProcessed); end; diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentCopilotCapability.EnumExt.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentCopilotCapability.EnumExt.al index 55394a7171..a0e9743c8a 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentCopilotCapability.EnumExt.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentCopilotCapability.EnumExt.al @@ -12,4 +12,8 @@ enumextension 6164 "E-Document Copilot Capability" extends "Copilot Capability" { Caption = 'E-Document analysis'; } + value(6166; "E-Document MLLM Analysis") + { + Caption = 'E-Document MLLM analysis'; + } } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al index a11862a36e..b53bb12b12 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/EDocumentSubscribers.Codeunit.al @@ -4,6 +4,7 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Format; using Microsoft.eServices.EDocument.IO.Peppol; using Microsoft.eServices.EDocument.OrderMatch; using Microsoft.EServices.EDocument.Processing; @@ -30,6 +31,8 @@ using Microsoft.Service.Document; using Microsoft.Service.History; using Microsoft.Service.Posting; using Microsoft.Utilities; +using Microsoft.Warehouse.Activity; +using System.AI; using System.Automation; using System.Reflection; using System.Telemetry; @@ -48,6 +51,14 @@ codeunit 6103 "E-Document Subscribers" DeleteDocumentQst: Label 'This document is linked to E-Document %1. Do you want to continue?', Comment = '%1 - E-Document Entry No.'; + [EventSubscriber(ObjectType::Page, Page::"Copilot AI Capabilities", OnRegisterCopilotCapability, '', false, false)] + local procedure HandleOnRegisterCopilotCapability() + var + EDocumentMLLMHandler: Codeunit "E-Document MLLM Handler"; + begin + EDocumentMLLMHandler.RegisterCopilotCapabilityIfNeeded(); + end; + #region Draft page user edits [EventSubscriber(ObjectType::Page, Page::"E-Document Purchase Draft", OnAfterValidateEvent, "Vendor No.", false, false)] @@ -225,6 +236,10 @@ codeunit 6103 "E-Document Subscribers" SalesShipmentHeader: Record "Sales Shipment Header"; DocumentSendingProfile: Record "Document Sending Profile"; begin + + if not AllowCreateEDocument(CommitIsSuppressed, InvtPickPutaway, PreviewMode, 'Sales-Post') then + exit; + if (SalesInvHdrNo = '') and (SalesCrMemoHdrNo = '') and (SalesShptHdrNo = '') then exit; if not EDocumentProcessing.GetDocSendingProfileForCust(SalesHeader."Bill-to Customer No.", DocumentSendingProfile) then @@ -260,10 +275,13 @@ codeunit 6103 "E-Document Subscribers" end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"TransferOrder-Post Shipment", OnAfterTransferOrderPostShipment, '', false, false)] - local procedure CreateEDocumentFromPostedTransferShipment(var TransferHeader: Record "Transfer Header"; CommitIsSuppressed: Boolean; var TransferShipmentHeader: Record "Transfer Shipment Header"; InvtPickPutaway: Boolean) + local procedure CreateEDocumentFromPostedTransferShipment(var TransferHeader: Record "Transfer Header"; CommitIsSuppressed: Boolean; PreviewMode: Boolean; var TransferShipmentHeader: Record "Transfer Shipment Header"; InvtPickPutaway: Boolean) var DocumentSendingProfile: Record "Document Sending Profile"; begin + if not AllowCreateEDocument(CommitIsSuppressed, InvtPickPutaway, PreviewMode, 'TransferOrder-Post Shipment') then + exit; + if TransferShipmentHeader."No." = '' then exit; @@ -274,6 +292,44 @@ codeunit 6103 "E-Document Subscribers" end; #endregion After posting events + #region Warehouse completion — deferred E-Document creation + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Whse.-Activity-Post", OnAfterPostWhseActivityCompleted, '', false, false)] + local procedure OnAfterPostWhseActivityCompleted(WhseActivHeader: Record "Warehouse Activity Header"; var PurchaseHeader: Record "Purchase Header"; var SalesHeader: Record "Sales Header"; var TransferHeader: Record "Transfer Header"; SuppressCommit: Boolean; IsPreview: Boolean) + var + SalesShipmentHeader: Record "Sales Shipment Header"; + SalesInvoiceHeader: Record "Sales Invoice Header"; + TransferShipmentHeader: Record "Transfer Shipment Header"; + DocumentSendingProfile: Record "Document Sending Profile"; + begin + // For Inventory Pick flows, E-Documents are created here instead of inline in the posting + // subscribers, because this event fires after all posting work completes (including + // PostRelatedInboundTransfer) — so the full transaction is already persisted. + // Other activity types (Put-away, Movement) are not affected. + + if WhseActivHeader.Type <> WhseActivHeader.Type::"Invt. Pick" then + exit; + if not AllowCreateEDocument(SuppressCommit, false, IsPreview, 'Whse.-Activity-Post') then + exit; + + // Sales Shipment + if SalesHeader."Last Shipping No." <> '' then + if SalesShipmentHeader.Get(SalesHeader."Last Shipping No.") then + if EDocumentProcessing.GetDocSendingProfileForCust(SalesHeader."Bill-to Customer No.", DocumentSendingProfile) then + CreateEDocumentFromPostedDocument(SalesShipmentHeader, DocumentSendingProfile, Enum::"E-Document Type"::"Sales Shipment"); + + // Sales Invoice (Ship+Invoice scenario) + if SalesHeader."Last Posting No." <> '' then + if SalesInvoiceHeader.Get(SalesHeader."Last Posting No.") then + if EDocumentProcessing.GetDocSendingProfileForCust(SalesHeader."Bill-to Customer No.", DocumentSendingProfile) then + CreateEDocumentFromPostedDocument(SalesInvoiceHeader, DocumentSendingProfile, Enum::"E-Document Type"::"Sales Invoice"); + + // Transfer Shipment + if TransferHeader."Last Shipment No." <> '' then + if TransferShipmentHeader.Get(TransferHeader."Last Shipment No.") then + if EDocumentProcessing.GetDocSendingProfileForTransferShipment(DocumentSendingProfile, TransferShipmentHeader."Transfer-to Code") then + CreateEDocumentFromPostedDocument(TransferShipmentHeader, DocumentSendingProfile, Enum::"E-Document Type"::"Transfer Shipment"); + end; + #endregion Warehouse completion [EventSubscriber(ObjectType::Table, Database::"Purchases & Payables Setup", OnAfterShouldDocumentTotalAmountsBeChecked, '', false, false)] local procedure OnShouldDocumentTotalAmountsBeChecked(PurchaseHeader: Record "Purchase Header"; var ShouldDocumentTotalAmountsBeChecked: Boolean) @@ -299,13 +355,16 @@ codeunit 6103 "E-Document Subscribers" CanDocumentTotalAmountsBeEdited := not EDocument.IsSourceDocumentStructured(); end; - [EventSubscriber(ObjectType::Codeunit, Codeunit::"Service-Post", 'OnAfterPostServiceDoc', '', false, false)] + [EventSubscriber(ObjectType::Codeunit, Codeunit::"Service-Post", OnAfterPostServiceDoc, '', false, false)] local procedure OnAfterPostServiceDoc(var ServiceHeader: Record "Service Header"; ServShipmentNo: Code[20]; ServInvoiceNo: Code[20]; ServCrMemoNo: Code[20]; var ServDocumentsMgt: Codeunit "Serv-Documents Mgt."; CommitIsSuppressed: Boolean; PassedShip: Boolean; PassedConsume: Boolean; PassedInvoice: Boolean; WhseShip: Boolean) var ServiceInvoiceHeader: Record "Service Invoice Header"; ServiceCrMemoHdr: Record "Service Cr.Memo Header"; DocumentSendingProfile: Record "Document Sending Profile"; begin + if not AllowCreateEDocument(CommitIsSuppressed, false, false, 'Service-Post') then + exit; + if (ServInvoiceNo = '') and (ServCrMemoNo = '') then exit; @@ -379,7 +438,7 @@ codeunit 6103 "E-Document Subscribers" local procedure OnBeforeOnDeletePurchaseHeader(var PurchaseHeader: Record "Purchase Header"; var IsHandled: Boolean) var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; ConfirmDialogMgt: Codeunit "Confirm Management"; begin @@ -391,9 +450,9 @@ codeunit 6103 "E-Document Subscribers" if not ConfirmDialogMgt.GetResponseOrDefault(StrSubstNo(DeleteDocumentQst, EDocument."Entry No")) then Error(''); - EDocImportParameters."Step to Run / Desired Status" := EDocImportParameters."Step to Run / Desired Status"::"Desired E-Document Status"; - EDocImportParameters."Desired E-Document Status" := "Import E-Doc. Proc. Status"::"Draft Ready"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run / Desired Status" := TempEDocImportParameters."Step to Run / Desired Status"::"Desired E-Document Status"; + TempEDocImportParameters."Desired E-Document Status" := "Import E-Doc. Proc. Status"::"Draft Ready"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); PurchaseHeader.Get(PurchaseHeader."Document Type", PurchaseHeader."No."); end; @@ -613,6 +672,28 @@ codeunit 6103 "E-Document Subscribers" end; end; + /// + /// Determine whether to allow creating E-Document based on the context of posting. + /// For Inventory Pick, we want to allow E-Document creation only in the OnAfterPostWhseActivityCompleted event, but not in the Sales-Post event, to avoid creating E-Document before the transaction is fully committed. + /// For other scenarios, we can create E-Document in the posting event. + /// + local procedure AllowCreateEDocument(CommitIsSuppressed: Boolean; InvtPickPutaway: Boolean; PreviewMode: Boolean; SourceEvent: Text): Boolean + var + Telemetry: Codeunit Telemetry; + TelemetryDimensions: Dictionary of [Text, Text]; + DeferredCreationLbl: Label 'E-Document creation deferred', Locked = true; + begin + if not (CommitIsSuppressed or InvtPickPutaway or PreviewMode) then + exit(true); + + TelemetryDimensions.Add('Source', SourceEvent); + TelemetryDimensions.Add('PreviewMode', Format(PreviewMode)); + TelemetryDimensions.Add('InvtPickPutaway', Format(InvtPickPutaway)); + TelemetryDimensions.Add('CommitIsSuppressed', Format(CommitIsSuppressed)); + Telemetry.LogMessage('0000SIG', DeferredCreationLbl, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, TelemetryDimensions); + exit(false); + end; + local procedure LogAfterValidate(EDocumentEntryNo: Integer; LineSystemId: Guid; FieldName: Text) var EDocument: Record "E-Document"; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocProcessDraft.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocProcessDraft.Enum.al index 3240293b1d..9b62c4ab3f 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocProcessDraft.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocProcessDraft.Enum.al @@ -13,9 +13,24 @@ enum 6107 "E-Doc. Process Draft" implements IProcessStructuredData { Extensible = true; +#if not CLEAN29 value(0; "Purchase Document") { Caption = 'Purchase Document'; Implementation = IProcessStructuredData = "Prepare Purchase E-Doc. Draft"; + ObsoleteState = Pending; + ObsoleteReason = 'Use "Purchase Invoice" or "Purchase Credit Memo" instead.'; + ObsoleteTag = '29.0'; } -} \ No newline at end of file +#endif + value(1; "Purchase Invoice") + { + Caption = 'Purchase Invoice'; + Implementation = IProcessStructuredData = "Prepare Purchase E-Doc. Draft"; + } + value(2; "Purchase Credit Memo") + { + Caption = 'Purchase Credit Memo'; + Implementation = IProcessStructuredData = "EDoc Prepare Cr. Memo Draft"; + } +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al index ea1ccaea1d..fa0fe2fb0b 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/EDocReadIntoDraft.Enum.al @@ -35,4 +35,9 @@ enum 6109 "E-Doc. Read into Draft" implements IStructuredFormatReader Caption = 'PEPPOL'; Implementation = IStructuredFormatReader = "E-Document PEPPOL Handler"; } + value(4; "MLLM") + { + Caption = 'MLLM'; + Implementation = IStructuredFormatReader = "E-Document MLLM Handler"; + } } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/FileFormat/EDocPDFFileFormat.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/FileFormat/EDocPDFFileFormat.Codeunit.al index 38d777f721..62be1b354a 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/FileFormat/EDocPDFFileFormat.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/FileFormat/EDocPDFFileFormat.Codeunit.al @@ -6,6 +6,7 @@ namespace Microsoft.EServices.EDocument.Format; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Interfaces; +using System.Config; using System.Utilities; codeunit 6191 "E-Doc. PDF File Format" implements IEDocFileFormat @@ -20,12 +21,31 @@ codeunit 6191 "E-Doc. PDF File Format" implements IEDocFileFormat end; procedure PreferredStructureDataImplementation(): Enum "Structure Received E-Doc." + var + FeatureConfiguration: Codeunit "Feature Configuration"; + Result: Enum "Structure Received E-Doc."; + IsExperiment: Boolean; + begin + IsExperiment := FeatureConfiguration.GetConfiguration(MLLMExperimentTok) = 'mllm'; + Result := IsExperiment ? "Structure Received E-Doc."::MLLM : "Structure Received E-Doc."::ADI; + OnAfterSetIStructureReceivedEDocumentForPdf(Result); + exit(Result); + end; + + /// + /// Allows subscribers to override which structure data implementation is used for PDF processing. + /// This is specifically used by the Payables Agent to force MLLM processing on, regardless of the experiment setting. + /// + [IntegrationEvent(false, false)] + local procedure OnAfterSetIStructureReceivedEDocumentForPdf(var Result: Enum "Structure Received E-Doc.") begin - exit("Structure Received E-Doc."::ADI); end; procedure FileExtension(): Text begin exit('pdf'); end; + + var + MLLMExperimentTok: Label 'EDocMLLMExtraction', Locked = true; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchCrMemo.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchCrMemo.Codeunit.al new file mode 100644 index 0000000000..b97c6db072 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchCrMemo.Codeunit.al @@ -0,0 +1,128 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Import; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Purchases.Document; +using Microsoft.Purchases.Payables; +using System.Telemetry; + +/// +/// Dealing with the creation of the purchase credit memo after the draft has been populated. +/// +codeunit 6404 "E-Doc. Create Purch. Cr. Memo" implements IEDocumentFinishDraft, IEDocumentCreatePurchaseCreditMemo +{ + Access = Internal; + + var + Telemetry: Codeunit "Telemetry"; + CrMemoAlreadyExistsErr: Label 'A purchase credit memo with external document number %1 already exists for vendor %2.', Comment = '%1 = Vendor Cr. Memo No., %2 = Vendor No.'; + DraftLineDoesNotContainTypeAndNumberErr: Label 'One of the draft lines do not contain the type and number. Please, specify these fields manually.'; + + procedure ApplyDraftToBC(EDocument: Record "E-Document"; EDocImportParameters: Record "E-Doc. Import Parameters"): RecordId + var + PurchaseHeader: Record "Purchase Header"; + EDocPurchaseDocumentHelper: Codeunit "E-Doc. Purch. Doc. Helper"; + EDocImpSessionTelemetry: Codeunit "E-Doc. Imp. Session Telemetry"; + EmptyRecordId: RecordId; + IEDocumentFinishPurchaseCrMemo: Interface IEDocumentCreatePurchaseCreditMemo; + begin + IEDocumentFinishPurchaseCrMemo := EDocImportParameters."Processing Customizations"; + if EDocImportParameters."Existing Doc. RecordId" <> EmptyRecordId then begin + EDocImpSessionTelemetry.SetBool('LinkedToExisting', true); + PurchaseHeader.Get(EDocImportParameters."Existing Doc. RecordId"); + end else + PurchaseHeader := IEDocumentFinishPurchaseCrMemo.CreatePurchaseCreditMemo(EDocument); + + EDocPurchaseDocumentHelper.FinalizeCreatedDocument(EDocument, PurchaseHeader); + + exit(PurchaseHeader.RecordId); + end; + + procedure RevertDraftActions(EDocument: Record "E-Document") + var + PurchaseHeader: Record "Purchase Header"; + EDocPurchaseDocumentHelper: Codeunit "E-Doc. Purch. Doc. Helper"; + begin + PurchaseHeader.SetRange("E-Document Link", EDocument.SystemId); + if not PurchaseHeader.FindFirst() then + exit; + + PurchaseHeader.TestField("Document Type", "Purchase Document Type"::"Credit Memo"); + EDocPurchaseDocumentHelper.RevertCreatedDocument(EDocument); + end; + + procedure CreatePurchaseCreditMemo(EDocument: Record "E-Document"): Record "Purchase Header" + var + PurchaseHeader: Record "Purchase Header"; + GLSetup: Record "General Ledger Setup"; + VendorLedgerEntry: Record "Vendor Ledger Entry"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + EDocRecordLink: Record "E-Doc. Record Link"; + EDocPurchaseDocumentHelper: Codeunit "E-Doc. Purch. Doc. Helper"; + PurchCalcDiscByType: Codeunit "Purch - Calc Disc. By Type"; + StopCreatingCreditMemo: Boolean; + VendorCrMemoNo: Code[35]; + PurchaseLineNo: Integer; + begin + EDocumentPurchaseHeader.GetFromEDocument(EDocument); + if not EDocPurchaseDocumentHelper.AllDraftLinesHaveTypeAndNumber(EDocumentPurchaseHeader) then begin + Telemetry.LogMessage('0000SNH', 'Draft line does not contain type or number', Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::All); + Error(DraftLineDoesNotContainTypeAndNumberErr); + end; + EDocumentPurchaseHeader.TestField("E-Document Entry No."); + PurchaseHeader.SetRange("Buy-from Vendor No.", EDocumentPurchaseHeader."[BC] Vendor No."); + PurchaseHeader."Document Type" := "Purchase Document Type"::"Credit Memo"; + PurchaseHeader."Pay-to Vendor No." := EDocumentPurchaseHeader."[BC] Vendor No."; + PurchaseHeader."Posting Description" := EDocumentPurchaseHeader."Posting Description"; + if EDocumentPurchaseHeader."Document Date" <> 0D then + PurchaseHeader.Validate("Document Date", EDocumentPurchaseHeader."Document Date"); + if EDocumentPurchaseHeader."Due Date" <> 0D then + PurchaseHeader.Validate("Due Date", EDocumentPurchaseHeader."Due Date"); + + VendorCrMemoNo := CopyStr(EDocumentPurchaseHeader."Sales Invoice No.", 1, MaxStrLen(PurchaseHeader."Vendor Cr. Memo No.")); + VendorLedgerEntry.SetLoadFields("Entry No."); + VendorLedgerEntry.ReadIsolation := VendorLedgerEntry.ReadIsolation::ReadUncommitted; + StopCreatingCreditMemo := PurchaseHeader.FindPostedDocumentWithSameExternalDocNo(VendorLedgerEntry, VendorCrMemoNo); + if StopCreatingCreditMemo then begin + Telemetry.LogMessage('0000SNI', CrMemoAlreadyExistsErr, Verbosity::Error, DataClassification::OrganizationIdentifiableInformation, TelemetryScope::All); + Error(CrMemoAlreadyExistsErr, VendorCrMemoNo, EDocumentPurchaseHeader."[BC] Vendor No."); + end; + + PurchaseHeader.Validate("Vendor Cr. Memo No.", VendorCrMemoNo); + if EDocumentPurchaseHeader."Purchase Order No." <> '' then + PurchaseHeader."Vendor Order No." := CopyStr(EDocumentPurchaseHeader."Purchase Order No.", 1, MaxStrLen(PurchaseHeader."Vendor Order No.")); + PurchaseHeader.Insert(true); + PurchaseHeader.Modify(); + + GLSetup.GetRecordOnce(); + if EDocumentPurchaseHeader."Currency Code" <> GLSetup.GetCurrencyCode('') then + PurchaseHeader.Validate("Currency Code", EDocumentPurchaseHeader."Currency Code"); + + if EDocumentPurchaseHeader."Applies-to Doc. No." <> '' then + PurchaseHeader."Applies-to Doc. No." := CopyStr(EDocumentPurchaseHeader."Applies-to Doc. No.", 1, MaxStrLen(PurchaseHeader."Applies-to Doc. No.")); + + PurchaseHeader.Modify(); + + EDocRecordLink.InsertEDocumentHeaderLink(EDocumentPurchaseHeader, PurchaseHeader); + + PurchaseLineNo := EDocPurchaseDocumentHelper.GetLastPurchaseLineNo("Purchase Document Type"::"Credit Memo", PurchaseHeader."No."); + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + if EDocumentPurchaseLine.FindSet() then + repeat + PurchaseLineNo += 10000; + EDocPurchaseDocumentHelper.CreatePurchaseLineFromDraft(PurchaseHeader, EDocumentPurchaseLine, EDocumentPurchaseHeader."Total Discount" > 0, PurchaseLineNo); + until EDocumentPurchaseLine.Next() = 0; + + PurchaseHeader.Modify(); + PurchCalcDiscByType.ApplyInvDiscBasedOnAmt(EDocumentPurchaseHeader."Total Discount", PurchaseHeader); + exit(PurchaseHeader); + end; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al index e26ff50213..0f2b7905d4 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocCreatePurchaseInvoice.Codeunit.al @@ -8,12 +8,9 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing; using Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.eServices.EDocument.Processing.Interfaces; -using Microsoft.Finance.Dimension; using Microsoft.Finance.GeneralLedger.Setup; -using Microsoft.Foundation.Attachment; using Microsoft.Purchases.Document; using Microsoft.Purchases.Payables; -using Microsoft.Purchases.Posting; using System.Telemetry; /// @@ -22,12 +19,9 @@ using System.Telemetry; codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, IEDocumentCreatePurchaseInvoice { Access = Internal; - Permissions = tabledata "Dimension Set Tree Node" = im, - tabledata "Dimension Set Entry" = im; var Telemetry: Codeunit "Telemetry"; - EDocImpSessionTelemetry: Codeunit "E-Doc. Imp. Session Telemetry"; InvoiceAlreadyExistsErr: Label 'A purchase invoice with external document number %1 already exists for vendor %2.', Comment = '%1 = Vendor Invoice No., %2 = Vendor No.'; DraftLineDoesNotConstantTypeAndNumberErr: Label 'One of the draft lines do not contain the type and number. Please, specify these fields manually.'; @@ -37,11 +31,13 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, PurchaseHeader: Record "Purchase Header"; TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; EDocPOMatching: Codeunit "E-Doc. PO Matching"; - DocumentAttachmentMgt: Codeunit "Document Attachment Mgmt"; + EDocPurchaseDocumentHelper: Codeunit "E-Doc. Purch. Doc. Helper"; + EDocImpSessionTelemetry: Codeunit "E-Doc. Imp. Session Telemetry"; EmptyRecordId: RecordId; IEDocumentFinishPurchaseDraft: Interface IEDocumentCreatePurchaseInvoice; YourMatchedLinesAreNotValidErr: Label 'The purchase invoice cannot be created because one or more of its matched lines are not valid matches. Review if your configuration allows for receiving at invoice.'; SomeLinesNotYetReceivedErr: Label 'Some of the matched purchase order lines have not yet been received, you need to either receive the lines or remove the matches.'; + MissingInformationForMatchErr: Label 'Some of the draft lines that were matched to purchase order lines are missing unit of measure information. Please specify the unit of measure for those lines and try again.'; begin EDocumentPurchaseHeader.GetFromEDocument(EDocument); @@ -50,9 +46,12 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, EDocPOMatching.SuggestReceiptsForMatchedOrderLines(EDocumentPurchaseHeader); EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::NotYetReceived); + TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::ExceedsInvoiceableQty); if not TempPOMatchWarnings.IsEmpty() then Error(SomeLinesNotYetReceivedErr); + TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::MissingInformationForMatch); + if not TempPOMatchWarnings.IsEmpty() then + Error(MissingInformationForMatchErr); IEDocumentFinishPurchaseDraft := EDocImportParameters."Processing Customizations"; if EDocImportParameters."Existing Doc. RecordId" <> EmptyRecordId then begin @@ -62,20 +61,7 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, PurchaseHeader := IEDocumentFinishPurchaseDraft.CreatePurchaseInvoice(EDocument); EDocPOMatching.TransferPOMatchesFromEDocumentToInvoice(EDocument); - PurchaseHeader.SetRecFilter(); - PurchaseHeader.FindFirst(); - PurchaseHeader."Doc. Amount Incl. VAT" := EDocumentPurchaseHeader.Total; - PurchaseHeader."Doc. Amount VAT" := EDocumentPurchaseHeader."Total VAT"; - PurchaseHeader.TestField("No."); - PurchaseHeader."E-Document Link" := EDocument.SystemId; - PurchaseHeader.Modify(); - - // Post document creation - DocumentAttachmentMgt.CopyAttachments(EDocument, PurchaseHeader); - DocumentAttachmentMgt.DeleteAttachedDocuments(EDocument); - - // Post document validation - Silently emit telemetry - EDocImpSessionTelemetry.SetBool('Totals Validation', TryValidateDocumentTotals(PurchaseHeader)); + EDocPurchaseDocumentHelper.FinalizeCreatedDocument(EDocument, PurchaseHeader); exit(PurchaseHeader.RecordId); end; @@ -84,19 +70,15 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, var PurchaseHeader: Record "Purchase Header"; EDocPOMatching: Codeunit "E-Doc. PO Matching"; - DocumentAttachmentMgt: Codeunit "Document Attachment Mgmt"; + EDocPurchaseDocumentHelper: Codeunit "E-Doc. Purch. Doc. Helper"; begin PurchaseHeader.SetRange("E-Document Link", EDocument.SystemId); if not PurchaseHeader.FindFirst() then exit; EDocPOMatching.TransferPOMatchesFromInvoiceToEDocument(PurchaseHeader); - DocumentAttachmentMgt.CopyAttachments(PurchaseHeader, EDocument); - DocumentAttachmentMgt.DeleteAttachedDocuments(PurchaseHeader); - PurchaseHeader.TestField("Document Type", "Purchase Document Type"::Invoice); - Clear(PurchaseHeader."E-Document Link"); - PurchaseHeader.Modify(); + EDocPurchaseDocumentHelper.RevertCreatedDocument(EDocument); end; procedure CreatePurchaseInvoice(EDocument: Record "E-Document"): Record "Purchase Header" @@ -108,6 +90,7 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, EDocumentPurchaseLine: Record "E-Document Purchase Line"; PurchaseLine: Record "Purchase Line"; EDocRecordLink: Record "E-Doc. Record Link"; + EDocPurchaseDocumentHelper: Codeunit "E-Doc. Purch. Doc. Helper"; PurchCalcDiscByType: Codeunit "Purch - Calc Disc. By Type"; EDocLineByReceipt: Query "E-Doc. Line by Receipt"; LastReceiptNo: Code[20]; @@ -118,7 +101,7 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, NullGuid: Guid; begin EDocumentPurchaseHeader.GetFromEDocument(EDocument); - if not AllDraftLinesHaveTypeAndNumberSpecificed(EDocumentPurchaseHeader) then begin + if not EDocPurchaseDocumentHelper.AllDraftLinesHaveTypeAndNumber(EDocumentPurchaseHeader) then begin Telemetry.LogMessage('0000PLY', 'Draft line does not contain type or number', Verbosity::Error, DataClassification::SystemMetadata, TelemetryScope::All); Error(DraftLineDoesNotConstantTypeAndNumberErr); end; @@ -127,6 +110,10 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, PurchaseHeader."Document Type" := "Purchase Document Type"::Invoice; PurchaseHeader."Pay-to Vendor No." := EDocumentPurchaseHeader."[BC] Vendor No."; PurchaseHeader."Posting Description" := EDocumentPurchaseHeader."Posting Description"; + if EDocumentPurchaseHeader."Document Date" <> 0D then + PurchaseHeader.Validate("Document Date", EDocumentPurchaseHeader."Document Date"); + if EDocumentPurchaseHeader."Due Date" <> 0D then + PurchaseHeader.Validate("Due Date", EDocumentPurchaseHeader."Due Date"); VendorInvoiceNo := CopyStr(EDocumentPurchaseHeader."Sales Invoice No.", 1, MaxStrLen(PurchaseHeader."Vendor Invoice No.")); VendorLedgerEntry.SetLoadFields("Entry No."); @@ -137,11 +124,9 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, Error(InvoiceAlreadyExistsErr, VendorInvoiceNo, EDocumentPurchaseHeader."[BC] Vendor No."); end; - if EDocumentPurchaseHeader."Document Date" <> 0D then - PurchaseHeader.Validate("Document Date", EDocumentPurchaseHeader."Document Date"); - if EDocumentPurchaseHeader."Due Date" <> 0D then - PurchaseHeader.Validate("Due Date", EDocumentPurchaseHeader."Due Date"); PurchaseHeader.Validate("Vendor Invoice No.", VendorInvoiceNo); + if EDocumentPurchaseHeader."Purchase Order No." <> '' then + PurchaseHeader."Vendor Order No." := CopyStr(EDocumentPurchaseHeader."Purchase Order No.", 1, MaxStrLen(PurchaseHeader."Vendor Order No.")); PurchaseHeader.Insert(true); PurchaseHeader."Invoice Received Date" := PurchaseHeader."Document Date"; @@ -155,7 +140,7 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, end; EDocRecordLink.InsertEDocumentHeaderLink(EDocumentPurchaseHeader, PurchaseHeader); - PurchaseLineNo := GetLastLineNumberOnPurchaseInvoice(PurchaseHeader."No."); // We get the last line number, even if this is a new document since recurrent lines get inserted on the header's creation + PurchaseLineNo := EDocPurchaseDocumentHelper.GetLastPurchaseLineNo("Purchase Document Type"::Invoice, PurchaseHeader."No."); // We get the last line number, even if this is a new document since recurrent lines get inserted on the header's creation // We create first the lines without any PO matches EDocLineByReceipt.SetRange(EDocumentEntryNo, EDocument."Entry No"); EDocLineByReceipt.SetRange(ReceiptNo, ''); @@ -163,7 +148,8 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, EDocLineByReceipt.Open(); while EDocLineByReceipt.Read() do begin EDocumentPurchaseLine.GetBySystemId(EDocLineByReceipt.SystemId); - CreatePurchaseInvoiceLine(PurchaseHeader, EDocumentPurchaseLine, EDocumentPurchaseHeader."Total Discount" > 0, PurchaseLineNo); + PurchaseLineNo += 10000; + EDocPurchaseDocumentHelper.CreatePurchaseLineFromDraft(PurchaseHeader, EDocumentPurchaseLine, EDocumentPurchaseHeader."Total Discount" > 0, PurchaseLineNo); end; EDocLineByReceipt.Close(); @@ -184,7 +170,8 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, PurchaseLine.Insert(); end; EDocumentPurchaseLine.GetBySystemId(EDocLineByReceipt.SystemId); - CreatePurchaseInvoiceLine(PurchaseHeader, EDocumentPurchaseLine, EDocumentPurchaseHeader."Total Discount" > 0, PurchaseLineNo); + PurchaseLineNo += 10000; + EDocPurchaseDocumentHelper.CreatePurchaseLineFromDraft(PurchaseHeader, EDocumentPurchaseLine, EDocumentPurchaseHeader."Total Discount" > 0, PurchaseLineNo); LastReceiptNo := EDocLineByReceipt.ReceiptNo; end; EDocLineByReceipt.Close(); @@ -193,82 +180,4 @@ codeunit 6117 "E-Doc. Create Purchase Invoice" implements IEDocumentFinishDraft, exit(PurchaseHeader); end; - local procedure CreatePurchaseInvoiceLine(PurchaseHeader: Record "Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; HasTotalDiscount: Boolean; var PurchaseLineNo: Integer) - var - PurchaseLine: Record "Purchase Line"; - EDocRecordLink: Record "E-Doc. Record Link"; - EDocumentPurchaseHistMapping: Codeunit "E-Doc. Purchase Hist. Mapping"; - DimensionManagement: Codeunit DimensionManagement; - PurchaseLineCombinedDimensions: array[10] of Integer; - GlobalDim1, GlobalDim2 : Code[20]; - begin - PurchaseLine."Document Type" := PurchaseHeader."Document Type"; - PurchaseLine."Document No." := PurchaseHeader."No."; - PurchaseLineNo += 10000; - PurchaseLine."Line No." := PurchaseLineNo; - PurchaseLine."Unit of Measure Code" := CopyStr(EDocumentPurchaseLine."[BC] Unit of Measure", 1, MaxStrLen(PurchaseLine."Unit of Measure Code")); - PurchaseLine."Variant Code" := EDocumentPurchaseLine."[BC] Variant Code"; - PurchaseLine.Type := EDocumentPurchaseLine."[BC] Purchase Line Type"; - PurchaseLine.Validate("No.", EDocumentPurchaseLine."[BC] Purchase Type No."); - if (PurchaseLine.Type = PurchaseLine.Type::"G/L Account") and HasTotalDiscount then - PurchaseLine.Validate("Allow Invoice Disc.", true); - PurchaseLine.Description := EDocumentPurchaseLine.Description; - - if EDocumentPurchaseLine."[BC] Item Reference No." <> '' then - PurchaseLine.Validate("Item Reference No.", EDocumentPurchaseLine."[BC] Item Reference No."); - - PurchaseLine.Validate(Quantity, EDocumentPurchaseLine.Quantity); - PurchaseLine.Validate("Direct Unit Cost", EDocumentPurchaseLine."Unit Price"); - if EDocumentPurchaseLine."Total Discount" > 0 then - PurchaseLine.Validate("Line Discount Amount", EDocumentPurchaseLine."Total Discount"); - PurchaseLine.Validate("Deferral Code", EDocumentPurchaseLine."[BC] Deferral Code"); - - PurchaseLineCombinedDimensions[1] := PurchaseLine."Dimension Set ID"; - PurchaseLineCombinedDimensions[2] := EDocumentPurchaseLine."[BC] Dimension Set ID"; - PurchaseLine.Validate("Dimension Set ID", DimensionManagement.GetCombinedDimensionSetID(PurchaseLineCombinedDimensions, GlobalDim1, GlobalDim2)); - PurchaseLine.Validate("Shortcut Dimension 1 Code", EDocumentPurchaseLine."[BC] Shortcut Dimension 1 Code"); - PurchaseLine.Validate("Shortcut Dimension 2 Code", EDocumentPurchaseLine."[BC] Shortcut Dimension 2 Code"); - EDocumentPurchaseHistMapping.ApplyAdditionalFieldsFromHistoryToPurchaseLine(EDocumentPurchaseLine, PurchaseLine); - PurchaseLine.Insert(); - EDocRecordLink.InsertEDocumentLineLink(EDocumentPurchaseLine, PurchaseLine); - end; - - [TryFunction] - local procedure TryValidateDocumentTotals(PurchaseHeader: Record "Purchase Header") - var - PurchPost: Codeunit "Purch.-Post"; - begin - // If document totals are setup, we just run the validation - PurchPost.CheckDocumentTotalAmounts(PurchaseHeader); - end; - - local procedure AllDraftLinesHaveTypeAndNumberSpecificed(EDocumentPurchaseHeader: Record "E-Document Purchase Header"): Boolean - var - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - begin - EDocumentPurchaseLine.SetLoadFields("[BC] Purchase Line Type", "[BC] Purchase Type No."); - EDocumentPurchaseLine.ReadIsolation(IsolationLevel::ReadCommitted); - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentPurchaseHeader."E-Document Entry No."); - if not EDocumentPurchaseLine.FindSet() then - exit(true); - repeat - if EDocumentPurchaseLine."[BC] Purchase Line Type" = EDocumentPurchaseLine."[BC] Purchase Line Type"::" " then - exit(false); - if EDocumentPurchaseLine."[BC] Purchase Type No." = '' then - exit(false); - until EDocumentPurchaseLine.Next() = 0; - exit(true); - end; - - local procedure GetLastLineNumberOnPurchaseInvoice(DocumentNo: Code[20]): Integer - var - PurchaseLine: Record "Purchase Line"; - begin - PurchaseLine.SetLoadFields("Line No."); - PurchaseLine.ReadIsolation := IsolationLevel::ReadUncommitted; - PurchaseLine.SetRange("Document Type", "Purchase Document Type"::Invoice); - PurchaseLine.SetRange("Document No.", DocumentNo); - if PurchaseLine.FindLast() then - exit(PurchaseLine."Line No."); - end; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocPurchDocHelper.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocPurchDocHelper.Codeunit.al new file mode 100644 index 0000000000..dd97e61275 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/FinishDraft/EDocPurchDocHelper.Codeunit.al @@ -0,0 +1,140 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Import; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.Finance.Dimension; +using Microsoft.Foundation.Attachment; +using Microsoft.Purchases.Document; +using Microsoft.Purchases.Posting; + +/// +/// Shared logic for creating BC purchase documents (invoices and credit memos) from e-document draft data. +/// +codeunit 6402 "E-Doc. Purch. Doc. Helper" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + Permissions = tabledata "Dimension Set Tree Node" = im, + tabledata "Dimension Set Entry" = im; + + procedure CreatePurchaseLineFromDraft(PurchaseHeader: Record "Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; HasTotalDiscount: Boolean; LineNo: Integer) + var + PurchaseLine: Record "Purchase Line"; + EDocRecordLink: Record "E-Doc. Record Link"; + EDocumentPurchaseHistMapping: Codeunit "E-Doc. Purchase Hist. Mapping"; + DimensionManagement: Codeunit DimensionManagement; + PurchaseLineCombinedDimensions: array[10] of Integer; + GlobalDim1, GlobalDim2 : Code[20]; + begin + PurchaseLine."Document Type" := PurchaseHeader."Document Type"; + PurchaseLine."Document No." := PurchaseHeader."No."; + PurchaseLine."Line No." := LineNo; + PurchaseLine."Unit of Measure Code" := CopyStr(EDocumentPurchaseLine."[BC] Unit of Measure", 1, MaxStrLen(PurchaseLine."Unit of Measure Code")); + PurchaseLine."Variant Code" := EDocumentPurchaseLine."[BC] Variant Code"; + PurchaseLine.Type := EDocumentPurchaseLine."[BC] Purchase Line Type"; + PurchaseLine.Validate("No.", EDocumentPurchaseLine."[BC] Purchase Type No."); + if (PurchaseLine.Type = PurchaseLine.Type::"G/L Account") and HasTotalDiscount then + PurchaseLine.Validate("Allow Invoice Disc.", true); + PurchaseLine.Description := EDocumentPurchaseLine.Description; + + if EDocumentPurchaseLine."[BC] Item Reference No." <> '' then + PurchaseLine.Validate("Item Reference No.", EDocumentPurchaseLine."[BC] Item Reference No."); + + PurchaseLine.Validate(Quantity, EDocumentPurchaseLine.Quantity); + PurchaseLine.Validate("Direct Unit Cost", EDocumentPurchaseLine."Unit Price"); + if EDocumentPurchaseLine."Total Discount" > 0 then + PurchaseLine.Validate("Line Discount Amount", EDocumentPurchaseLine."Total Discount"); + PurchaseLine.Validate("Deferral Code", EDocumentPurchaseLine."[BC] Deferral Code"); + + PurchaseLineCombinedDimensions[1] := PurchaseLine."Dimension Set ID"; + PurchaseLineCombinedDimensions[2] := EDocumentPurchaseLine."[BC] Dimension Set ID"; + PurchaseLine.Validate("Dimension Set ID", DimensionManagement.GetCombinedDimensionSetID(PurchaseLineCombinedDimensions, GlobalDim1, GlobalDim2)); + PurchaseLine.Validate("Shortcut Dimension 1 Code", EDocumentPurchaseLine."[BC] Shortcut Dimension 1 Code"); + PurchaseLine.Validate("Shortcut Dimension 2 Code", EDocumentPurchaseLine."[BC] Shortcut Dimension 2 Code"); + EDocumentPurchaseHistMapping.ApplyAdditionalFieldsFromHistoryToPurchaseLine(EDocumentPurchaseLine, PurchaseLine); + PurchaseLine.Insert(); + EDocRecordLink.InsertEDocumentLineLink(EDocumentPurchaseLine, PurchaseLine); + end; + + procedure AllDraftLinesHaveTypeAndNumber(EDocumentPurchaseHeader: Record "E-Document Purchase Header"): Boolean + var + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseLine.SetLoadFields("[BC] Purchase Line Type", "[BC] Purchase Type No."); + EDocumentPurchaseLine.ReadIsolation(IsolationLevel::ReadCommitted); + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentPurchaseHeader."E-Document Entry No."); + if not EDocumentPurchaseLine.FindSet() then + exit(true); + repeat + if EDocumentPurchaseLine."[BC] Purchase Line Type" = EDocumentPurchaseLine."[BC] Purchase Line Type"::" " then + exit(false); + if EDocumentPurchaseLine."[BC] Purchase Type No." = '' then + exit(false); + until EDocumentPurchaseLine.Next() = 0; + exit(true); + end; + + [TryFunction] + procedure TryValidateDocumentTotals(PurchaseHeader: Record "Purchase Header") + var + PurchPost: Codeunit "Purch.-Post"; + begin + PurchPost.CheckDocumentTotalAmounts(PurchaseHeader); + end; + + procedure GetLastPurchaseLineNo(DocumentType: Enum "Purchase Document Type"; DocumentNo: Code[20]): Integer + var + PurchaseLine: Record "Purchase Line"; + begin + PurchaseLine.SetLoadFields("Line No."); + PurchaseLine.ReadIsolation := IsolationLevel::ReadUncommitted; + PurchaseLine.SetRange("Document Type", DocumentType); + PurchaseLine.SetRange("Document No.", DocumentNo); + if PurchaseLine.FindLast() then + exit(PurchaseLine."Line No."); + end; + + procedure FinalizeCreatedDocument(EDocument: Record "E-Document"; var PurchaseHeader: Record "Purchase Header") + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + DocumentAttachmentMgt: Codeunit "Document Attachment Mgmt"; + EDocImpSessionTelemetry: Codeunit "E-Doc. Imp. Session Telemetry"; + begin + EDocumentPurchaseHeader.GetFromEDocument(EDocument); + + PurchaseHeader.SetRecFilter(); + PurchaseHeader.FindFirst(); + PurchaseHeader."Doc. Amount Incl. VAT" := EDocumentPurchaseHeader.Total; + PurchaseHeader."Doc. Amount VAT" := EDocumentPurchaseHeader."Total VAT"; + PurchaseHeader.TestField("No."); + PurchaseHeader."E-Document Link" := EDocument.SystemId; + PurchaseHeader.Modify(); + + DocumentAttachmentMgt.CopyAttachments(EDocument, PurchaseHeader); + DocumentAttachmentMgt.DeleteAttachedDocuments(EDocument); + + EDocImpSessionTelemetry.SetBool('Totals Validation', TryValidateDocumentTotals(PurchaseHeader)); + end; + + procedure RevertCreatedDocument(EDocument: Record "E-Document") + var + PurchaseHeader: Record "Purchase Header"; + DocumentAttachmentMgt: Codeunit "Document Attachment Mgmt"; + begin + PurchaseHeader.SetRange("E-Document Link", EDocument.SystemId); + if not PurchaseHeader.FindFirst() then + exit; + + DocumentAttachmentMgt.CopyAttachments(PurchaseHeader, EDocument); + DocumentAttachmentMgt.DeleteAttachedDocuments(PurchaseHeader); + + Clear(PurchaseHeader."E-Document Link"); + PurchaseHeader.Modify(); + end; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/ImportEDocumentProcess.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/ImportEDocumentProcess.Codeunit.al index 63e70b7b6f..f7ba514c45 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/ImportEDocumentProcess.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/ImportEDocumentProcess.Codeunit.al @@ -34,7 +34,7 @@ codeunit 6104 "Import E-Document Process" ImportProcessVersion := GlobalEDocument.GetEDocumentService().GetImportProcessVersion(); if ImportProcessVersion = "E-Document Import Process"::"Version 1.0" then begin - ProcessEDocumentV1(GlobalEDocument, GlobalEDocImportParameters, GlobalStep, GlobalUndoStep); + ProcessEDocumentV1(GlobalEDocument, TempGlobalEDocImportParameters, GlobalStep, GlobalUndoStep); exit; end; @@ -51,9 +51,9 @@ codeunit 6104 "Import E-Document Process" GlobalStep::"Read into Draft": ReadIntoDraft(GlobalEDocument); GlobalStep::"Prepare draft": - PrepareDraft(GlobalEDocument, GlobalEDocImportParameters); + PrepareDraft(GlobalEDocument, TempGlobalEDocImportParameters); GlobalStep::"Finish draft": - FinishDraft(GlobalEDocument, GlobalEDocImportParameters); + FinishDraft(GlobalEDocument, TempGlobalEDocImportParameters); end; GlobalEDocument.Get(GlobalEDocument."Entry No"); @@ -236,7 +236,7 @@ codeunit 6104 "Import E-Document Process" this.GlobalEDocument := EDocument; GlobalStep := NewStep; GlobalUndoStep := NewUndoStep; - this.GlobalEDocImportParameters := EDocImportParameters; + this.TempGlobalEDocImportParameters := EDocImportParameters; end; procedure IsEDocumentInStateGE(EDocument: Record "E-Document"; QueriedState: Enum "Import E-Doc. Proc. Status"): Boolean @@ -360,7 +360,7 @@ codeunit 6104 "Import E-Document Process" var GlobalEDocument: Record "E-Document"; - GlobalEDocImportParameters: Record "E-Doc. Import Parameters"; + TempGlobalEDocImportParameters: Record "E-Doc. Import Parameters"; EDocumentProcessing: Codeunit "E-Document Processing"; GlobalStep: Enum "Import E-Document Steps"; GlobalUndoStep: Boolean; diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocPrepareCrMemoDraft.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocPrepareCrMemoDraft.Codeunit.al new file mode 100644 index 0000000000..dc91c9216a --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocPrepareCrMemoDraft.Codeunit.al @@ -0,0 +1,38 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Import; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.Purchases.Vendor; + +codeunit 6403 "EDoc Prepare Cr. Memo Draft" implements IProcessStructuredData +{ + Access = Internal; + + var + PrepareDraftHelper: Codeunit "EDoc Prepare Purch. Draft"; + + procedure PrepareDraft(EDocument: Record "E-Document"; EDocImportParameters: Record "E-Doc. Import Parameters"): Enum "E-Document Type" + begin + PrepareDraftHelper.PrepareDraft(EDocument, EDocImportParameters); + exit("E-Document Type"::"Purchase Credit Memo"); + end; + + procedure OpenDraftPage(var EDocument: Record "E-Document") + begin + PrepareDraftHelper.OpenDraftPage(EDocument); + end; + + procedure CleanUpDraft(EDocument: Record "E-Document") + begin + PrepareDraftHelper.CleanUpDraft(EDocument); + end; + + procedure GetVendor(EDocument: Record "E-Document"; Customizations: Enum "E-Doc. Proc. Customizations") Vendor: Record Vendor + begin + Vendor := PrepareDraftHelper.GetVendor(EDocument, Customizations); + end; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocPreparePurchDraft.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocPreparePurchDraft.Codeunit.al new file mode 100644 index 0000000000..23612228b1 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocPreparePurchDraft.Codeunit.al @@ -0,0 +1,175 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Import; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.AI; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.eServices.EDocument.Processing.Interfaces; +using Microsoft.Foundation.UOM; +using Microsoft.Purchases.Document; +using Microsoft.Purchases.Vendor; +using System.Log; + +/// +/// Shared logic for preparing purchase document drafts (invoices and credit memos). +/// +codeunit 6406 "EDoc Prepare Purch. Draft" +{ + Access = Internal; + + var + EDocImpSessionTelemetry: Codeunit "E-Doc. Imp. Session Telemetry"; + + procedure PrepareDraft(EDocument: Record "E-Document"; EDocImportParameters: Record "E-Doc. Import Parameters") + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + UnitOfMeasure: Record "Unit of Measure"; + Vendor: Record Vendor; + PurchaseOrder: Record "Purchase Header"; + EDocVendorAssignmentHistory: Record "E-Doc. Vendor Assign. History"; + EDocPurchaseHistMapping: Codeunit "E-Doc. Purchase Hist. Mapping"; + EDocActivityLogSession: Codeunit "E-Doc. Activity Log Session"; + IUnitOfMeasureProvider: Interface IUnitOfMeasureProvider; + IPurchaseLineProvider: Interface IPurchaseLineProvider; + IPurchaseOrderProvider: Interface IPurchaseOrderProvider; + begin + IUnitOfMeasureProvider := EDocImportParameters."Processing Customizations"; + IPurchaseLineProvider := EDocImportParameters."Processing Customizations"; + IPurchaseOrderProvider := EDocImportParameters."Processing Customizations"; + + if EDocActivityLogSession.CreateSession() then; + + EDocumentPurchaseHeader.GetFromEDocument(EDocument); + EDocumentPurchaseHeader.TestField("E-Document Entry No."); + if EDocumentPurchaseHeader."[BC] Vendor No." = '' then begin + Vendor := GetVendor(EDocument, EDocImportParameters."Processing Customizations"); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + end; + + PurchaseOrder := IPurchaseOrderProvider.GetPurchaseOrder(EDocumentPurchaseHeader); + if PurchaseOrder."No." <> '' then begin + EDocumentPurchaseHeader."[BC] Purchase Order No." := PurchaseOrder."No."; + EDocumentPurchaseHeader.Modify(); + end; + if EDocPurchaseHistMapping.FindRelatedPurchaseHeaderInHistory(EDocument, EDocVendorAssignmentHistory) then + EDocPurchaseHistMapping.UpdateMissingHeaderValuesFromHistory(EDocVendorAssignmentHistory, EDocumentPurchaseHeader); + EDocumentPurchaseHeader.Modify(); + + EDocImpSessionTelemetry.SetBool('Vendor', EDocumentPurchaseHeader."[BC] Vendor No." <> ''); + if EDocumentPurchaseHeader."[BC] Vendor No." <> '' then begin + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + + if EDocumentPurchaseLine.FindSet() then + repeat + UnitOfMeasure := IUnitOfMeasureProvider.GetUnitOfMeasure(EDocument, EDocumentPurchaseLine."Line No.", EDocumentPurchaseLine."Unit of Measure"); + EDocumentPurchaseLine."[BC] Unit of Measure" := UnitOfMeasure.Code; + IPurchaseLineProvider.GetPurchaseLine(EDocumentPurchaseLine); + EDocumentPurchaseLine.Modify(); + until EDocumentPurchaseLine.Next() = 0; + + CopilotLineMatching(EDocument."Entry No"); + end; + + Clear(EDocumentPurchaseLine); + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + if EDocumentPurchaseLine.FindSet() then + repeat + EDocImpSessionTelemetry.SetLine(EDocumentPurchaseLine.SystemId); + until EDocumentPurchaseLine.Next() = 0; + + LogAllActivitySessionChanges(EDocActivityLogSession); + + if EDocActivityLogSession.EndSession() then; + end; + + procedure GetVendor(EDocument: Record "E-Document"; Customizations: Enum "E-Doc. Proc. Customizations") Vendor: Record Vendor + var + IVendorProvider: Interface IVendorProvider; + begin + IVendorProvider := Customizations; + Vendor := IVendorProvider.GetVendor(EDocument); + end; + + procedure OpenDraftPage(var EDocument: Record "E-Document") + var + EDocumentPurchaseDraft: Page "E-Document Purchase Draft"; + begin + EDocumentPurchaseDraft.Editable(true); + EDocumentPurchaseDraft.SetRecord(EDocument); + EDocumentPurchaseDraft.Run(); + end; + + procedure CleanUpDraft(EDocument: Record "E-Document") + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseHeader.SetRange("E-Document Entry No.", EDocument."Entry No"); + if not EDocumentPurchaseHeader.IsEmpty() then + EDocumentPurchaseHeader.DeleteAll(true); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + if not EDocumentPurchaseLine.IsEmpty() then + EDocumentPurchaseLine.DeleteAll(true); + end; + + local procedure LogAllActivitySessionChanges(EDocActivityLogSession: Codeunit "E-Doc. Activity Log Session") + begin + Log(EDocActivityLogSession, EDocActivityLogSession.AccountNumberTok()); + Log(EDocActivityLogSession, EDocActivityLogSession.DeferralTok()); + Log(EDocActivityLogSession, EDocActivityLogSession.ItemRefTok()); + Log(EDocActivityLogSession, EDocActivityLogSession.TextToAccountMappingTok()); + end; + + local procedure Log(EDocActivityLogSession: Codeunit "E-Doc. Activity Log Session"; ActivityLogName: Text) + var + ActivityLog: Codeunit "Activity Log Builder"; + ActivityLogList: List of [Codeunit "Activity Log Builder"]; + Found: Boolean; + begin + Clear(ActivityLogList); + EDocActivityLogSession.GetAll(ActivityLogName, ActivityLogList, Found); + foreach ActivityLog in ActivityLogList do + ActivityLog.Log(); + end; + + local procedure CopilotLineMatching(EDocumentEntryNo: Integer) + var + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseLine.SetLoadFields("E-Document Entry No.", "[BC] Purchase Type No.", "[BC] Deferral Code", Description, "Product Code", Quantity, "Unit of Measure", "Unit Price"); + EDocumentPurchaseLine.ReadIsolation(IsolationLevel::ReadCommitted); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.SetRange("[BC] Purchase Type No.", ''); + EDocumentPurchaseLine.SetRange("[BC] Item Reference No.", ''); + + if not EDocumentPurchaseLine.IsEmpty() then begin + Commit(); + Codeunit.Run(Codeunit::"E-Doc. Historical Matching", EDocumentPurchaseLine); + end; + + Clear(EDocumentPurchaseLine); + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.SetRange("[BC] Purchase Type No.", ''); + EDocumentPurchaseLine.SetRange("[BC] Item Reference No.", ''); + if not EDocumentPurchaseLine.IsEmpty() then begin + Commit(); + Codeunit.Run(Codeunit::"E-Doc. GL Account Matching", EDocumentPurchaseLine); + end; + + Clear(EDocumentPurchaseLine); + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.SetRange("[BC] Deferral Code", ''); + EDocumentPurchaseLine.SetRange("[BC] Item Reference No.", ''); + if not EDocumentPurchaseLine.IsEmpty() then begin + Commit(); + if Codeunit.Run(Codeunit::"E-Doc. Deferral Matching", EDocumentPurchaseLine) then; + end; + end; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocProcCustomizations.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocProcCustomizations.Enum.al index e533729eed..6120760440 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocProcCustomizations.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/EDocProcCustomizations.Enum.al @@ -11,16 +11,18 @@ enum 6110 "E-Doc. Proc. Customizations" implements IPurchaseOrderProvider, IPurchaseLineProvider, IUnitOfMeasureProvider, - IEDocumentCreatePurchaseInvoice + IEDocumentCreatePurchaseInvoice, + IEDocumentCreatePurchaseCreditMemo { Extensible = true; DefaultImplementation = IVendorProvider = "E-Doc. Providers", IPurchaseOrderProvider = "E-Doc. Providers", IPurchaseLineProvider = "E-Doc. Providers", IUnitOfMeasureProvider = "E-Doc. Providers", - IEDocumentCreatePurchaseInvoice = "E-Doc. Create Purchase Invoice"; + IEDocumentCreatePurchaseInvoice = "E-Doc. Create Purchase Invoice", + IEDocumentCreatePurchaseCreditMemo = "E-Doc. Create Purch. Cr. Memo"; value(0; Default) { } -} \ No newline at end of file +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/PreparePurchaseEDocDraft.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/PreparePurchaseEDocDraft.Codeunit.al index 7997ca6d44..5fc3e99889 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/PreparePurchaseEDocDraft.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/PrepareDraft/PreparePurchaseEDocDraft.Codeunit.al @@ -5,179 +5,34 @@ namespace Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument; -using Microsoft.eServices.EDocument.Processing.AI; -using Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.eServices.EDocument.Processing.Interfaces; -using Microsoft.Foundation.UOM; -using Microsoft.Purchases.Document; using Microsoft.Purchases.Vendor; -using System.Log; codeunit 6125 "Prepare Purchase E-Doc. Draft" implements IProcessStructuredData { Access = Internal; var - EDocImpSessionTelemetry: Codeunit "E-Doc. Imp. Session Telemetry"; + PrepareDraftHelper: Codeunit "EDoc Prepare Purch. Draft"; procedure PrepareDraft(EDocument: Record "E-Document"; EDocImportParameters: Record "E-Doc. Import Parameters"): Enum "E-Document Type" - var - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - UnitOfMeasure: Record "Unit of Measure"; - Vendor: Record Vendor; - PurchaseOrder: Record "Purchase Header"; - EDocVendorAssignmentHistory: Record "E-Doc. Vendor Assign. History"; - EDocPurchaseHistMapping: Codeunit "E-Doc. Purchase Hist. Mapping"; - EDocActivityLogSession: Codeunit "E-Doc. Activity Log Session"; - IUnitOfMeasureProvider: Interface IUnitOfMeasureProvider; - IPurchaseLineProvider: Interface IPurchaseLineProvider; - IPurchaseOrderProvider: Interface IPurchaseOrderProvider; begin - IUnitOfMeasureProvider := EDocImportParameters."Processing Customizations"; - IPurchaseLineProvider := EDocImportParameters."Processing Customizations"; - IPurchaseOrderProvider := EDocImportParameters."Processing Customizations"; - - if EDocActivityLogSession.CreateSession() then; - - EDocumentPurchaseHeader.GetFromEDocument(EDocument); - EDocumentPurchaseHeader.TestField("E-Document Entry No."); - if EDocumentPurchaseHeader."[BC] Vendor No." = '' then begin - Vendor := GetVendor(EDocument, EDocImportParameters."Processing Customizations"); - EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; - end; - - PurchaseOrder := IPurchaseOrderProvider.GetPurchaseOrder(EDocumentPurchaseHeader); - if PurchaseOrder."No." <> '' then begin - // Matching purchase order specified in the E-Document - EDocumentPurchaseHeader."[BC] Purchase Order No." := PurchaseOrder."No."; - EDocumentPurchaseHeader.Modify(); - end; - if EDocPurchaseHistMapping.FindRelatedPurchaseHeaderInHistory(EDocument, EDocVendorAssignmentHistory) then - EDocPurchaseHistMapping.UpdateMissingHeaderValuesFromHistory(EDocVendorAssignmentHistory, EDocumentPurchaseHeader); - EDocumentPurchaseHeader.Modify(); - - // If we can't find a vendor - EDocImpSessionTelemetry.SetBool('Vendor', EDocumentPurchaseHeader."[BC] Vendor No." <> ''); - if EDocumentPurchaseHeader."[BC] Vendor No." <> '' then begin - - // Get all purchase lines for the document - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); - - // Apply basic unit of measure and text-to-account resolution first - if EDocumentPurchaseLine.FindSet() then - repeat - UnitOfMeasure := IUnitOfMeasureProvider.GetUnitOfMeasure(EDocument, EDocumentPurchaseLine."Line No.", EDocumentPurchaseLine."Unit of Measure"); - EDocumentPurchaseLine."[BC] Unit of Measure" := UnitOfMeasure.Code; - IPurchaseLineProvider.GetPurchaseLine(EDocumentPurchaseLine); - EDocumentPurchaseLine.Modify(); - until EDocumentPurchaseLine.Next() = 0; - - // Apply all Copilot-powered matching techniques to the lines - CopilotLineMatching(EDocument."Entry No"); - end; - - // Log telemetry and activity sessions - Clear(EDocumentPurchaseLine); - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); - if EDocumentPurchaseLine.FindSet() then - repeat - EDocImpSessionTelemetry.SetLine(EDocumentPurchaseLine.SystemId); - until EDocumentPurchaseLine.Next() = 0; - - // Log all accumulated activity session changes at the end - LogAllActivitySessionChanges(EDocActivityLogSession); - - if EDocActivityLogSession.EndSession() then; + PrepareDraftHelper.PrepareDraft(EDocument, EDocImportParameters); exit("E-Document Type"::"Purchase Invoice"); end; - local procedure LogAllActivitySessionChanges(EDocActivityLogSession: Codeunit "E-Doc. Activity Log Session") - begin - Log(EDocActivityLogSession, EDocActivityLogSession.AccountNumberTok()); - Log(EDocActivityLogSession, EDocActivityLogSession.DeferralTok()); - Log(EDocActivityLogSession, EDocActivityLogSession.ItemRefTok()); - Log(EDocActivityLogSession, EDocActivityLogSession.TextToAccountMappingTok()); - end; - - local procedure Log(EDocActivityLogSession: Codeunit "E-Doc. Activity Log Session"; ActivityLogName: Text) - var - ActivityLog: Codeunit "Activity Log Builder"; - ActivityLogList: List of [Codeunit "Activity Log Builder"]; - Found: Boolean; - begin - Clear(ActivityLogList); - EDocActivityLogSession.GetAll(ActivityLogName, ActivityLogList, Found); - foreach ActivityLog in ActivityLogList do - ActivityLog.Log(); - end; - - local procedure CopilotLineMatching(EDocumentEntryNo: Integer) - var - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - begin - EDocumentPurchaseLine.SetLoadFields("E-Document Entry No.", "[BC] Purchase Type No.", "[BC] Deferral Code"); - EDocumentPurchaseLine.ReadIsolation(IsolationLevel::ReadCommitted); - - // Step 1: Apply historical pattern matching - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); - EDocumentPurchaseLine.SetRange("[BC] Purchase Type No.", ''); - EDocumentPurchaseLine.SetRange("[BC] Item Reference No.", ''); - - if not EDocumentPurchaseLine.IsEmpty() then begin - Commit(); - Codeunit.Run(Codeunit::"E-Doc. Historical Matching", EDocumentPurchaseLine); - end; - - // Step 2: Apply line-to-account matching for remaining lines with no purchase type - Clear(EDocumentPurchaseLine); - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); - EDocumentPurchaseLine.SetRange("[BC] Purchase Type No.", ''); - EDocumentPurchaseLine.SetRange("[BC] Item Reference No.", ''); - if not EDocumentPurchaseLine.IsEmpty() then begin - Commit(); - Codeunit.Run(Codeunit::"E-Doc. GL Account Matching", EDocumentPurchaseLine); - end; - - // Step 3: Apply deferral matching for lines with a purchase type but no deferral code - Clear(EDocumentPurchaseLine); - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); - EDocumentPurchaseLine.SetRange("[BC] Deferral Code", ''); - EDocumentPurchaseLine.SetRange("[BC] Item Reference No.", ''); - if not EDocumentPurchaseLine.IsEmpty() then begin - Commit(); - if Codeunit.Run(Codeunit::"E-Doc. Deferral Matching", EDocumentPurchaseLine) then; - end; - end; - procedure OpenDraftPage(var EDocument: Record "E-Document") - var - EDocumentPurchaseDraft: Page "E-Document Purchase Draft"; begin - EDocumentPurchaseDraft.Editable(true); - EDocumentPurchaseDraft.SetRecord(EDocument); - EDocumentPurchaseDraft.Run(); + PrepareDraftHelper.OpenDraftPage(EDocument); end; procedure CleanUpDraft(EDocument: Record "E-Document") - var - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine: Record "E-Document Purchase Line"; begin - EDocumentPurchaseHeader.SetRange("E-Document Entry No.", EDocument."Entry No"); - if not EDocumentPurchaseHeader.IsEmpty() then - EDocumentPurchaseHeader.DeleteAll(true); - - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); - if not EDocumentPurchaseLine.IsEmpty() then - EDocumentPurchaseLine.DeleteAll(true); + PrepareDraftHelper.CleanUpDraft(EDocument); end; procedure GetVendor(EDocument: Record "E-Document"; Customizations: Enum "E-Doc. Proc. Customizations") Vendor: Record Vendor - var - IVendorProvider: Interface IVendorProvider; begin - IVendorProvider := Customizations; - Vendor := IVendorProvider.GetVendor(EDocument); + Vendor := PrepareDraftHelper.GetVendor(EDocument, Customizations); end; -} \ No newline at end of file +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al index 581ed8ed08..a49e0ab6aa 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocPurchaseDraftSubform.Page.al @@ -55,6 +55,11 @@ page 6183 "E-Doc. Purchase Draft Subform" Visible = HasEDocumentOrderMatchWarnings; StyleExpr = MatchWarningsStyleExpr; ToolTip = 'Specifies any warnings related to matching this line to a purchase order line.'; + + trigger OnDrillDown() + begin + ShowMatchWarningDetails(); + end; } field("Line Type"; Rec."[BC] Purchase Line Type") { @@ -323,7 +328,7 @@ page 6183 "E-Doc. Purchase Draft Subform" var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; - EDocumentPOMatchWarnings: Record "E-Doc PO Match Warning"; + TempEDocumentPOMatchWarnings: Record "E-Doc PO Match Warning"; EDocPurchaseHistMapping: Codeunit "E-Doc. Purchase Hist. Mapping"; EDocPOMatching: Codeunit "E-Doc. PO Matching"; AdditionalColumns, OrderMatchedCaption, MatchWarningsCaption, MatchWarningsStyleExpr : Text; @@ -348,11 +353,6 @@ page 6183 "E-Doc. Purchase Draft Subform" end; trigger OnAfterGetRecord() - var - MissingInfoLbl: Label 'Missing information for match'; - NotYetReceivedLbl: Label 'Not yet received'; - QuantityMismatchLbl: Label 'Quantity mismatch'; - NoWarningsLbl: Label 'No warnings'; begin if EDocumentPurchaseLine.Get(Rec."E-Document Entry No.", Rec."Line No.") then; AdditionalColumns := Rec.AdditionalColumnsDisplayText(); @@ -361,21 +361,7 @@ page 6183 "E-Doc. Purchase Draft Subform" IsLineMatchedToOrderLine := EDocPOMatching.IsEDocumentLineMatchedToAnyPOLine(EDocumentPurchaseLine); IsLineMatchedToReceiptLine := EDocPOMatching.IsEDocumentLineMatchedToAnyReceiptLine(EDocumentPurchaseLine); OrderMatchedCaption := IsLineMatchedToOrderLine ? GetSummaryOfMatchedOrders() : ''; - MatchWarningsStyleExpr := 'None'; - EDocumentPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", Rec.SystemId); - if EDocumentPOMatchWarnings.FindFirst() then begin - case EDocumentPOMatchWarnings."Warning Type" of - Enum::"E-Doc PO Match Warning"::MissingInformationForMatch: - MatchWarningsCaption := MissingInfoLbl; - Enum::"E-Doc PO Match Warning"::NotYetReceived: - MatchWarningsCaption := NotYetReceivedLbl; - Enum::"E-Doc PO Match Warning"::QuantityMismatch: - MatchWarningsCaption := QuantityMismatchLbl; - end; - MatchWarningsStyleExpr := 'Ambiguous'; - end - else - MatchWarningsCaption := NoWarningsLbl; + UpdateMatchWarnings(); end; internal procedure SetEDocumentPurchaseHeader(EDocPurchHeader: Record "E-Document Purchase Header") @@ -470,8 +456,8 @@ page 6183 "E-Doc. Purchase Draft Subform" local procedure UpdatePOMatching() begin IsEDocumentMatchedToAnyPOLine := EDocPOMatching.IsEDocumentMatchedToAnyPOLine(EDocumentPurchaseHeader); - EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, EDocumentPOMatchWarnings); - HasEDocumentOrderMatchWarnings := not EDocumentPOMatchWarnings.IsEmpty(); + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempEDocumentPOMatchWarnings); + HasEDocumentOrderMatchWarnings := not TempEDocumentPOMatchWarnings.IsEmpty(); end; local procedure GetSummaryOfMatchedOrders(): Text @@ -498,4 +484,85 @@ page 6183 "E-Doc. Purchase Draft Subform" exit(StrSubstNo(MatchedToSingleOrderMultipleLinesLbl, MatchedPO)); end; + local procedure UpdateMatchWarnings() + var + MissingInfoLbl: Label 'Unit of measure information is missing'; + ExceedsInvoiceableQtyLbl: Label 'Exceeds quantity received'; + ExceedsRemainingToInvoiceLbl: Label 'Exceeds remaining to invoice'; + OverReceiptLbl: Label 'Over-receipt'; + NoWarningsLbl: Label 'No warnings'; + MultipleWarningsLbl: Label 'Multiple warnings'; + MostSevereStyle: Text; + SeverityLevel: Integer; + CurrentSeverity: Integer; + begin + MatchWarningsCaption := NoWarningsLbl; + MatchWarningsStyleExpr := 'None'; + + TempEDocumentPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", Rec.SystemId); + + // Severity: Unfavorable (critical) > Ambiguous (warning) > Subordinate (info) + SeverityLevel := 0; + if TempEDocumentPOMatchWarnings.FindSet() then + repeat + case TempEDocumentPOMatchWarnings."Warning Type" of + Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty: + begin + CurrentSeverity := 3; + MatchWarningsCaption := ExceedsInvoiceableQtyLbl; + MostSevereStyle := 'Unfavorable'; + end; + Enum::"E-Doc PO Match Warning"::MissingInformationForMatch: + begin + CurrentSeverity := 3; + MatchWarningsCaption := MissingInfoLbl; + MostSevereStyle := 'Unfavorable'; + end; + Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice: + begin + CurrentSeverity := 2; + MatchWarningsCaption := ExceedsRemainingToInvoiceLbl; + MostSevereStyle := 'Ambiguous'; + end; + Enum::"E-Doc PO Match Warning"::OverReceipt: + begin + CurrentSeverity := 1; + MatchWarningsCaption := OverReceiptLbl; + MostSevereStyle := 'Subordinate'; + end; + end; + if CurrentSeverity > SeverityLevel then begin + SeverityLevel := CurrentSeverity; + MatchWarningsStyleExpr := MostSevereStyle; + end; + until TempEDocumentPOMatchWarnings.Next() = 0; + + if TempEDocumentPOMatchWarnings.Count() > 1 then + MatchWarningsCaption := MultipleWarningsLbl; + end; + + local procedure ShowMatchWarningDetails() + var + WarningDetails: TextBuilder; + MissingInfoDetailLbl: Label 'Quantity information for this line is missing to complete the match. Verify that the draft line has a unit of measure assigned for this item.'; + begin + TempEDocumentPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", Rec.SystemId); + if not TempEDocumentPOMatchWarnings.FindSet() then + exit; + + repeat + case TempEDocumentPOMatchWarnings."Warning Type" of + Enum::"E-Doc PO Match Warning"::MissingInformationForMatch: + WarningDetails.AppendLine('• ' + MissingInfoDetailLbl); + Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty, + Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice, + Enum::"E-Doc PO Match Warning"::OverReceipt: + WarningDetails.AppendLine('• ' + TempEDocumentPOMatchWarnings."Warning Message"); + end; + until TempEDocumentPOMatchWarnings.Next() = 0; + + if WarningDetails.Length() > 0 then + Message(WarningDetails.ToText()); + end; + } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al index 35fa9c3c8c..db638982ec 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseDraft.Page.al @@ -154,6 +154,26 @@ page 6181 "E-Document Purchase Draft" ToolTip = 'Specifies the extracted due date.'; Editable = true; + trigger OnValidate() + begin + EDocumentPurchaseHeader.Modify(); + CurrPage.Update(); + end; + } + field("Vendor Invoice No."; EDocumentPurchaseHeader."Vendor Invoice No.") + { + Caption = 'Vendor Invoice No.'; + ToolTip = 'Specifies the vendor''s invoice number referenced in the credit memo billing reference.'; + Visible = IsCreditMemo; + Editable = false; + } + field("Applies-to Doc. No."; EDocumentPurchaseHeader."Applies-to Doc. No.") + { + Caption = 'Applies-to Doc. No.'; + ToolTip = 'Specifies the posted purchase invoice number in Business Central that this credit memo applies to.'; + Visible = IsCreditMemo; + Editable = PageEditable; + trigger OnValidate() begin EDocumentPurchaseHeader.Modify(); @@ -297,7 +317,7 @@ page 6181 "E-Document Purchase Draft" trigger OnAction() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; begin Session.LogMessage('0000PCO', FinalizeDraftInvokedTxt, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', EDocumentPurchaseHeader.FeatureName()); Rec.SetAutoCalcFields("Import Processing Status"); @@ -306,7 +326,7 @@ page 6181 "E-Document Purchase Draft" Rec.ShowRecord(); exit; end; - FinalizeEDocument(EDocImportParameters); + FinalizeEDocument(TempEDocImportParameters); end; } action(ResetDraftDocument) @@ -502,6 +522,7 @@ page 6181 "E-Document Purchase Draft" HasErrorsOrWarnings := false; HasErrors := false; PageEditable := IsEditable(); + IsCreditMemo := Rec."Document Type" = Enum::"E-Document Type"::"Purchase Credit Memo"; EDocumentNotification.SendPurchaseDocumentDraftNotifications(Rec."Entry No"); if Rec."Entry No" <> 0 then @@ -533,6 +554,7 @@ page 6181 "E-Document Purchase Draft" (Rec.Status = Enum::"E-Document Status"::Error); PageEditable := IsEditable(); + IsCreditMemo := Rec."Document Type" = Enum::"E-Document Type"::"Purchase Credit Memo"; end; local procedure SetPageCaption() @@ -641,7 +663,7 @@ page 6181 "E-Document Purchase Draft" local procedure ResetDraft() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; ConfirmDialogMgt: Codeunit "Confirm Management"; Progress: Dialog; @@ -654,10 +676,10 @@ page 6181 "E-Document Purchase Draft" Progress.Open(ProcessingDocumentMsg); // Regardless of document state, we re-run the read data into IR, then prepare draft step. - EDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Read into Draft"; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); - EDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); + TempEDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Read into Draft"; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); + TempEDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); Rec.Get(Rec."Entry No"); if GuiAllowed() then @@ -666,7 +688,7 @@ page 6181 "E-Document Purchase Draft" local procedure PrepareDraft() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; EDocumentHelper: Codeunit "E-Document Helper"; Progress: Dialog; @@ -676,8 +698,8 @@ page 6181 "E-Document Purchase Draft" if GuiAllowed() then Progress.Open(ProcessingDocumentMsg); - EDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); + TempEDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); Rec.Get(Rec."Entry No"); if GuiAllowed() then @@ -686,7 +708,7 @@ page 6181 "E-Document Purchase Draft" local procedure AnalyzeEDocument() var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; Progress: Dialog; begin @@ -696,10 +718,10 @@ page 6181 "E-Document Purchase Draft" Progress.Open(ProcessingDocumentMsg); // Regardless of document state, we re-run the structure received data, then prepare draft step. - EDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Structure received data"; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); - EDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(Rec, EDocImportParameters); + TempEDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Structure received data"; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); + TempEDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(Rec, TempEDocImportParameters); Rec.Get(Rec."Entry No"); if GuiAllowed() then @@ -729,7 +751,7 @@ page 6181 "E-Document Purchase Draft" local procedure DoLinkToExistingDocument() var PurchaseHeader: Record "Purchase Header"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; ConfirmDialogMgt: Codeunit "Confirm Management"; LinkToExistingDocumentQst: Label 'Do you want to link this e-document to %1 %2?', Comment = '%1 = Document Type, %2 = Document No.'; RelinkToExistingDocumentQst: Label 'This e-document is already linked to a document. Linking to %1 %2 will unlink the currently linked document. You will need to manually clean up that document. Do you want to continue?', Comment = '%1 = Document Type, %2 = Document No.'; @@ -745,8 +767,8 @@ page 6181 "E-Document Purchase Draft" if not ConfirmDialogMgt.GetResponseOrDefault(ConfirmQst, Rec.Status <> Rec.Status::Processed) then exit; - EDocImportParameters."Existing Doc. RecordId" := PurchaseHeader.RecordId(); - FinalizeEDocument(EDocImportParameters); + TempEDocImportParameters."Existing Doc. RecordId" := PurchaseHeader.RecordId(); + FinalizeEDocument(TempEDocImportParameters); end; var @@ -760,9 +782,9 @@ page 6181 "E-Document Purchase Draft" HasErrorsOrWarnings, HasErrors : Boolean; ShowFinalizeDraftAction: Boolean; ShowAnalyzeDocumentAction: Boolean; - FinalizeDraftInvokedTxt: Label 'User invoked Finalize Draft action.'; - FinalizeDraftPerformedTxt: Label 'User completed Finalize Draft action.'; + FinalizeDraftInvokedTxt: Label 'User invoked Finalize Draft action.', Locked = true; + FinalizeDraftPerformedTxt: Label 'User completed Finalize Draft action.', Locked = true; ProcessingDocumentMsg: Label 'Processing document...'; ResetDraftQst: Label 'All the changes that you may have made on the document draft will be lost. Do you want to continue?'; - PageEditable, HasPDFSource : Boolean; + PageEditable, HasPDFSource, IsCreditMemo : Boolean; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseHeader.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseHeader.Table.al index 4735a1a3d6..67dcfbdc46 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseHeader.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseHeader.Table.al @@ -224,6 +224,16 @@ table 6100 "E-Document Purchase Header" Caption = 'Posting Description'; DataClassification = CustomerContent; } + field(39; "Applies-to Doc. No."; Text[100]) + { + Caption = 'Applies-to Doc. No.'; + DataClassification = CustomerContent; + } + field(40; "Vendor Invoice No."; Text[100]) + { + Caption = 'Vendor Invoice No.'; + DataClassification = CustomerContent; + } #endregion Purchase fields #region Business Central Data - Validated fields [101-200] @@ -287,6 +297,6 @@ table 6100 "E-Document Purchase Header" var FeatureTelemetry: Codeunit "Feature Telemetry"; - DeleteDraftPerformedTxt: Label 'User deleted the draft.'; + DeleteDraftPerformedTxt: Label 'User deleted the draft.', Locked = true; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseLine.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseLine.Table.al index b197a68cb4..df4c126db9 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseLine.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/EDocumentPurchaseLine.Table.al @@ -329,12 +329,14 @@ table 6101 "E-Document Purchase Line" var OldDimSetID: Integer; begin - OldDimSetID := "[BC] Dimension Set ID"; - "[BC] Dimension Set ID" := DimMgt.EditDimensionSet( - Rec, "[BC] Dimension Set ID", StrSubstNo('%1 %2', "E-Document Entry No.", "Line No."), - "[BC] Shortcut Dimension 1 Code", "[BC] Shortcut Dimension 2 Code"); - DimMgt.UpdateGlobalDimFromDimSetID("[BC] Dimension Set ID", "[BC] Shortcut Dimension 1 Code", "[BC] Shortcut Dimension 2 Code"); - exit(OldDimSetID <> "[BC] Dimension Set ID"); + OldDimSetID := Rec."[BC] Dimension Set ID"; + Rec."[BC] Dimension Set ID" := DimMgt.EditDimensionSet( + Rec, Rec."[BC] Dimension Set ID", StrSubstNo('%1 %2', Rec."E-Document Entry No.", Rec."Line No."), + Rec."[BC] Shortcut Dimension 1 Code", Rec."[BC] Shortcut Dimension 2 Code"); + DimMgt.UpdateGlobalDimFromDimSetID(Rec."[BC] Dimension Set ID", Rec."[BC] Shortcut Dimension 1 Code", Rec."[BC] Shortcut Dimension 2 Code"); + if OldDimSetID <> Rec."[BC] Dimension Set ID" then + Rec.Modify(); + exit(OldDimSetID <> Rec."[BC] Dimension Set ID"); end; internal procedure GetFromLinkedPurchaseLine(PurchaseLine: Record "Purchase Line"): Boolean diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/History/EDocPurchaseHistMapping.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/History/EDocPurchaseHistMapping.Codeunit.al index 32c26f0673..8b2e8ffbdb 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/History/EDocPurchaseHistMapping.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/History/EDocPurchaseHistMapping.Codeunit.al @@ -159,7 +159,7 @@ codeunit 6120 "E-Doc. Purchase Hist. Mapping" if UnitOfMeasure.Get(PurchInvLine."Unit of Measure") then // we only assign if it's a valid unit of measure EDocumentPurchaseLine."[BC] Unit of Measure" := CopyStr(PurchInvLine."Unit of Measure", 1, MaxStrLen(EDocumentPurchaseLine."[BC] Unit of Measure")); - if (EDocumentPurchaseLine."[BC] Purchase Line Type" = "Purchase Line Type"::" ") and (EDocumentPurchaseLine."[BC] Purchase Type No." = '') then begin + if EDocumentPurchaseLine."[BC] Purchase Type No." = '' then begin // We first check if the purchase invoice line came from an allocation account line // If so, we set the account type and number explictly since the type and number of the line has changed if not IsNullGuid(PurchInvLine."Alloc. Purch. Line SystemId") then begin diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Enum.al index 813c0e7e8e..c763fcde25 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Enum.al @@ -16,4 +16,13 @@ enum 6111 "E-Doc PO Match Warning" value(2; MissingInformationForMatch) { } + value(3; ExceedsInvoiceableQty) + { + } + value(4; ExceedsRemainingToInvoice) + { + } + value(5; OverReceipt) + { + } } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Table.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Table.al index 8a62481cd4..36bf07065d 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Table.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatchWarning.Table.al @@ -25,6 +25,12 @@ table 6115 "E-Doc PO Match Warning" Caption = 'Warning Type'; Editable = false; } + field(3; "Warning Message"; Text[250]) + { + DataClassification = SystemMetadata; + Caption = 'Warning Message'; + Editable = false; + } } keys { diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al index 250f44236d..1aae56fe46 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/Purchase/PurchaseOrderMatching/EDocPOMatching.Codeunit.al @@ -213,6 +213,10 @@ codeunit 6196 "E-Doc. PO Matching" TempPurchaseLine: Record "Purchase Line" temporary; EDocLineQuantity: Decimal; PurchaseLinesQuantity, PurchaseLinesQuantityInvoiced, PurchaseLinesQuantityReceived : Decimal; + RemainingToInvoice, InvoiceableQty : Decimal; + ExceedsInvoiceableQtyLbl: Label 'Invoice quantity (%1) exceeds what can be invoiced according to what has been received (%2) by %3. The order line has to be received before invoicing.', Comment = '%1 = Invoice qty, %2 = Invoiceable qty, %3 = Difference'; + ExceedsRemainingToInvoiceLbl: Label 'Invoice quantity (%1) exceeds what is missing to invoice from the order (%2) by %3.', Comment = '%1 = Invoice qty, %2 = Remaining to invoice, %3 = Difference'; + OverReceiptLbl: Label 'Invoice will close out order but there is an over-receipt of %1 units.', Comment = '%1 = Over-receipt quantity'; begin LoadPOLinesMatchedToEDocumentLine(EDocumentPurchaseLine, TempPurchaseLine); PurchaseLinesQuantityInvoiced := 0; @@ -232,17 +236,37 @@ codeunit 6196 "E-Doc. PO Matching" POMatchWarnings.Insert(); exit; end; - if EDocLineQuantity <> PurchaseLinesQuantity - PurchaseLinesQuantityInvoiced then begin - POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; - POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::QuantityMismatch; - POMatchWarnings.Insert(); - end; - if (EDocLineQuantity + PurchaseLinesQuantityInvoiced) > PurchaseLinesQuantityReceived then + + // I = Invoice quantity (from the e-document line) + // R = Remaining to invoice on the PO (Ordered - Previously Invoiced) + // J = Invoiceable quantity (Received - Previously Invoiced) + RemainingToInvoice := PurchaseLinesQuantity - PurchaseLinesQuantityInvoiced; + InvoiceableQty := PurchaseLinesQuantityReceived - PurchaseLinesQuantityInvoiced; + + // I > J: Invoice exceeds what has been received and not yet invoiced + if EDocLineQuantity > InvoiceableQty then if ShouldWarnIfNotYetReceived(EDocumentPurchaseLine.GetBCVendor()."No.") then begin POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; - POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::NotYetReceived; + POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::ExceedsInvoiceableQty; + POMatchWarnings."Warning Message" := CopyStr(StrSubstNo(ExceedsInvoiceableQtyLbl, EDocLineQuantity, InvoiceableQty, EDocLineQuantity - InvoiceableQty), 1, MaxStrLen(POMatchWarnings."Warning Message")); POMatchWarnings.Insert(); end; + + // I > R: Invoice exceeds what remains on the order + if EDocLineQuantity > RemainingToInvoice then begin + POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; + POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::ExceedsRemainingToInvoice; + POMatchWarnings."Warning Message" := CopyStr(StrSubstNo(ExceedsRemainingToInvoiceLbl, EDocLineQuantity, RemainingToInvoice, EDocLineQuantity - RemainingToInvoice), 1, MaxStrLen(POMatchWarnings."Warning Message")); + POMatchWarnings.Insert(); + end; + + // I = R and I < J: Order will be closed but there is an over-receipt + if (EDocLineQuantity = RemainingToInvoice) and (EDocLineQuantity < InvoiceableQty) then begin + POMatchWarnings."E-Doc. Purchase Line SystemId" := EDocumentPurchaseLine.SystemId; + POMatchWarnings."Warning Type" := "E-Doc PO Match Warning"::OverReceipt; + POMatchWarnings."Warning Message" := CopyStr(StrSubstNo(OverReceiptLbl, InvoiceableQty - RemainingToInvoice), 1, MaxStrLen(POMatchWarnings."Warning Message")); + POMatchWarnings.Insert(); + end; end; /// @@ -475,7 +499,7 @@ codeunit 6196 "E-Doc. PO Matching" EDocumentPurchaseLine."[BC] Unit of Measure" := MatchedUnitOfMeasure; EDocumentPurchaseLine.Modify(); AppendPOMatchWarnings(EDocumentPurchaseLine, TempMatchWarnings); - TempMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::NotYetReceived); + TempMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::ExceedsInvoiceableQty); if (not TempMatchWarnings.IsEmpty) and (not CanMatchInvoiceLineToPOLineWithoutReceipt(EDocumentPurchaseLine, PurchaseLine)) then Error(NotYetReceivedErr); end; @@ -763,8 +787,8 @@ codeunit 6196 "E-Doc. PO Matching" TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::NotYetReceived); - // For each line that has a Not Yet Received warning, we check if it can be matched without receipt + TempPOMatchWarnings.SetRange("Warning Type", "E-Doc PO Match Warning"::ExceedsInvoiceableQty); + // For each line that exceeds invoiceable qty, we check if it can be matched without receipt if TempPOMatchWarnings.FindSet() then repeat EDocumentPurchaseLine.GetBySystemId(TempPOMatchWarnings."E-Doc. Purchase Line SystemId"); diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al index 6cbb0a83a6..194b0d9adf 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDoc.Enum.al @@ -26,4 +26,9 @@ enum 6103 "Structure Received E-Doc." implements IStructureReceivedEDocument Caption = 'Azure Document Intelligence'; Implementation = IStructureReceivedEDocument = "E-Document ADI Handler"; } + value(3; "MLLM") + { + Caption = 'MLLM Extraction'; + Implementation = IStructureReceivedEDocument = "E-Document MLLM Handler"; + } } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMSchemaHelper.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMSchemaHelper.Codeunit.al new file mode 100644 index 0000000000..93468d3d62 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocMLLMSchemaHelper.Codeunit.al @@ -0,0 +1,281 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Import; + +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Foundation.Company; + +codeunit 6232 "E-Doc. MLLM Schema Helper" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure GetDefaultSchema() Value: Text + var + DefaultSchemaFileLbl: Label 'AITools/ubl_example.json', Locked = true; + SchemaJson: JsonObject; + begin + SchemaJson := NavApp.GetResourceAsJson(DefaultSchemaFileLbl, TextEncoding::UTF8); + PrefillCustomerFromCompanyInfo(SchemaJson); + SchemaJson.WriteTo(Value); + end; + + local procedure PrefillCustomerFromCompanyInfo(var SchemaJson: JsonObject) + var + CompanyInformation: Record "Company Information"; + CustomerPartyObj: JsonObject; + PartyObj: JsonObject; + PartyNameObj: JsonObject; + PostalAddressObj: JsonObject; + CountryObj: JsonObject; + TaxSchemeObj: JsonObject; + PartyTaxSchemeObj: JsonObject; + begin + if not CompanyInformation.Get() then + exit; + + PartyNameObj.Add('name', CompanyInformation.Name); + + CountryObj.Add('identification_code', CompanyInformation."Country/Region Code"); + PostalAddressObj.Add('street_name', CompanyInformation.Address); + PostalAddressObj.Add('additional_street_name', CompanyInformation."Address 2"); + PostalAddressObj.Add('city_name', CompanyInformation.City); + PostalAddressObj.Add('postal_zone', CompanyInformation."Post Code"); + PostalAddressObj.Add('country', CountryObj); + + TaxSchemeObj.Add('id', 'VAT'); + PartyTaxSchemeObj.Add('company_id', CompanyInformation."VAT Registration No."); + PartyTaxSchemeObj.Add('tax_scheme', TaxSchemeObj); + + PartyObj.Add('party_name', PartyNameObj); + PartyObj.Add('postal_address', PostalAddressObj); + PartyObj.Add('party_tax_scheme', PartyTaxSchemeObj); + + CustomerPartyObj.Add('party', PartyObj); + SchemaJson.Replace('accounting_customer_party', CustomerPartyObj); + end; + +#pragma warning disable AA0139 + procedure MapHeaderFromJson(HeaderObj: JsonObject; var TempHeader: Record "E-Document Purchase Header" temporary) + var + NestedObj: JsonObject; + NestedObj2: JsonObject; + NestedObj3: JsonObject; + CurrencyText: Text; + begin + GetString(HeaderObj, 'id', MaxStrLen(TempHeader."Sales Invoice No."), TempHeader."Sales Invoice No."); + GetDate(HeaderObj, 'issue_date', TempHeader."Document Date"); + GetDate(HeaderObj, 'due_date', TempHeader."Due Date"); + + GetString(HeaderObj, 'document_currency_code', MaxStrLen(TempHeader."Currency Code"), CurrencyText); + SetCurrencyCode(CurrencyText, TempHeader."Currency Code"); + + if GetNestedObject(HeaderObj, 'order_reference', NestedObj) then + GetString(NestedObj, 'id', MaxStrLen(TempHeader."Purchase Order No."), TempHeader."Purchase Order No."); + + if GetNestedObject(HeaderObj, 'payment_terms', NestedObj) then + GetString(NestedObj, 'note', MaxStrLen(TempHeader."Payment Terms"), TempHeader."Payment Terms"); + + if GetNestedObject(HeaderObj, 'accounting_supplier_party', NestedObj) then + if GetNestedObject(NestedObj, 'party', NestedObj2) then begin + if GetNestedObject(NestedObj2, 'party_name', NestedObj3) then + GetString(NestedObj3, 'name', MaxStrLen(TempHeader."Vendor Company Name"), TempHeader."Vendor Company Name"); + if GetNestedObject(NestedObj2, 'postal_address', NestedObj3) then + BuildAddress(NestedObj3, MaxStrLen(TempHeader."Vendor Address"), TempHeader."Vendor Address"); + if GetNestedObject(NestedObj2, 'party_tax_scheme', NestedObj3) then + GetString(NestedObj3, 'company_id', MaxStrLen(TempHeader."Vendor VAT Id"), TempHeader."Vendor VAT Id"); + if GetNestedObject(NestedObj2, 'contact', NestedObj3) then + GetString(NestedObj3, 'name', MaxStrLen(TempHeader."Vendor Contact Name"), TempHeader."Vendor Contact Name"); + end; + + if GetNestedObject(HeaderObj, 'accounting_customer_party', NestedObj) then + if GetNestedObject(NestedObj, 'party', NestedObj2) then begin + if GetNestedObject(NestedObj2, 'party_name', NestedObj3) then + GetString(NestedObj3, 'name', MaxStrLen(TempHeader."Customer Company Name"), TempHeader."Customer Company Name"); + if GetNestedObject(NestedObj2, 'postal_address', NestedObj3) then + BuildAddress(NestedObj3, MaxStrLen(TempHeader."Customer Address"), TempHeader."Customer Address"); + if GetNestedObject(NestedObj2, 'party_tax_scheme', NestedObj3) then + GetString(NestedObj3, 'company_id', MaxStrLen(TempHeader."Customer VAT Id"), TempHeader."Customer VAT Id"); + end; + + if GetNestedObject(HeaderObj, 'delivery', NestedObj) then begin + if GetNestedObject(NestedObj, 'delivery_location', NestedObj2) then + if GetNestedObject(NestedObj2, 'address', NestedObj3) then + BuildAddress(NestedObj3, MaxStrLen(TempHeader."Shipping Address"), TempHeader."Shipping Address"); + if GetNestedObject(NestedObj, 'delivery_party', NestedObj2) then + if GetNestedObject(NestedObj2, 'party_name', NestedObj3) then + GetString(NestedObj3, 'name', MaxStrLen(TempHeader."Shipping Address Recipient"), TempHeader."Shipping Address Recipient"); + end; + + if GetNestedObject(HeaderObj, 'payment_means', NestedObj) then + if GetNestedObject(NestedObj, 'payee_financial_account', NestedObj2) then + GetString(NestedObj2, 'name', MaxStrLen(TempHeader."Remittance Address Recipient"), TempHeader."Remittance Address Recipient"); + + if GetNestedObject(HeaderObj, 'tax_total', NestedObj) then + GetDecimal(NestedObj, 'tax_amount', TempHeader."Total VAT"); + + if GetNestedObject(HeaderObj, 'legal_monetary_total', NestedObj) then begin + GetDecimal(NestedObj, 'tax_exclusive_amount', TempHeader."Sub Total"); + GetDecimal(NestedObj, 'allowance_total_amount', TempHeader."Total Discount"); + GetDecimal(NestedObj, 'payable_amount', TempHeader.Total); + GetDecimal(NestedObj, 'payable_amount', TempHeader."Amount Due"); + end; + end; + + procedure MapLinesFromJson(LinesArray: JsonArray; EDocEntryNo: Integer; var TempLine: Record "E-Document Purchase Line" temporary) + var + LineToken: JsonToken; + LineObj: JsonObject; + NestedObj: JsonObject; + NestedObj2: JsonObject; + LineNumber: Integer; + begin + TempLine.DeleteAll(); + + for LineNumber := 0 to LinesArray.Count() - 1 do + if LinesArray.Get(LineNumber, LineToken) then begin + Clear(TempLine); + TempLine."E-Document Entry No." := EDocEntryNo; + TempLine."Line No." := 10000 + (LineNumber * 10000); + + LineObj := LineToken.AsObject(); + + if GetNestedObject(LineObj, 'item', NestedObj) then begin + GetString(NestedObj, 'name', MaxStrLen(TempLine.Description), TempLine.Description); + if GetNestedObject(NestedObj, 'sellers_item_identification', NestedObj2) then + GetString(NestedObj2, 'id', MaxStrLen(TempLine."Product Code"), TempLine."Product Code"); + if GetNestedObject(NestedObj, 'classified_tax_category', NestedObj2) then + GetDecimal(NestedObj2, 'percent', TempLine."VAT Rate"); + end; + + if GetNestedObject(LineObj, 'invoiced_quantity', NestedObj) then begin + GetDecimal(NestedObj, 'value', TempLine.Quantity); + GetString(NestedObj, 'unit_code', MaxStrLen(TempLine."Unit of Measure"), TempLine."Unit of Measure"); + end; + if TempLine.Quantity <= 0 then + TempLine.Quantity := 1; + + if GetNestedObject(LineObj, 'price', NestedObj) then + GetDecimal(NestedObj, 'price_amount', TempLine."Unit Price"); + + GetDecimal(LineObj, 'line_extension_amount', TempLine."Sub Total"); + + if GetNestedObject(LineObj, 'allowance_charge', NestedObj) then + if GetNestedObject(NestedObj, 'amount', NestedObj2) then + GetDecimal(NestedObj2, 'value', TempLine."Total Discount"); + + TempLine.Insert(); + end; + end; +#pragma warning restore AA0139 + + local procedure GetString(JsonObj: JsonObject; PropertyName: Text; MaxLen: Integer; var FieldValue: Text) + var + JsonToken: JsonToken; + TextValue: Text; + begin + if not JsonObj.Get(PropertyName, JsonToken) then + exit; + if JsonToken.AsValue().IsNull() then + exit; + TextValue := JsonToken.AsValue().AsText(); + if StrLen(TextValue) > MaxLen then + TextValue := CopyStr(TextValue, 1, MaxLen); + FieldValue := TextValue; + end; + + local procedure GetDate(JsonObj: JsonObject; PropertyName: Text; var FieldValue: Date) + var + JsonToken: JsonToken; + DateText: Text; + DateValue: Date; + begin + if not JsonObj.Get(PropertyName, JsonToken) then + exit; + if JsonToken.AsValue().IsNull() then + exit; + DateText := JsonToken.AsValue().AsText(); + if DateText = '' then + exit; + if Evaluate(DateValue, DateText, 9) then + FieldValue := DateValue; + end; + + local procedure GetDecimal(JsonObj: JsonObject; PropertyName: Text; var FieldValue: Decimal) + var + JsonToken: JsonToken; + begin + if not JsonObj.Get(PropertyName, JsonToken) then + exit; + if JsonToken.AsValue().IsNull() then + exit; + FieldValue := JsonToken.AsValue().AsDecimal(); + end; + + local procedure GetNestedObject(JsonObj: JsonObject; PropertyName: Text; var NestedObj: JsonObject): Boolean + var + JsonToken: JsonToken; + begin + if not JsonObj.Get(PropertyName, JsonToken) then + exit(false); + if not JsonToken.IsObject() then + exit(false); + NestedObj := JsonToken.AsObject(); + exit(true); + end; + + local procedure BuildAddress(PostalAddressObj: JsonObject; MaxLen: Integer; var FieldValue: Text) + var + CountryObj: JsonObject; + Street: Text; + AdditionalStreet: Text; + City: Text; + PostalZone: Text; + CountryCode: Text; + Address: Text; + begin + GetString(PostalAddressObj, 'street_name', 250, Street); + GetString(PostalAddressObj, 'additional_street_name', 250, AdditionalStreet); + GetString(PostalAddressObj, 'city_name', 250, City); + GetString(PostalAddressObj, 'postal_zone', 250, PostalZone); + if GetNestedObject(PostalAddressObj, 'country', CountryObj) then + GetString(CountryObj, 'identification_code', 250, CountryCode); + + Address := Street; + AppendToAddress(Address, AdditionalStreet, ', '); + AppendToAddress(Address, City, ', '); + AppendToAddress(Address, PostalZone, ' '); + AppendToAddress(Address, CountryCode, ', '); + + if StrLen(Address) > MaxLen then + Address := CopyStr(Address, 1, MaxLen); + FieldValue := Address; + end; + + local procedure AppendToAddress(var Address: Text; Part: Text; Separator: Text) + begin + if Part = '' then + exit; + if Address <> '' then + Address += Separator; + Address += Part; + end; + + local procedure SetCurrencyCode(CurrencyText: Text; var CurrencyCode: Code[10]) + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + if CurrencyText = '' then + exit; + + GeneralLedgerSetup.Get(); + if UpperCase(CurrencyText) = GeneralLedgerSetup."LCY Code" then + exit; + + CurrencyCode := CopyStr(UpperCase(CurrencyText), 1, MaxStrLen(CurrencyCode)); + end; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocPurchaseDraftUtility.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocPurchaseDraftUtility.Codeunit.al new file mode 100644 index 0000000000..70c9f96a07 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocPurchaseDraftUtility.Codeunit.al @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Import; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; + +codeunit 6234 "E-Doc. Purchase Draft Utility" +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + procedure PersistDraft(EDocument: Record "E-Document"; var TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary; var TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + // Clean up old data, since we are re-reading data + EDocumentPurchaseHeader.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocumentPurchaseHeader.DeleteAll(); + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocumentPurchaseLine.DeleteAll(); + + EDocumentPurchaseHeader := TempEDocPurchaseHeader; + EDocumentPurchaseHeader."E-Document Entry No." := EDocument."Entry No"; + EDocumentPurchaseHeader.Insert(); + OnInsertedEDocumentPurchaseHeader(EDocument, EDocumentPurchaseHeader); + + if TempEDocPurchaseLine.FindSet() then begin + repeat + EDocumentPurchaseLine := TempEDocPurchaseLine; + EDocumentPurchaseLine."E-Document Entry No." := EDocument."Entry No"; + EDocumentPurchaseLine."Line No." := EDocumentPurchaseLine.GetNextLineNo(EDocument."Entry No"); + EDocumentPurchaseLine.Insert(); + until TempEDocPurchaseLine.Next() = 0; + + OnInsertedEDocumentPurchaseLines(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine); + end; + end; + + [InternalEvent(false, false)] + local procedure OnInsertedEDocumentPurchaseHeader(EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header") + begin + end; + + [InternalEvent(false, false)] + local procedure OnInsertedEDocumentPurchaseLines(EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line") + begin + end; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentADIHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentADIHandler.Codeunit.al index c7a2b995ac..52ca55a954 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentADIHandler.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentADIHandler.Codeunit.al @@ -17,6 +17,8 @@ using System.Utilities; codeunit 6174 "E-Document ADI Handler" implements IStructureReceivedEDocument, IStructuredFormatReader, IStructuredDataType { Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; var EDocumentJsonHelper: Codeunit "EDocument Json Helper"; @@ -73,33 +75,11 @@ codeunit 6174 "E-Document ADI Handler" implements IStructureReceivedEDocument, I var TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary; TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary; - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine: Record "E-Document Purchase Line"; + EDocPurchaseDraftUtility: Codeunit "E-Doc. Purchase Draft Utility"; begin - // Clean up old data, since we are re-reading data - EDocumentPurchaseHeader.SetRange("E-Document Entry No.", EDocument."Entry No"); - EDocumentPurchaseHeader.DeleteAll(); - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); - EDocumentPurchaseLine.DeleteAll(); - ReadIntoBuffer(EDocument, TempBlob, TempEDocPurchaseHeader, TempEDocPurchaseLine); - EDocumentPurchaseHeader := TempEDocPurchaseHeader; - EDocumentPurchaseHeader."E-Document Entry No." := EDocument."Entry No"; - EDocumentPurchaseHeader.Insert(); - OnInsertedEDocumentPurchaseHeader(EDocument, EDocumentPurchaseHeader); - - if TempEDocPurchaseLine.FindSet() then begin - repeat - EDocumentPurchaseLine := TempEDocPurchaseLine; - EDocumentPurchaseLine."E-Document Entry No." := EDocument."Entry No"; - EDocumentPurchaseLine."Line No." := EDocumentPurchaseLine.GetNextLineNo(EDocument."Entry No"); - EDocumentPurchaseLine.Insert(); - until TempEDocPurchaseLine.Next() = 0; - - OnInsertedEDocumentPurchaseLines(EDocument, EDocumentPurchaseHeader, EDocumentPurchaseLine); - end; - - exit(Enum::"E-Doc. Process Draft"::"Purchase Document"); + EDocPurchaseDraftUtility.PersistDraft(EDocument, TempEDocPurchaseHeader, TempEDocPurchaseLine); + exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice"); end; local procedure ReadIntoBuffer( @@ -207,14 +187,4 @@ codeunit 6174 "E-Document ADI Handler" implements IStructureReceivedEDocument, I TempEDocPurchaseLine."Total Discount" := (TempEDocPurchaseLine."Unit Price" * TempEDocPurchaseLine.Quantity) - TempEDocPurchaseLine."Sub Total"; end; #pragma warning restore AA0139 - - [InternalEvent(false, false)] - local procedure OnInsertedEDocumentPurchaseHeader(EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header") - begin - end; - - [InternalEvent(false, false)] - local procedure OnInsertedEDocumentPurchaseLines(EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line") - begin - end; } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandler.Codeunit.al new file mode 100644 index 0000000000..58003701f8 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentMLLMHandler.Codeunit.al @@ -0,0 +1,294 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Format; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Import; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.eServices.EDocument.Processing.Interfaces; +using System.AI; +using System.Telemetry; +using System.Text; +using System.Utilities; + +codeunit 6231 "E-Document MLLM Handler" implements IStructureReceivedEDocument, IStructuredFormatReader, IStructuredDataType +{ + Access = Internal; + InherentEntitlements = X; + InherentPermissions = X; + + var + Telemetry: Codeunit Telemetry; + StructuredData: Text; + FileFormat: Enum "E-Doc. File Format"; + FeatureNameLbl: Label 'E-Document MLLM Extraction', Locked = true; + FileDataLbl: Label 'data:application/pdf;base64,%1', Locked = true; + SystemPromptResourceTok: Label 'Prompts/EDocMLLMExtraction-SystemPrompt.md', Locked = true; + UserPromptLbl: Label 'Extract invoice data into this UBL JSON structure: %1. \n\nExtract ONLY visible values. Return JSON only.', Locked = true; + MLLMExtractionStartedMsg: Label 'MLLM extraction started.', Locked = true; + MLLMExtractionSucceededMsg: Label 'MLLM extraction succeeded.', Locked = true; + MLLMApiCallSucceededMsg: Label 'MLLM API call succeeded.', Locked = true; + MLLMApiCallFailedMsg: Label 'MLLM API call failed, falling back to ADI.', Locked = true; + MLLMEmptyResponseMsg: Label 'MLLM returned empty response, falling back to ADI.', Locked = true; + MLLMJsonParseFailedMsg: Label 'MLLM response is not valid JSON, falling back to ADI.', Locked = true; + MLLMSchemaValidationFailedMsg: Label 'MLLM response missing required vendor fields (name or address), falling back to ADI.', Locked = true; + ADIFallbackSucceededMsg: Label 'ADI fallback produced structured data.', Locked = true; + ADIFallbackFailedMsg: Label 'ADI fallback returned empty result.', Locked = true; + + procedure StructureReceivedEDocument(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType + var + ResponseJson: JsonObject; + CustomDimensions: Dictionary of [Text, Text]; + ResponseText: Text; + begin + Telemetry.LogMessage('0000SGQ', MLLMExtractionStartedMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, GetCustomDimensions()); + + RegisterCopilotCapabilityIfNeeded(); + + ResponseText := CallMLLM(EDocumentDataStorage); + + if not ValidateAndUnwrapResponse(ResponseText, ResponseJson) then + exit(FallbackToADI(EDocumentDataStorage)); + + StructuredData := ResponseText; + FileFormat := "E-Doc. File Format"::JSON; + + CustomDimensions := GetCustomDimensions(); + CustomDimensions.Add('LineCount', Format(GetInvoiceLineCount(ResponseJson))); + Telemetry.LogMessage('0000SGR', MLLMExtractionSucceededMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, CustomDimensions); + + exit(this); + end; + + local procedure CallMLLM(EDocumentDataStorage: Record "E-Doc. Data Storage"): Text + var + Base64Convert: Codeunit "Base64 Convert"; + AzureOpenAI: Codeunit "Azure OpenAI"; + AOAIChatMessages: Codeunit "AOAI Chat Messages"; + AOAIChatCompletionParams: Codeunit "AOAI Chat Completion Params"; + AOAIUserMessage: Codeunit "AOAI User Message"; + AOAIOperationResponse: Codeunit "AOAI Operation Response"; + AOAIDeployments: Codeunit "AOAI Deployments"; + EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper"; + FromTempBlob: Codeunit "Temp Blob"; + CustomDimensions: Dictionary of [Text, Text]; + InStream: InStream; + Base64Data: Text; + StartTime: DateTime; + DurationMs: Integer; + begin + // Load schema and convert PDF to base64 + FromTempBlob := EDocumentDataStorage.GetTempBlob(); + FromTempBlob.CreateInStream(InStream, TextEncoding::UTF8); + Base64Data := Base64Convert.ToBase64(InStream); + + // Build AOAI call + AzureOpenAI.SetAuthorization(Enum::"AOAI Model Type"::"Chat Completions", AOAIDeployments.GetGPT41MiniPreview()); + AzureOpenAI.SetCopilotCapability(Enum::"Copilot Capability"::"E-Document MLLM Analysis"); + + AOAIChatCompletionParams.SetTemperature(0); + AOAIChatCompletionParams.SetJsonMode(true); + + AOAIChatMessages.SetPrimarySystemMessage(NavApp.GetResourceAsText(SystemPromptResourceTok, TextEncoding::UTF8)); + + AOAIUserMessage.AddFilePart(StrSubstNo(FileDataLbl, Base64Data)); + AOAIUserMessage.AddTextPart(StrSubstNo(UserPromptLbl, EDocMLLMSchemaHelper.GetDefaultSchema())); + AOAIChatMessages.AddUserMessage(AOAIUserMessage); + + StartTime := CurrentDateTime(); + AzureOpenAI.GenerateChatCompletion(AOAIChatMessages, AOAIChatCompletionParams, AOAIOperationResponse); + DurationMs := CurrentDateTime() - StartTime; + + CustomDimensions := GetCustomDimensions(); + CustomDimensions.Add('DurationMs', Format(DurationMs)); + + if not AOAIOperationResponse.IsSuccess() then begin + Telemetry.LogMessage('0000SGS', MLLMApiCallFailedMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, CustomDimensions); + exit(''); + end; + + Telemetry.LogMessage('0000SGT', MLLMApiCallSucceededMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, CustomDimensions); + exit(AOAIOperationResponse.GetResult()); + end; + + local procedure ValidateAndUnwrapResponse(var ResponseText: Text; var ResponseJson: JsonObject): Boolean + var + ContentToken: JsonToken; + begin + if ResponseText = '' then begin + Telemetry.LogMessage('0000SGU', MLLMEmptyResponseMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, GetCustomDimensions()); + exit(false); + end; + + if not ResponseJson.ReadFrom(ResponseText) then begin + Telemetry.LogMessage('0000SGV', MLLMJsonParseFailedMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, GetCustomDimensions()); + exit(false); + end; + + // Unwrap 'content' wrapper if AOAI wrapped the response + if ResponseJson.Get('content', ContentToken) then begin + ResponseText := ContentToken.AsValue().AsText(); + if not ResponseJson.ReadFrom(ResponseText) then begin + Telemetry.LogMessage('0000SGW', MLLMJsonParseFailedMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, GetCustomDimensions()); + exit(false); + end; + end; + + if not ValidateMLLMResponse(ResponseJson) then begin + Telemetry.LogMessage('0000SGX', MLLMSchemaValidationFailedMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, GetCustomDimensions()); + exit(false); + end; + + exit(true); + end; + + local procedure FallbackToADI(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType + var + ADIHandler: Codeunit "E-Document ADI Handler"; + ADIResult: Interface IStructuredDataType; + begin + ADIResult := ADIHandler.StructureReceivedEDocument(EDocumentDataStorage); + + if ADIResult.GetContent() <> '' then + Telemetry.LogMessage('0000SGY', ADIFallbackSucceededMsg, Verbosity::Normal, DataClassification::SystemMetadata, TelemetryScope::All, GetCustomDimensions()) + else + Telemetry.LogMessage('0000SGZ', ADIFallbackFailedMsg, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, GetCustomDimensions()); + + exit(ADIResult); + end; + + local procedure ValidateMLLMResponse(ResponseJson: JsonObject): Boolean + var + SupplierToken: JsonToken; + PartyToken: JsonToken; + NameToken: JsonToken; + AddressToken: JsonToken; + SupplierObj: JsonObject; + PartyObj: JsonObject; + NameObj: JsonObject; + VendorName: Text; + begin + if not ResponseJson.Get('accounting_supplier_party', SupplierToken) then + exit(false); + if not SupplierToken.IsObject() then + exit(false); + SupplierObj := SupplierToken.AsObject(); + + if not SupplierObj.Get('party', PartyToken) then + exit(false); + if not PartyToken.IsObject() then + exit(false); + PartyObj := PartyToken.AsObject(); + + if not PartyObj.Get('party_name', NameToken) then + exit(false); + if not NameToken.IsObject() then + exit(false); + NameObj := NameToken.AsObject(); + + if not NameObj.Get('name', NameToken) then + exit(false); + VendorName := NameToken.AsValue().AsText(); + if VendorName = '' then + exit(false); + + if not PartyObj.Get('postal_address', AddressToken) then + exit(false); + if not AddressToken.IsObject() then + exit(false); + + exit(true); + end; + + local procedure GetInvoiceLineCount(ResponseJson: JsonObject): Integer + var + LinesToken: JsonToken; + begin + if ResponseJson.Get('invoice_line', LinesToken) then + if LinesToken.IsArray() then + exit(LinesToken.AsArray().Count()); + exit(0); + end; + + procedure GetFileFormat(): Enum "E-Doc. File Format" + begin + exit(this.FileFormat); + end; + + procedure GetContent(): Text + begin + exit(this.StructuredData); + end; + + procedure GetReadIntoDraftImpl(): Enum "E-Doc. Read into Draft" + begin + exit("E-Doc. Read into Draft"::MLLM); + end; + + procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft" + var + TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary; + TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary; + EDocPurchaseDraftUtility: Codeunit "E-Doc. Purchase Draft Utility"; + begin + ReadIntoBuffer(EDocument, TempBlob, TempEDocPurchaseHeader, TempEDocPurchaseLine); + EDocPurchaseDraftUtility.PersistDraft(EDocument, TempEDocPurchaseHeader, TempEDocPurchaseLine); + exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice"); + end; + + local procedure ReadIntoBuffer( + EDocument: Record "E-Document"; + TempBlob: Codeunit "Temp Blob"; + var TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary; + var TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary) + var + EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper"; + InStream: InStream; + SourceJsonObject: JsonObject; + LinesToken: JsonToken; + LinesArray: JsonArray; + BlobAsText: Text; + begin + TempBlob.CreateInStream(InStream, TextEncoding::UTF8); + InStream.Read(BlobAsText); + SourceJsonObject.ReadFrom(BlobAsText); + + EDocMLLMSchemaHelper.MapHeaderFromJson(SourceJsonObject, TempEDocPurchaseHeader); + TempEDocPurchaseHeader."E-Document Entry No." := EDocument."Entry No"; + + if SourceJsonObject.Get('invoice_line', LinesToken) then + if LinesToken.IsArray() then begin + LinesArray := LinesToken.AsArray(); + EDocMLLMSchemaHelper.MapLinesFromJson(LinesArray, EDocument."Entry No", TempEDocPurchaseLine); + end; + end; + + procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") + var + TempEDocPurchaseHeader: Record "E-Document Purchase Header" temporary; + TempEDocPurchaseLine: Record "E-Document Purchase Line" temporary; + EDocReadablePurchaseDoc: Page "E-Doc. Readable Purchase Doc."; + begin + ReadIntoBuffer(EDocument, TempBlob, TempEDocPurchaseHeader, TempEDocPurchaseLine); + EDocReadablePurchaseDoc.SetBuffer(TempEDocPurchaseHeader, TempEDocPurchaseLine); + EDocReadablePurchaseDoc.Run(); + end; + + local procedure GetCustomDimensions(): Dictionary of [Text, Text] + var + CustomDimensions: Dictionary of [Text, Text]; + begin + CustomDimensions.Add('Category', FeatureNameLbl); + exit(CustomDimensions); + end; + + procedure RegisterCopilotCapabilityIfNeeded() + var + CopilotCapability: Codeunit "Copilot Capability"; + begin + if not CopilotCapability.IsCapabilityRegistered(Enum::"Copilot Capability"::"E-Document MLLM Analysis") then + CopilotCapability.RegisterCapability(Enum::"Copilot Capability"::"E-Document MLLM Analysis", ''); + end; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al index 7f7bbeb459..f9af9884ce 100644 --- a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLHandler.Codeunit.al @@ -8,66 +8,129 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.eServices.EDocument.Processing.Interfaces; -using Microsoft.Finance.GeneralLedger.Setup; using System.Utilities; +/// +/// Reads PEPPOL BIS 3.0 Invoice and CreditNote XML into v2 import draft staging tables. +/// This codeunit orchestrates *what* to parse and in what order. +/// Reusable extraction logic lives in "E-Document PEPPOL Utility". +/// Spec reference: https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-invoice/tree/ +/// https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-creditnote/tree/ +/// codeunit 6173 "E-Document PEPPOL Handler" implements IStructuredFormatReader { Access = Internal; InherentEntitlements = X; InherentPermissions = X; + var + PeppolUtility: Codeunit "E-Document PEPPOL Utility"; + BillingReferenceEmptyTelemetryTxt: Label 'CreditNote BillingReference is empty - no originating invoice reference found.', Locked = true; + procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft" var EDocumentPurchaseHeader: Record "E-Document Purchase Header"; DocStream: InStream; PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; - XmlElement: XmlElement; - CommonAggregateComponentsLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'; - CommonBasicComponentsLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'; - DocumentationLbl: Label 'urn:un:unece:uncefact:documentation:2'; - QualifiedDatatypesLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2'; - UnqualifiedDataTypesSchemaModuleLbl: Label 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2'; - DefaultInvoiceLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'; - CreditNoteNotSupportedLbl: Label 'Credit notes are not supported'; + RootElement: XmlElement; begin EDocumentPurchaseHeader.InsertForEDocument(EDocument); TempBlob.CreateInStream(DocStream, TextEncoding::UTF8); XmlDocument.ReadFrom(DocStream, PeppolXML); - XmlNamespaces.AddNamespace('cac', CommonAggregateComponentsLbl); - XmlNamespaces.AddNamespace('cbc', CommonBasicComponentsLbl); - XmlNamespaces.AddNamespace('ccts', DocumentationLbl); - XmlNamespaces.AddNamespace('qdt', QualifiedDatatypesLbl); - XmlNamespaces.AddNamespace('udt', UnqualifiedDataTypesSchemaModuleLbl); - XmlNamespaces.AddNamespace('inv', DefaultInvoiceLbl); - - PeppolXML.GetRoot(XmlElement); - case UpperCase(XmlElement.LocalName()) of + PeppolUtility.InitializePEPPOL3Namespaces(XmlNamespaces); + + PeppolXML.GetRoot(RootElement); + case UpperCase(RootElement.LocalName()) of 'INVOICE': begin - PopulatePurchaseInvoiceHeader(PeppolXML, XmlNamespaces, EDocumentPurchaseHeader); - InsertPurchaseInvoiceLines(PeppolXML, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No."); + PopulateInvoiceHeader(PeppolXML, XmlNamespaces, EDocumentPurchaseHeader); + InsertPurchaseLines(PeppolXML, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", '/inv:Invoice/cac:InvoiceLine', 'cac:InvoiceLine', 'cbc:InvoicedQuantity'); + InsertAllowanceChargeLines(PeppolXML, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", '/inv:Invoice'); + InsertDocumentAttachments(EDocument, PeppolXML, XmlNamespaces, '/inv:Invoice'); end; 'CREDITNOTE': - Error(CreditNoteNotSupportedLbl); + begin + PopulateCreditMemoHeader(PeppolXML, XmlNamespaces, EDocumentPurchaseHeader); + InsertPurchaseLines(PeppolXML, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", '/cre:CreditNote/cac:CreditNoteLine', 'cac:CreditNoteLine', 'cbc:CreditedQuantity'); + InsertAllowanceChargeLines(PeppolXML, XmlNamespaces, EDocumentPurchaseHeader."E-Document Entry No.", '/cre:CreditNote'); + InsertDocumentAttachments(EDocument, PeppolXML, XmlNamespaces, '/cre:CreditNote'); + end; end; EDocumentPurchaseHeader.Modify(); EDocument.Direction := EDocument.Direction::Incoming; - exit(Enum::"E-Doc. Process Draft"::"Purchase Document"); + + case UpperCase(RootElement.LocalName()) of + 'INVOICE': + exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice"); + 'CREDITNOTE': + exit(Enum::"E-Doc. Process Draft"::"Purchase Credit Memo"); + else + exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice"); + end; + end; + + #region Header Orchestration + + local procedure PopulateInvoiceHeader(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var Header: Record "E-Document Purchase Header") + begin + PopulateInvoiceDocumentInfo(PeppolXML, XmlNamespaces, Header); + PeppolUtility.PopulateSupplierInfo(PeppolXML, XmlNamespaces, '/inv:Invoice', Header); + PeppolUtility.PopulateCustomerInfo(PeppolXML, XmlNamespaces, '/inv:Invoice', Header); + // Per PEPPOL BIS 3.0: Invoice has DueDate as a direct child element + PeppolUtility.PopulateAmountsAndDates(PeppolXML, XmlNamespaces, '/inv:Invoice', '/inv:Invoice/cbc:DueDate', Header); + PeppolUtility.PopulateCurrency(PeppolXML, XmlNamespaces, '/inv:Invoice', Header); + end; + + local procedure PopulateCreditMemoHeader(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var Header: Record "E-Document Purchase Header") + begin + PopulateCreditNoteDocumentInfo(PeppolXML, XmlNamespaces, Header); + PeppolUtility.PopulateSupplierInfo(PeppolXML, XmlNamespaces, '/cre:CreditNote', Header); + PeppolUtility.PopulateCustomerInfo(PeppolXML, XmlNamespaces, '/cre:CreditNote', Header); + // Per PEPPOL BIS 3.0: CreditNote has no top-level DueDate; it is under PaymentMeans. + // Spec ref: https://docs.peppol.eu/poacc/billing/3.0/syntax/ubl-creditnote/cac-PaymentMeans/cbc-PaymentDueDate/ + PeppolUtility.PopulateAmountsAndDates(PeppolXML, XmlNamespaces, '/cre:CreditNote', '/cre:CreditNote/cac:PaymentMeans/cbc:PaymentDueDate', Header); + PeppolUtility.PopulateCurrency(PeppolXML, XmlNamespaces, '/cre:CreditNote', Header); + end; + + local procedure PopulateInvoiceDocumentInfo(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var Header: Record "E-Document Purchase Header") + var + Value: Text; + begin + if PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/inv:Invoice/cbc:ID', Value) then + Header."Sales Invoice No." := CopyStr(Value, 1, MaxStrLen(Header."Sales Invoice No.")); + if PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/inv:Invoice/cac:OrderReference/cbc:ID', Value) then + Header."Purchase Order No." := CopyStr(Value, 1, MaxStrLen(Header."Purchase Order No.")); + end; + + local procedure PopulateCreditNoteDocumentInfo(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var Header: Record "E-Document Purchase Header") + var + Value: Text; + begin + if PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/cre:CreditNote/cbc:ID', Value) then + Header."Sales Invoice No." := CopyStr(Value, 1, MaxStrLen(Header."Sales Invoice No.")); + if PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/cre:CreditNote/cac:OrderReference/cbc:ID', Value) then + Header."Purchase Order No." := CopyStr(Value, 1, MaxStrLen(Header."Purchase Order No.")); + if PeppolUtility.TryGetStringValue(PeppolXML, XmlNamespaces, '/cre:CreditNote/cac:BillingReference/cac:InvoiceDocumentReference/cbc:ID', Value) then + Header."Vendor Invoice No." := CopyStr(Value, 1, MaxStrLen(Header."Vendor Invoice No.")); + if Header."Vendor Invoice No." = '' then + Session.LogMessage('0000SNJ', BillingReferenceEmptyTelemetryTxt, Verbosity::Warning, DataClassification::SystemMetadata, TelemetryScope::All, 'Category', 'E-Document'); end; - local procedure InsertPurchaseInvoiceLines(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; EDocumentEntryNo: Integer) + #endregion Header Orchestration + + #region Line Orchestration + + local procedure InsertPurchaseLines(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; EDocumentEntryNo: Integer; LineXPath: Text; LineElementName: Text; QuantityElementName: Text) var EDocumentPurchaseLine: Record "E-Document Purchase Line"; NewLineXML: XmlDocument; LineXMLList: XmlNodeList; LineXMLNode: XmlNode; i: Integer; - InvoiceLinePathLbl: Label '/inv:Invoice/cac:InvoiceLine'; begin - if not PeppolXML.SelectNodes(InvoiceLinePathLbl, XmlNamespaces, LineXMLList) then + if not PeppolXML.SelectNodes(LineXPath, XmlNamespaces, LineXMLList) then exit; for i := 1 to LineXMLList.Count do begin @@ -76,131 +139,85 @@ codeunit 6173 "E-Document PEPPOL Handler" implements IStructuredFormatReader EDocumentPurchaseLine."Line No." := EDocumentPurchaseLine.GetNextLineNo(EDocumentEntryNo); LineXMLList.Get(i, LineXMLNode); NewLineXML.ReplaceNodes(LineXMLNode); - PopulateEDocumentPurchaseLine(NewLineXML, XmlNamespaces, EDocumentPurchaseLine); + PeppolUtility.PopulatePurchaseLine(NewLineXML, XmlNamespaces, EDocumentPurchaseLine, LineElementName, QuantityElementName); EDocumentPurchaseLine.Insert(); end; end; -#pragma warning disable AA0139 // false positive: overflow handled by SetStringValueInField - local procedure PopulatePurchaseInvoiceHeader(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseHeader: Record "E-Document Purchase Header") + local procedure InsertAllowanceChargeLines(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; EDocumentEntryNo: Integer; RootPath: Text) var - XMLNode: XmlNode; - begin - SetStringValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Sales Invoice No."), EDocumentPurchaseHeader."Sales Invoice No."); - SetStringValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:OrderReference/cbc:ID', MaxStrLen(EDocumentPurchaseHeader."Purchase Order No."), EDocumentPurchaseHeader."Purchase Order No."); - SetStringValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name', MaxStrLen(EDocumentPurchaseHeader."Vendor Company Name"), EDocumentPurchaseHeader."Vendor Company Name"); - // Line below, using PayeeParty, shall be used when the Payee is different from the Seller. Otherwise, it will not be shown in the XML. - SetStringValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:PayeeParty/cac:PartyName/cbc:Name', MaxStrLen(EDocumentPurchaseHeader."Vendor Company Name"), EDocumentPurchaseHeader."Vendor Company Name"); - SetNumberValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount', EDocumentPurchaseHeader."Total Discount"); - SetNumberValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:LegalMonetaryTotal/cbc:PayableAmount', EDocumentPurchaseHeader."Amount Due"); - SetNumberValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:LegalMonetaryTotal/cbc:PayableAmount', EDocumentPurchaseHeader.Total); - SetStringValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:Name', MaxStrLen(EDocumentPurchaseHeader."Vendor Contact Name"), EDocumentPurchaseHeader."Vendor Contact Name"); - SetStringValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName', MaxStrLen(EDocumentPurchaseHeader."Vendor Address"), EDocumentPurchaseHeader."Vendor Address"); - SetStringValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID', MaxStrLen(EDocumentPurchaseHeader."Customer VAT Id"), EDocumentPurchaseHeader."Customer VAT Id"); - // Line below, using PayeeParty, shall be used when the Payee is different from the Seller. Otherwise, it will not be shown in the XML. - SetStringValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:PayeeParty/cac:PartyLegalEntity/cbc:CompanyID', MaxStrLen(EDocumentPurchaseHeader."Vendor VAT Id"), EDocumentPurchaseHeader."Vendor VAT Id"); - SetNumberValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount', EDocumentPurchaseHeader."Sub Total"); - EDocumentPurchaseHeader."Total VAT" := EDocumentPurchaseHeader."Total" - EDocumentPurchaseHeader."Sub Total" - EDocumentPurchaseHeader."Total Discount"; - SetDateValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cbc:DueDate', EDocumentPurchaseHeader."Due Date"); - SetDateValueInField(PeppolXML, XMLNamespaces, '/inv:Invoice/cbc:IssueDate', EDocumentPurchaseHeader."Document Date"); - SetStringValueInField(PeppolXML, XmlNamespaces, '/inv:Invoice/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', MaxStrLen(EDocumentPurchaseHeader."Vendor VAT Id"), EDocumentPurchaseHeader."Vendor VAT Id"); - SetCurrencyValueInField(PeppolXML, XmlNamespaces, '/inv:Invoice/cbc:DocumentCurrencyCode', MaxStrLen(EDocumentPurchaseHeader."Currency Code"), EDocumentPurchaseHeader."Currency Code"); - SetStringValueInField(PeppolXML, XmlNamespaces, '/inv:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name', MaxStrLen(EDocumentPurchaseHeader."Customer Company Name"), EDocumentPurchaseHeader."Customer Company Name"); - SetStringValueInField(PeppolXML, XmlNamespaces, '/inv:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', MaxStrLen(EDocumentPurchaseHeader."Customer VAT Id"), EDocumentPurchaseHeader."Customer VAT Id"); - SetStringValueInField(PeppolXML, XmlNamespaces, '/inv:Invoice/cac:AccountingCustomerParty/cac:Party/cac:PostalAddress/cbc:StreetName', MaxStrLen(EDocumentPurchaseHeader."Customer Address"), EDocumentPurchaseHeader."Customer Address"); - SetStringValueInField(PeppolXML, XmlNamespaces, '/inv:Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID', MaxStrLen(EDocumentPurchaseHeader."Customer GLN"), EDocumentPurchaseHeader."Customer GLN"); - SetStringValueInField(PeppolXML, XmlNamespaces, '/inv:Invoice/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID', MaxStrLen(EDocumentPurchaseHeader."Customer Company Id"), EDocumentPurchaseHeader."Customer Company Id"); - - if PeppolXML.SelectSingleNode('/inv:Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID', XmlNamespaces, XMLNode) then - if XMLNode.AsXmlAttribute().Value() = '0088' then // GLN - SetStringValueInField(PeppolXML, XmlNamespaces, '/inv:Invoice/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID', MaxStrLen(EDocumentPurchaseHeader."Vendor GLN"), EDocumentPurchaseHeader."Vendor GLN"); - end; - - local procedure PopulateEDocumentPurchaseLine(LineXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var EDocumentPurchaseLine: Record "E-Document Purchase Line") - begin - SetNumberValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cbc:InvoicedQuantity', EDocumentPurchaseLine.Quantity); - SetStringValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cbc:InvoicedQuantity/@unitCode', MaxStrLen(EDocumentPurchaseLine."Unit of Measure"), EDocumentPurchaseLine."Unit of Measure"); - SetNumberValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cbc:LineExtensionAmount', EDocumentPurchaseLine."Sub Total"); - SetCurrencyValueInField(LineXML, XmlNamespaces, 'cac:InvoiceLine/cbc:LineExtensionAmount/@currencyID', MaxStrLen(EDocumentPurchaseLine."Currency Code"), EDocumentPurchaseLine."Currency Code"); - SetNumberValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cac:AllowanceCharge/cbc:Amount', EDocumentPurchaseLine."Total Discount"); - SetStringValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cbc:Note', MaxStrLen(EDocumentPurchaseLine.Description), EDocumentPurchaseLine.Description); - SetStringValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cac:Item/cbc:Name', MaxStrLen(EDocumentPurchaseLine.Description), EDocumentPurchaseLine.Description); - SetStringValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cac:Item/cbc:Description', MaxStrLen(EDocumentPurchaseLine.Description), EDocumentPurchaseLine.Description); - SetStringValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cac:Item/cac:SellersItemIdentification/cbc:ID', MaxStrLen(EDocumentPurchaseLine."Product Code"), EDocumentPurchaseLine."Product Code"); - SetStringValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cac:Item/cac:StandardItemIdentification/cbc:ID', MaxStrLen(EDocumentPurchaseLine."Product Code"), EDocumentPurchaseLine."Product Code"); - SetNumberValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cac:Item/cac:ClassifiedTaxCategory/cbc:Percent', EDocumentPurchaseLine."VAT Rate"); - SetNumberValueInField(LineXML, XMLNamespaces, 'cac:InvoiceLine/cac:Price/cbc:PriceAmount', EDocumentPurchaseLine."Unit Price"); - end; - - local procedure SetCurrencyValueInField(XMLDocument: XmlDocument; XMLNamespaces: XmlNamespaceManager; Path: Text; MaxLength: Integer; var CurrencyField: Code[10]) - var - GLSetup: Record "General Ledger Setup"; - XMLNode: XmlNode; - CurrencyCode: Code[10]; + ChargeXML: XmlDocument; + ChargeNodes: XmlNodeList; + ChargeNode: XmlNode; + ChargeIndicator: Text; + i: Integer; begin - if not XMLDocument.SelectSingleNode(Path, XMLNamespaces, XMLNode) then + if not PeppolXML.SelectNodes(RootPath + '/cac:AllowanceCharge', XmlNamespaces, ChargeNodes) then exit; - GLSetup.Get(); - - if XMLNode.IsXmlElement() then begin - CurrencyCode := CopyStr(XMLNode.AsXmlElement().InnerText(), 1, MaxLength); - if GLSetup."LCY Code" <> CurrencyCode then - CurrencyField := CurrencyCode; - exit; - end; + for i := 1 to ChargeNodes.Count do begin + ChargeNodes.Get(i, ChargeNode); + ChargeXML.ReplaceNodes(ChargeNode); - if XMLNode.IsXmlAttribute() then begin - CurrencyCode := CopyStr(XMLNode.AsXmlAttribute().Value, 1, MaxLength); - if GLSetup."LCY Code" <> CurrencyCode then - CurrencyField := CurrencyCode; - exit; + if PeppolUtility.TryGetStringValue(ChargeXML, XmlNamespaces, 'cac:AllowanceCharge/cbc:ChargeIndicator', ChargeIndicator) then + if UpperCase(ChargeIndicator) = 'TRUE' then + InsertSingleChargeLine(ChargeXML, XmlNamespaces, EDocumentEntryNo); end; end; -#pragma warning restore AA0139 - local procedure SetStringValueInField(XMLDocument: XmlDocument; XMLNamespaces: XmlNamespaceManager; Path: Text; MaxLength: Integer; var Field: Text) + local procedure InsertSingleChargeLine(ChargeXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; EDocumentEntryNo: Integer) var - XMLNode: XmlNode; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + ChargeAmount: Decimal; + Value: Text; + CurrencyCode: Text; begin - if not XMLDocument.SelectSingleNode(Path, XMLNamespaces, XMLNode) then - exit; + Clear(EDocumentPurchaseLine); + EDocumentPurchaseLine.Validate("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine."Line No." := EDocumentPurchaseLine.GetNextLineNo(EDocumentEntryNo); + EDocumentPurchaseLine.Quantity := 1; - if XMLNode.IsXmlElement() then begin - Field := CopyStr(XMLNode.AsXmlElement().InnerText(), 1, MaxLength); - exit; - end; + PeppolUtility.SetNumberValueInField(ChargeXML, XmlNamespaces, 'cac:AllowanceCharge/cbc:Amount', ChargeAmount); + EDocumentPurchaseLine."Unit Price" := ChargeAmount; + EDocumentPurchaseLine."Sub Total" := ChargeAmount; - if XMLNode.IsXmlAttribute() then begin - Field := CopyStr(XMLNode.AsXmlAttribute().Value(), 1, MaxLength); - exit; - end; - end; + if PeppolUtility.TryGetStringValue(ChargeXML, XmlNamespaces, 'cac:AllowanceCharge/cbc:AllowanceChargeReason', Value) then + EDocumentPurchaseLine.Description := CopyStr(Value, 1, MaxStrLen(EDocumentPurchaseLine.Description)); - local procedure SetNumberValueInField(XMLDocument: XmlDocument; XMLNamespaces: XmlNamespaceManager; Path: Text; var DecimalValue: Decimal) - var - XMLNode: XmlNode; - begin - if not XMLDocument.SelectSingleNode(Path, XMLNamespaces, XMLNode) then - exit; + PeppolUtility.SetNumberValueInField(ChargeXML, XmlNamespaces, 'cac:AllowanceCharge/cac:TaxCategory/cbc:Percent', EDocumentPurchaseLine."VAT Rate"); + + if PeppolUtility.TryGetStringValue(ChargeXML, XmlNamespaces, 'cac:AllowanceCharge/cbc:Amount/@currencyID', CurrencyCode) then + PeppolUtility.SetCurrencyIfForeign(CurrencyCode, EDocumentPurchaseLine."Currency Code"); - if XMLNode.AsXmlElement().InnerText() <> '' then - Evaluate(DecimalValue, XMLNode.AsXmlElement().InnerText(), 9); + EDocumentPurchaseLine.Insert(); end; - local procedure SetDateValueInField(XMLDocument: XmlDocument; XMLNamespaces: XmlNamespaceManager; Path: Text; var DateValue: Date) + #endregion Line Orchestration + + #region Attachment Orchestration + + local procedure InsertDocumentAttachments(EDocument: Record "E-Document"; PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; RootPath: Text) var - XMLNode: XmlNode; + AttachmentNodes: XmlNodeList; + AttachmentNode: XmlNode; + AttachmentXML: XmlDocument; + i: Integer; begin - if not XMLDocument.SelectSingleNode(Path, XMLNamespaces, XMLNode) then + if not PeppolXML.SelectNodes(RootPath + '/cac:AdditionalDocumentReference', XmlNamespaces, AttachmentNodes) then exit; - if XMLNode.AsXmlElement().InnerText() <> '' then - Evaluate(DateValue, XMLNode.AsXmlElement().InnerText(), 9); + for i := 1 to AttachmentNodes.Count do begin + AttachmentNodes.Get(i, AttachmentNode); + AttachmentXML.ReplaceNodes(AttachmentNode); + PeppolUtility.ExtractAttachment(EDocument, AttachmentXML, XmlNamespaces); + end; end; + #endregion Attachment Orchestration + procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") begin Error('A view is not implemented for this handler.'); end; + } diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLUtility.Codeunit.al b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLUtility.Codeunit.al new file mode 100644 index 0000000000..68edb0fbee --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/StructureReceivedEDocument/EDocumentPEPPOLUtility.Codeunit.al @@ -0,0 +1,339 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Import; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.Finance.GeneralLedger.Setup; +using System.Text; +using System.Utilities; + +/// +/// Reusable PEPPOL BIS 3.0 extraction helpers for reading UBL XML into staging tables. +/// Contains generic UBL party, amounts, line, attachment, and currency logic +/// shared across Invoice and CreditNote document types. +/// +codeunit 6401 "E-Document PEPPOL Utility" +{ + Access = Public; + InherentEntitlements = X; + InherentPermissions = X; + + #region Namespace Initialization + + procedure InitializePEPPOL3Namespaces(var XmlNamespaces: XmlNamespaceManager) + var + CommonAggregateComponentsLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2'; + CommonBasicComponentsLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2'; + DocumentationLbl: Label 'urn:un:unece:uncefact:documentation:2'; + QualifiedDatatypesLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:QualifiedDatatypes-2'; + UnqualifiedDataTypesSchemaModuleLbl: Label 'urn:un:unece:uncefact:data:specification:UnqualifiedDataTypesSchemaModule:2'; + DefaultInvoiceLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'; + DefaultCreditNoteLbl: Label 'urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2'; + begin + XmlNamespaces.AddNamespace('cac', CommonAggregateComponentsLbl); + XmlNamespaces.AddNamespace('cbc', CommonBasicComponentsLbl); + XmlNamespaces.AddNamespace('ccts', DocumentationLbl); + XmlNamespaces.AddNamespace('qdt', QualifiedDatatypesLbl); + XmlNamespaces.AddNamespace('udt', UnqualifiedDataTypesSchemaModuleLbl); + XmlNamespaces.AddNamespace('inv', DefaultInvoiceLbl); + XmlNamespaces.AddNamespace('cre', DefaultCreditNoteLbl); + end; + + #endregion Namespace Initialization + + #region XML Value Extraction + + procedure TryGetStringValue(XMLDocument: XmlDocument; XMLNamespaces: XmlNamespaceManager; Path: Text; var Value: Text): Boolean + var + XMLNode: XmlNode; + begin + if not XMLDocument.SelectSingleNode(Path, XMLNamespaces, XMLNode) then + exit(false); + + if XMLNode.IsXmlElement() then begin + Value := XMLNode.AsXmlElement().InnerText(); + exit(true); + end; + + if XMLNode.IsXmlAttribute() then begin + Value := XMLNode.AsXmlAttribute().Value(); + exit(true); + end; + + exit(false); + end; + + procedure SetNumberValueInField(XMLDocument: XmlDocument; XMLNamespaces: XmlNamespaceManager; Path: Text; var DecimalValue: Decimal) + var + XMLNode: XmlNode; + begin + if not XMLDocument.SelectSingleNode(Path, XMLNamespaces, XMLNode) then + exit; + + if not XMLNode.IsXmlElement() then + exit; + + if XMLNode.AsXmlElement().InnerText() <> '' then + Evaluate(DecimalValue, XMLNode.AsXmlElement().InnerText(), 9); + end; + + procedure SetDateValueInField(XMLDocument: XmlDocument; XMLNamespaces: XmlNamespaceManager; Path: Text; var DateValue: Date) + var + XMLNode: XmlNode; + begin + if not XMLDocument.SelectSingleNode(Path, XMLNamespaces, XMLNode) then + exit; + + if not XMLNode.IsXmlElement() then + exit; + + if XMLNode.AsXmlElement().InnerText() <> '' then + Evaluate(DateValue, XMLNode.AsXmlElement().InnerText(), 9); + end; + + #endregion XML Value Extraction + + #region Header Field Extraction + + /// + /// Extracts AccountingSupplierParty and PayeeParty fields from a UBL document. + /// Per PEPPOL BIS 3.0: PartyName is optional; RegistrationName is mandatory fallback. + /// PayeeParty, when present, overrides vendor name and VAT ID. + /// SchemeID 0088 on EndpointID = GLN. + /// + internal procedure PopulateSupplierInfo(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; RootPath: Text; var Header: Record "E-Document Purchase Header") + var + XmlNode: XmlNode; + Value: Text; + begin + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PartyName/cbc:Name', Value) then + Header."Vendor Company Name" := CopyStr(Value, 1, MaxStrLen(Header."Vendor Company Name")); + if Header."Vendor Company Name" = '' then + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName', Value) then + Header."Vendor Company Name" := CopyStr(Value, 1, MaxStrLen(Header."Vendor Company Name")); + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:PayeeParty/cac:PartyName/cbc:Name', Value) then + Header."Vendor Company Name" := CopyStr(Value, 1, MaxStrLen(Header."Vendor Company Name")); + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:Contact/cbc:Name', Value) then + Header."Vendor Contact Name" := CopyStr(Value, 1, MaxStrLen(Header."Vendor Contact Name")); + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PostalAddress/cbc:StreetName', Value) then + Header."Vendor Address" := CopyStr(Value, 1, MaxStrLen(Header."Vendor Address")); + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingSupplierParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', Value) then + Header."Vendor VAT Id" := CopyStr(Value, 1, MaxStrLen(Header."Vendor VAT Id")); + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:PayeeParty/cac:PartyLegalEntity/cbc:CompanyID', Value) then + Header."Vendor VAT Id" := CopyStr(Value, 1, MaxStrLen(Header."Vendor VAT Id")); + + if PeppolXML.SelectSingleNode(RootPath + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID/@schemeID', XmlNamespaces, XmlNode) then + if XmlNode.AsXmlAttribute().Value() = '0088' then + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingSupplierParty/cac:Party/cbc:EndpointID', Value) then + Header."Vendor GLN" := CopyStr(Value, 1, MaxStrLen(Header."Vendor GLN")); + end; + + /// + /// Extracts AccountingCustomerParty fields from a UBL document. + /// Per PEPPOL BIS 3.0: PartyName is optional; RegistrationName is mandatory fallback. + /// SchemeID 0088 on EndpointID = GLN. Customer Company Id stores schemeID:value. + /// + internal procedure PopulateCustomerInfo(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; RootPath: Text; var Header: Record "E-Document Purchase Header") + var + XmlNode: XmlNode; + SchemeID: Text; + EndpointValue: Text; + Value: Text; + begin + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PartyName/cbc:Name', Value) then + Header."Customer Company Name" := CopyStr(Value, 1, MaxStrLen(Header."Customer Company Name")); + if Header."Customer Company Name" = '' then + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:RegistrationName', Value) then + Header."Customer Company Name" := CopyStr(Value, 1, MaxStrLen(Header."Customer Company Name")); + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PartyLegalEntity/cbc:CompanyID', Value) then + Header."Customer VAT Id" := CopyStr(Value, 1, MaxStrLen(Header."Customer VAT Id")); + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PartyTaxScheme/cbc:CompanyID', Value) then + Header."Customer VAT Id" := CopyStr(Value, 1, MaxStrLen(Header."Customer VAT Id")); + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingCustomerParty/cac:Party/cac:PostalAddress/cbc:StreetName', Value) then + Header."Customer Address" := CopyStr(Value, 1, MaxStrLen(Header."Customer Address")); + + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID', EndpointValue) then begin + if PeppolXML.SelectSingleNode(RootPath + '/cac:AccountingCustomerParty/cac:Party/cbc:EndpointID/@schemeID', XmlNamespaces, XmlNode) then + SchemeID := XmlNode.AsXmlAttribute().Value(); + + if SchemeID = '0088' then + Header."Customer GLN" := CopyStr(EndpointValue, 1, MaxStrLen(Header."Customer GLN")); + + Header."Customer Company Id" := CopyStr(SchemeID + ':' + EndpointValue, 1, MaxStrLen(Header."Customer Company Id")); + end; + end; + + /// + /// Extracts LegalMonetaryTotal amounts, IssueDate, and DueDate from a UBL document. + /// DueDatePath is parameterized because Invoice uses /cbc:DueDate while + /// CreditNote uses /cac:PaymentMeans/cbc:PaymentDueDate. + /// + internal procedure PopulateAmountsAndDates(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; RootPath: Text; DueDatePath: Text; var Header: Record "E-Document Purchase Header") + begin + SetNumberValueInField(PeppolXML, XmlNamespaces, RootPath + '/cac:LegalMonetaryTotal/cbc:PayableAmount', Header.Total); + SetNumberValueInField(PeppolXML, XmlNamespaces, RootPath + '/cac:LegalMonetaryTotal/cbc:TaxExclusiveAmount', Header."Sub Total"); + SetNumberValueInField(PeppolXML, XmlNamespaces, RootPath + '/cac:LegalMonetaryTotal/cbc:AllowanceTotalAmount', Header."Total Discount"); + Header."Total VAT" := Header."Total" - Header."Sub Total" - Header."Total Discount"; + + SetDateValueInField(PeppolXML, XmlNamespaces, DueDatePath, Header."Due Date"); + SetDateValueInField(PeppolXML, XmlNamespaces, RootPath + '/cbc:IssueDate', Header."Document Date"); + end; + + /// + /// Extracts DocumentCurrencyCode and applies the BC LCY-blank convention. + /// + internal procedure PopulateCurrency(PeppolXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; RootPath: Text; var Header: Record "E-Document Purchase Header") + var + DocumentCurrencyCode: Text; + begin + if TryGetStringValue(PeppolXML, XmlNamespaces, RootPath + '/cbc:DocumentCurrencyCode', DocumentCurrencyCode) then + SetCurrencyIfForeign(DocumentCurrencyCode, Header."Currency Code"); + end; + + #endregion Header Field Extraction + + #region Line Field Extraction + + /// + /// Populates a staging line record from a UBL InvoiceLine or CreditNoteLine element. + /// LineElementName and QuantityElementName are parameterized to handle both document types. + /// + internal procedure PopulatePurchaseLine(LineXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var Line: Record "E-Document Purchase Line"; LineElementName: Text; QuantityElementName: Text) + var + Value: Text; + begin + SetNumberValueInField(LineXML, XmlNamespaces, LineElementName + '/' + QuantityElementName, Line.Quantity); + if TryGetStringValue(LineXML, XmlNamespaces, LineElementName + '/' + QuantityElementName + '/@unitCode', Value) then + Line."Unit of Measure" := CopyStr(Value, 1, MaxStrLen(Line."Unit of Measure")); + SetNumberValueInField(LineXML, XmlNamespaces, LineElementName + '/cbc:LineExtensionAmount', Line."Sub Total"); + SetNumberValueInField(LineXML, XmlNamespaces, LineElementName + '/cac:AllowanceCharge/cbc:Amount', Line."Total Discount"); + + // Per PEPPOL BIS 3.0: Item Name (1..1, mandatory) is the primary short product description. + // Item Description (0..1) is an optional longer description that may exceed field capacity. + // Priority: Name (always present per spec), fallback to Description if Name is absent. + if TryGetStringValue(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cbc:Name', Value) then + Line.Description := CopyStr(Value, 1, MaxStrLen(Line.Description)); + if Line.Description = '' then + if TryGetStringValue(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cbc:Description', Value) then + Line.Description := CopyStr(Value, 1, MaxStrLen(Line.Description)); + + // Per PEPPOL BIS 3.0: SellersItemIdentification is the seller's internal product code. + // StandardItemIdentification is a registered standard (e.g., GTIN via schemeID 0160). + // StandardItemIdentification takes priority as the more universally recognized identifier. + if TryGetStringValue(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cac:SellersItemIdentification/cbc:ID', Value) then + if Value <> '' then + Line."Product Code" := CopyStr(Value, 1, MaxStrLen(Line."Product Code")); + if TryGetStringValue(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cac:StandardItemIdentification/cbc:ID', Value) then + if Value <> '' then + Line."Product Code" := CopyStr(Value, 1, MaxStrLen(Line."Product Code")); + + SetNumberValueInField(LineXML, XmlNamespaces, LineElementName + '/cac:Item/cac:ClassifiedTaxCategory/cbc:Percent', Line."VAT Rate"); + SetNumberValueInField(LineXML, XmlNamespaces, LineElementName + '/cac:Price/cbc:PriceAmount', Line."Unit Price"); + + PopulateLineCurrency(LineXML, XmlNamespaces, Line, LineElementName); + end; + + local procedure PopulateLineCurrency(LineXML: XmlDocument; XmlNamespaces: XmlNamespaceManager; var Line: Record "E-Document Purchase Line"; LineElementName: Text) + var + LineCurrencyCode: Text; + begin + if TryGetStringValue(LineXML, XmlNamespaces, LineElementName + '/cbc:LineExtensionAmount/@currencyID', LineCurrencyCode) then + SetCurrencyIfForeign(LineCurrencyCode, Line."Currency Code"); + end; + + #endregion Line Field Extraction + + #region Attachment Extraction + + /// + /// Extracts a single embedded base64 attachment from an AdditionalDocumentReference element. + /// Skips external URI references and bare references without embedded content. + /// Per PEPPOL BIS 3.0: @filename and @mimeCode are mandatory on EmbeddedDocumentBinaryObject. + /// + internal procedure ExtractAttachment(EDocument: Record "E-Document"; AttachmentXML: XmlDocument; XmlNamespaces: XmlNamespaceManager) + var + EDocAttachmentProcessor: Codeunit "E-Doc. Attachment Processor"; + Base64Convert: Codeunit "Base64 Convert"; + AttachmentBlob: Codeunit "Temp Blob"; + InStream: InStream; + OutStream: OutStream; + Base64Content: Text; + FileName: Text; + MimeCode: Text; + FileExtension: Text; + ElementName: Text; + begin + ElementName := 'cac:AdditionalDocumentReference'; + + if not TryGetStringValue(AttachmentXML, XmlNamespaces, ElementName + '/cac:Attachment/cbc:EmbeddedDocumentBinaryObject', Base64Content) then + exit; + + if Base64Content = '' then + exit; + + if not TryGetStringValue(AttachmentXML, XmlNamespaces, ElementName + '/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@filename', FileName) then + TryGetStringValue(AttachmentXML, XmlNamespaces, ElementName + '/cbc:ID', FileName); + + if FileName = '' then + exit; + + if not FileName.Contains('.') then + if TryGetStringValue(AttachmentXML, XmlNamespaces, ElementName + '/cac:Attachment/cbc:EmbeddedDocumentBinaryObject/@mimeCode', MimeCode) then begin + FileExtension := MimeToFileExtension(MimeCode); + if FileExtension <> '' then + FileName := FileName + '.' + FileExtension; + end; + + AttachmentBlob.CreateOutStream(OutStream); + Base64Convert.FromBase64(Base64Content, OutStream); + AttachmentBlob.CreateInStream(InStream); + EDocAttachmentProcessor.Insert(EDocument, InStream, FileName); + end; + + local procedure MimeToFileExtension(MimeCode: Text): Text + begin + case MimeCode of + 'image/jpeg': + exit('jpeg'); + 'image/png': + exit('png'); + 'application/pdf': + exit('pdf'); + 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet': + exit('xlsx'); + 'application/vnd.oasis.opendocument.spreadsheet': + exit('ods'); + 'text/csv': + exit('csv'); + else + exit(''); + end; + end; + + #endregion Attachment Extraction + + #region Currency + + /// + /// BC convention: blank Currency Code means LCY. Sets the field to the currency code + /// only if it differs from LCY. Explicitly blanks the field when it matches LCY. + /// + procedure SetCurrencyIfForeign(CurrencyFromXml: Text; var CurrencyCode: Code[10]) + var + GLSetup: Record "General Ledger Setup"; + begin + if CurrencyFromXml = '' then + exit; + + GLSetup.GetRecordOnce(); + if GLSetup."LCY Code" = CopyStr(CurrencyFromXml, 1, MaxStrLen(CurrencyCode)) then + CurrencyCode := '' + else + CurrencyCode := CopyStr(CurrencyFromXml, 1, MaxStrLen(CurrencyCode)); + end; + + #endregion Currency +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Processing/Import/docs/CLAUDE.md new file mode 100644 index 0000000000..ef28c2289a --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/docs/CLAUDE.md @@ -0,0 +1,27 @@ +# Import pipeline + +The V2 import pipeline converts a received blob (XML, JSON, PDF) into a posted BC purchase invoice through four discrete stages, each producing an intermediate status that can be undone and re-run. The pipeline is orchestrated by `ImportEDocumentProcess.Codeunit.al`, which dispatches to interface implementations so that every stage is replaceable by extensions. + +## How it works + +An incoming E-Document enters the pipeline at status `Unprocessed` with a raw blob in `E-Doc. Data Storage`. Each step advances the status by one notch: **Structure** converts unstructured data (e.g. a PDF via Azure Document Intelligence) into a structured blob and moves to `Readable`. **Read into draft** parses that structured blob into purchase staging tables (`E-Document Purchase Header` / `E-Document Purchase Line`) and moves to `Ready for draft`. **Prepare draft** resolves vendor, items, UOM, GL accounts, and purchase order matches -- filling in the `[BC]` validated columns on those staging tables -- then moves to `Draft ready`. **Finish draft** creates the actual BC purchase invoice (or links to an existing document), writes `E-Doc. Record Link` entries for traceability, and moves to `Processed`. + +Each step is undoable. Undoing Finish Draft deletes the purchase invoice and restores PO matches. Undoing Prepare Draft clears header mappings, vendor assignment, and resets Document Type. Undoing Structure clears the structured data pointer. The user can fix data at any stage and re-run forward from there. + +V1 services are still supported: when `GetImportProcessVersion()` returns `Version 1.0`, the pipeline collapses all stages into a single "Finish draft" call that delegates to the legacy `E-Doc. Import` codeunit. + +## Things to know + +- The pipeline status is an ordered enum (`Unprocessed` = 0 through `Processed` = 4). `StatusStepIndex()` maps each status to a numeric index used for comparison and navigation -- this is how `GetNextStep()` / `GetPreviousStep()` work. + +- The `E-Doc. Import Parameters` table is temporary and controls pipeline execution: which step to run, whether to target a step or a desired status, processing customizations, and V1-compatibility flags. + +- Interface dispatch is layered: `IEDocFileFormat` determines the preferred `IStructureReceivedEDocument`, which returns an `IStructuredDataType` that specifies the `IStructuredFormatReader`, which returns the `IProcessStructuredData` enum. Each stage's output feeds the next stage's interface selection. + +- The `E-Doc. Proc. Customizations` enum is a multi-interface enum that bundles `IVendorProvider`, `IPurchaseOrderProvider`, `IPurchaseLineProvider`, `IUnitOfMeasureProvider`, and `IEDocumentCreatePurchaseInvoice` with defaults from `EDocProviders.Codeunit.al`. Extensions add a new enum value to swap all five at once. + +- AI-assisted matching runs during Prepare Draft: historical matching first, then Copilot GL account matching for remaining unresolved lines, then deferral matching. Each step commits before invoking the next codeunit to isolate failures. + +- The history system is populated by event subscribers on `Purch.-Post`: `OnAfterPurchInvLineInsert` and `OnAfterPostPurchaseDoc` create entries in `E-Doc. Purchase Line History` and `E-Doc. Vendor Assign. History`, completing the learning loop. + +- See `../docs/CLAUDE.md` for the parent Processing module context. The `src/Processing/Interfaces/` folder defines 18 interfaces that underpin this pipeline. diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/docs/business-logic.md b/src/Apps/W1/EDocument/App/src/Processing/Import/docs/business-logic.md new file mode 100644 index 0000000000..896ff52c33 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/docs/business-logic.md @@ -0,0 +1,106 @@ +# Import pipeline business logic + +## Pipeline overview + +```mermaid +flowchart TD + A["Unprocessed
(raw blob)"] -->|"Structure received data"| B["Readable
(structured blob)"] + B -->|"Read into draft"| C["Ready for draft
(staging tables populated)"] + C -->|"Prepare draft"| D["Draft ready
(BC entities resolved)"] + D -->|"Finish draft"| E["Processed
(Purchase Invoice created)"] + E -.->|"Undo finish"| D + D -.->|"Undo prepare"| C + C -.->|"Undo read"| B + B -.->|"Undo structure"| A +``` + +The pipeline is driven by `ImportEDocumentProcess.Codeunit.al`. Its `OnRun()` trigger checks the service's import process version. V1 services skip straight to a legacy code path. V2 services dispatch to one of four local procedures based on the configured step. After each step, the processing status is advanced (or rolled back for undo) and the E-Document status is recalculated. + +## Stage 1 -- Structure received data + +**Transition:** Unprocessed --> Readable + +The E-Document arrives with an `Unstructured Data Entry No.` pointing to a raw blob in `E-Doc. Data Storage`. The Structure step loads that blob, resolves the file format via `IEDocFileFormat` (the enum value stored on the data storage record), and asks it for a `PreferredStructureDataImplementation()`. + +For XML, the preferred implementation is `"Already Structured"` -- the blob is already parseable, so the unstructured and structured data entry numbers are set to the same value and nothing is converted. For PDF, the preferred implementation is `"ADI"` -- Azure Document Intelligence converts the binary into a JSON blob. The ADI handler (`EDocumentADIHandler.Codeunit.al`) base64-encodes the blob, calls `AzureDocumentIntelligence.AnalyzeInvoice()`, and stores the JSON result. If ADI fails (returns empty), it falls back to `"Blank Draft"` so the user can populate fields manually. + +When the data is actually converted (i.e. not "Already Structured"), the original unstructured blob is saved as a document attachment on the E-Document for reference. The structured result is stored as a new `E-Doc. Data Storage` entry via the log, and its entry number goes into `E-Document."Structured Data Entry No."`. + +The `IStructuredDataType` returned by the structure implementation also specifies which `IStructuredFormatReader` should be used in the next stage. If the structure step says ADI, the reader will be ADI. If it says nothing (`Unspecified`), the service's default reader is used. This chaining means a single PDF upload can auto-select the entire downstream pipeline. + +## Stage 2 -- Read into draft + +**Transition:** Readable --> Ready for draft + +The structured blob is loaded and passed to the `IStructuredFormatReader` determined in Stage 1. Two readers ship in the core: + +- **PEPPOL** (`EDocumentPEPPOLHandler.Codeunit.al`): Parses UBL 2.1 XML. Extracts vendor party info, invoice totals, currency, dates, and iterates `cac:InvoiceLine` nodes to create `E-Document Purchase Line` records. Uses XPath with UBL namespace prefixes. +- **ADI** (`EDocumentADIHandler.Codeunit.al`): Parses the ADI JSON schema. Maps ADI field names like `vendorName`, `invoiceId`, `productCode` to the staging table columns. Sets quantity to 1 when ADI returns zero or negative. + +Both readers insert an `E-Document Purchase Header` and one `E-Document Purchase Line` per invoice line. These staging tables use **dual nomenclature**: fields 2-100 hold the raw external data exactly as extracted (e.g. `"Vendor Company Name"`, `"Product Code"`), while fields 101-200 hold validated BC references (e.g. `"[BC] Vendor No."`, `"[BC] Purchase Type No."`). At this stage, only the external-data columns are populated. + +The reader returns an `E-Doc. Process Draft` enum value (currently only `"Purchase Document"`) that determines which `IProcessStructuredData` implementation runs in Stage 3. + +## Stage 3 -- Prepare draft + +**Transition:** Ready for draft --> Draft ready + +This is where the system resolves external data into BC entities. `PreparePurchaseEDocDraft.Codeunit.al` implements `IProcessStructuredData` and orchestrates the resolution. + +### Vendor resolution + +`GetVendor()` delegates to `IVendorProvider`. The default provider (`EDocProviders.Codeunit.al`) tries a four-step waterfall: + +1. **VAT ID + GLN** -- calls `EDocumentImportHelper.FindVendor()` with the extracted VAT ID and GLN +2. **Service Participant** -- looks up the vendor's external ID in the `Service Participant` table, first scoped to the specific service, then across all services +3. **Name + Address** -- calls `FindVendorByNameAndAddress()` as a last resort +4. **Historical** -- if the direct provider returns nothing, `EDocPurchaseHistMapping.FindRelatedPurchaseHeaderInHistory()` searches `E-Doc. Vendor Assign. History` by GLN, then VAT ID, then company name, then address (most specific first, most recent first). If a match is found, the vendor number is copied from the linked posted purchase invoice header. + +### Line enrichment + +For each `E-Document Purchase Line`, the pipeline resolves: + +- **Unit of Measure** via `IUnitOfMeasureProvider` -- tries Code, then International Standard Code, then Description +- **Purchase line type and number** via `IPurchaseLineProvider` -- the default implementation tries Item Reference first (filtering by vendor, product code, UOM, and date validity), then falls back to Text-to-Account Mapping. Each successful match writes an Activity Log entry explaining the reasoning. + +### Purchase order matching + +`IPurchaseOrderProvider.GetPurchaseOrder()` checks if the `"Purchase Order No."` extracted from the document matches an existing PO. If found, the PO number is stored on the purchase header's `[BC] Purchase Order No.` field for use during Finish Draft. + +### Copilot-assisted matching + +After the direct resolution pass, three Copilot codeunits run sequentially on lines that still lack a `[BC] Purchase Type No.`: + +1. **Historical matching** (`E-Doc. Historical Matching`) -- searches `E-Doc. Purchase Line History` for past invoices with the same product code or similar description from the same vendor. Applies the posted line's type, number, deferral code, dimensions, and UOM to the draft. +2. **GL account matching** (`E-Doc. GL Account Matching`) -- uses AI to suggest a G/L account for lines with no item match. +3. **Deferral matching** (`E-Doc. Deferral Matching`) -- for lines that have a type but no deferral code, suggests a deferral template. + +Each step commits before invoking the next to isolate failures. Deferral matching swallows errors silently to avoid blocking the pipeline. + +## Stage 4 -- Finish draft + +**Transition:** Draft ready --> Processed + +`IEDocumentFinishDraft.ApplyDraftToBC()` creates the actual BC document. The default implementation (`EDocCreatePurchaseInvoice.Codeunit.al`) performs: + +1. **Validation**: Checks that all draft lines have a type and number. Verifies PO match validity -- matched lines must be receivable and have UOM info. +2. **Receipt suggestion**: For lines matched to PO lines, `SuggestReceiptsForMatchedOrderLines()` proposes receipt lines. +3. **Invoice creation**: Creates a `Purchase Header` (type Invoice), sets vendor, dates, currency, and vendor invoice number. Checks for duplicate external document numbers. Inserts lines without PO matches first, then lines grouped by receipt number with comment-line separators. +4. **PO match transfer**: `TransferPOMatchesFromEDocumentToInvoice()` moves match records from the E-Document to the purchase invoice. +5. **Traceability**: `EDocRecordLink.InsertEDocumentHeaderLink()` and `InsertEDocumentLineLink()` create `E-Doc. Record Link` entries linking draft records to their BC counterparts via SystemId. +6. **Post-creation**: Copies document attachments from the E-Document to the purchase header, applies invoice discount, sets `E-Document Link` GUID on the purchase header, and validates document totals. + +Alternatively, if `EDocImportParameters."Existing Doc. RecordId"` is set, no new invoice is created -- the E-Document is linked to an existing purchase document instead. + +### Undo finish + +`RevertDraftActions()` finds the purchase invoice via the `E-Document Link` GUID, transfers PO matches back to the E-Document, moves attachments back, clears the link, and clears `Document Record ID`. The purchase invoice itself is not deleted -- it must be handled separately. + +## The learning loop + +When a purchase invoice created by this pipeline is posted, event subscribers on `Purch.-Post` fire: + +- `OnAfterPurchInvLineInsert` writes to `E-Doc. Purchase Line History` -- recording the vendor, product code, description, and the posted invoice line's SystemId. The link is found by traversing `E-Doc. Record Link` from the purchase line back to the draft line. +- `OnAfterPostPurchaseDoc` writes to `E-Doc. Vendor Assign. History` -- recording the vendor identifiers from the original E-Document draft header and the posted invoice header's SystemId. + +Both event subscribers then delete the `E-Doc. Record Link` entries since the link has been "graduated" to permanent history. This means the history tables grow monotonically and future imports get progressively better at vendor and line resolution. diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/docs/data-model.md b/src/Apps/W1/EDocument/App/src/Processing/Import/docs/data-model.md new file mode 100644 index 0000000000..56c3bea146 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/docs/data-model.md @@ -0,0 +1,79 @@ +# Import pipeline data model + +## Purchase staging tables + +The staging tables hold the intermediate representation of an imported document between the Read and Finish stages. They use **dual nomenclature**: fields 2-100 store raw external data exactly as extracted from the source document, while fields 101-200 store validated BC entity references populated during Prepare Draft. + +```mermaid +erDiagram + E_DOCUMENT ||--o| E_DOCUMENT_PURCHASE_HEADER : "Entry No" + E_DOCUMENT_PURCHASE_HEADER ||--o{ E_DOCUMENT_PURCHASE_LINE : "E-Document Entry No" + E_DOCUMENT_PURCHASE_HEADER }o--o| VENDOR : "[BC] Vendor No." + E_DOCUMENT_PURCHASE_HEADER }o--o| PURCHASE_HEADER : "[BC] Purchase Order No." +``` + +**E-Document Purchase Header** (`Purchase/EDocumentPurchaseHeader.Table.al`, table 6100) is keyed on `E-Document Entry No.` (one-to-one with E-Document). External fields include `Vendor Company Name`, `Vendor VAT Id`, `Vendor GLN`, `Purchase Order No.`, `Sales Invoice No.`, address blocks, and monetary totals. BC fields are `[BC] Vendor No.` and `[BC] Purchase Order No.`. + +**E-Document Purchase Line** (`Purchase/EDocumentPurchaseLine.Table.al`, table 6101) is keyed on `(E-Document Entry No., Line No.)`. External fields include `Product Code`, `Description`, `Quantity`, `Unit Price`, `Unit of Measure`, and `Currency Code`. BC fields include `[BC] Purchase Line Type`, `[BC] Purchase Type No.`, `[BC] Unit of Measure`, `[BC] Deferral Code`, `[BC] Item Reference No.`, `[BC] Variant Code`, `[BC] Dimension Set ID`, and shortcut dimension codes. The `E-Doc. Purch. Line History Id` metadata field links to the historical match that populated the BC fields, if any. + +The staging tables are writable by the user through the `E-Document Purchase Draft` page. When the user changes a `[BC]` field on a line that has PO matches, an `OnValidate` trigger confirms removal of those matches before proceeding. + +## Header and line mappings + +```mermaid +erDiagram + E_DOCUMENT ||--o| E_DOCUMENT_HEADER_MAPPING : "E-Document Entry No." + E_DOCUMENT ||--o{ E_DOCUMENT_LINE_MAPPING : "E-Document Entry No., Line No." +``` + +**E-Document Header Mapping** (table 6102) stores validated BC overrides for the header -- `Vendor No.` and `Purchase Order No.` -- applied during Finish Draft. Deleted when Prepare Draft is undone. + +**E-Document Line Mapping** (table 6105) stores validated BC overrides per line -- purchase line type/number, UOM, deferral code, dimensions, item reference, variant code, and a history ID. These are the "confirmed" values that override what the provider chain suggested. + +## Purchase order matching + +```mermaid +erDiagram + E_DOCUMENT_PURCHASE_LINE ||--o{ E_DOC_PURCHASE_LINE_PO_MATCH : "E-Doc. Purchase Line SystemId" + PURCHASE_LINE ||--o{ E_DOC_PURCHASE_LINE_PO_MATCH : "Purchase Line SystemId" + PURCH_RCPT_LINE ||--o{ E_DOC_PURCHASE_LINE_PO_MATCH : "Receipt Line SystemId" +``` + +**E-Doc. Purchase Line PO Match** (`Purchase/PurchaseOrderMatching/EDocPurchaseLinePOMatch.Table.al`, table 6114) is the N:M junction table linking e-document draft lines to purchase order lines and optionally to receipt lines. The composite key is `(E-Doc. Purchase Line SystemId, Purchase Line SystemId, Receipt Line SystemId)` -- all three are Guid fields using SystemId references. + +`EDocPOMatching.Codeunit.al` manages this table: loading available PO lines for matching (filtering by vendor and optionally by order number), verifying match validity, suggesting receipts for matched lines, and transferring matches between E-Document and Purchase Invoice during Finish Draft / Undo Finish. + +## Historical learning + +```mermaid +erDiagram + E_DOC_PURCHASE_LINE_HISTORY }o--|| PURCH_INV_LINE : "Purch. Inv. Line SystemId" + E_DOC_VENDOR_ASSIGN_HISTORY }o--|| PURCH_INV_HEADER : "Purch. Inv. Header SystemId" + E_DOC_PURCHASE_LINE_HISTORY }o--|| VENDOR : "Vendor No." +``` + +**E-Doc. Purchase Line History** (`Purchase/History/EDocPurchaseLineHistory.Table.al`, table 6140) records what BC entities were assigned to past draft lines. Key fields: `Vendor No.`, `Product Code`, `Description`, and `Purch. Inv. Line SystemId`. Four secondary keys enable flexible lookup: by `(Vendor No., Product Code, Description)`, by `(Product Code, Description)`, by `(Vendor No., Product Code)`, and by `(Vendor No., Description)`. The history search in `EDocPurchaseHistMapping.FindRelatedPurchaseLineInHistory()` tries product code first, then exact description match, then prefix match, then substring match -- all scoped to the same vendor and sorted most-recent-first. + +**E-Doc. Vendor Assign. History** (`Purchase/History/EDocVendorAssignHistory.Table.al`, table 6108) records past vendor identifier-to-vendor-number mappings. Key fields: `Vendor Company Name`, `Vendor Address`, `Vendor VAT Id`, `Vendor GLN`, and `Purch. Inv. Header SystemId`. The `Vendor No From Purch. Header` FlowField resolves the vendor number from the posted invoice. When the same identifier combination appears again, the existing record is updated rather than duplicated. + +Both tables are populated by `EDocPurchaseHistMapping.Codeunit.al` via event subscribers on `Purch.-Post`. The `E-Doc. Record Link` entries that connect draft records to BC records are consumed during this process and then deleted -- they serve as temporary bridges that are "graduated" to permanent history on posting. + +## Record links + +**E-Doc. Record Link** (`../EDocRecordLink.Table.al`, table 6141) provides SystemId-based links between draft staging records and BC records created during Finish Draft. Each entry stores source table/SystemId and target table/SystemId. Two links are created per line (draft line --> purchase line) plus one per header (draft header --> purchase header). These links serve two purposes: they allow navigation from draft records to their BC counterparts, and they are the mechanism by which the posting event subscribers find the original draft data to populate the history tables. + +## Additional fields + +```mermaid +erDiagram + ED_PURCHASE_LINE_FIELD_SETUP ||--o{ E_DOCUMENT_LINE_FIELD : "Field No." + E_DOCUMENT_PURCHASE_LINE ||--o{ E_DOCUMENT_LINE_FIELD : "E-Document Entry No., Line No." +``` + +**ED Purchase Line Field Setup** (`AdditionalFields/EDPurchaseLineFieldSetup.Table.al`, table 6112) defines which `Purch. Inv. Line` fields should be tracked as additional columns on draft lines, scoped per E-Document Service. Fields that already exist on the staging tables (Type, No., UOM, etc.) are automatically omitted. + +**E-Document Line - Field** (`AdditionalFields/EDocumentLineField.Table.al`, table 6110) is a polymorphic value store keyed on `(E-Document Entry No., Line No., Field No.)`. It has six typed value columns: `Text Value`, `Decimal Value`, `Date Value`, `Boolean Value`, `Code Value`, and `Integer Value`. The `Get()` procedure implements a three-tier resolution: if a physical record exists, it returns `Customized`. Otherwise, it looks up the `E-Doc. Purch. Line History Id` on the draft line to find the posted invoice line and reads the value from history, returning `Historic`. If neither exists, it returns `Default` with blank values. During Finish Draft, `ApplyAdditionalFieldsFromHistoryToPurchaseLine()` validates these values onto the actual purchase line via FieldRef. + +## Import parameters + +**E-Doc. Import Parameters** (table 6106) is a **temporary** table that configures a single pipeline execution. Key fields: `Processing Customizations` (which provider enum to use), `Step to Run` / `Desired E-Document Status` (forward to a step or target a status), `Existing Doc. RecordId` (link to existing document instead of creating), and V1 compatibility flags. Being temporary, it exists only in memory during the import call. diff --git a/src/Apps/W1/EDocument/App/src/Processing/Import/docs/extensibility.md b/src/Apps/W1/EDocument/App/src/Processing/Import/docs/extensibility.md new file mode 100644 index 0000000000..92dbc635aa --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Import/docs/extensibility.md @@ -0,0 +1,148 @@ +# Import pipeline extensibility + +The import pipeline is built on extensible enums backed by interfaces. Every stage dispatches through an interface, so extensions replace behavior by adding enum values with new implementations. The interfaces live in `src/Processing/Interfaces/`. + +## Add a new document format parser + +To support a new file format (e.g. CSV invoices), implement two things: + +**File format detection.** Extend the `"E-Doc. File Format"` enum with a new value whose `IEDocFileFormat` implementation returns the file extension, a content preview method, and a preferred structure implementation: + +``` +interface IEDocFileFormat + procedure FileExtension(): Text + procedure PreviewContent(FileName: Text; TempBlob: Codeunit "Temp Blob") + procedure PreferredStructureDataImplementation(): Enum "Structure Received E-Doc." +``` + +The built-in implementations are in `FileFormat/`: XML returns `"Already Structured"`, PDF returns `"ADI"`, JSON returns `"Already Structured"`. Your format should point to whichever structuring implementation makes sense. + +**Structured format reader.** Extend the `"E-Doc. Read into Draft"` enum with a new value whose `IStructuredFormatReader` implementation parses the structured blob into `E-Document Purchase Header` / `E-Document Purchase Line` records: + +``` +interface IStructuredFormatReader + procedure ReadIntoDraft(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob"): Enum "E-Doc. Process Draft" + procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") +``` + +`ReadIntoDraft` must insert the staging records and return the `"E-Doc. Process Draft"` enum value that determines which Prepare Draft implementation runs. See `EDocumentPEPPOLHandler.Codeunit.al` for a complete XML example and `EDocumentADIHandler.Codeunit.al` for a JSON example. + +## Add a new structuring mechanism + +If you have a non-trivial conversion step (e.g. a custom OCR service for scanned documents), extend the `"Structure Received E-Doc."` enum with a new value whose `IStructureReceivedEDocument` implementation converts the raw blob: + +``` +interface IStructureReceivedEDocument + procedure StructureReceivedEDocument(EDocumentDataStorage: Record "E-Doc. Data Storage"): Interface IStructuredDataType +``` + +Your implementation receives the raw blob and must return an `IStructuredDataType` -- a stateful object that holds the file format, the text content, and optionally specifies which `IStructuredFormatReader` to use downstream: + +``` +interface IStructuredDataType + procedure GetFileFormat(): Enum "E-Doc. File Format" + procedure GetContent(): Text + procedure GetReadIntoDraftImpl(): Enum "E-Doc. Read into Draft" +``` + +The ADI handler (`EDocumentADIHandler.Codeunit.al`) implements all three interfaces (`IStructureReceivedEDocument`, `IStructuredDataType`, `IStructuredFormatReader`) in a single codeunit because it owns the entire chain from PDF to staging tables. + +## Customize vendor resolution + +Extend the `"E-Doc. Proc. Customizations"` enum. This is a multi-interface enum that bundles five provider interfaces with defaults from `EDocProviders.Codeunit.al`. Your new enum value provides custom implementations for any or all of: + +``` +interface IVendorProvider + procedure GetVendor(EDocument: Record "E-Document"): Record Vendor +``` + +The default implementation (`EDocProviders.GetVendor`) tries VAT ID + GLN lookup, then Service Participant matching, then name + address search. Replace it to add your own vendor matching logic -- for example, looking up a custom identifier from a localized field. + +## Customize line resolution + +The same `"E-Doc. Proc. Customizations"` enum also controls line-level resolution: + +``` +interface IPurchaseLineProvider + procedure GetPurchaseLine(var EDocumentPurchaseLine: Record "E-Document Purchase Line") +``` + +The default implementation tries Item Reference by vendor + product code, then Text-to-Account Mapping by description. Your implementation receives the draft line with external data populated and should set `[BC] Purchase Line Type`, `[BC] Purchase Type No.`, and related fields. + +Note: `IPurchaseLineAccountProvider` (same signature pattern but with explicit out-parameters for account type and number) is **obsolete as of v27** -- replaced by `IPurchaseLineProvider`. + +``` +interface IUnitOfMeasureProvider + procedure GetUnitOfMeasure(EDocument: Record "E-Document"; EDocumentLineId: Integer; ExternalUnitOfMeasure: Text): Record "Unit of Measure" +``` + +The default tries UOM Code, then International Standard Code, then Description. Override this if your vendors use non-standard UOM identifiers. + +## Customize purchase order matching + +``` +interface IPurchaseOrderProvider + procedure GetPurchaseOrder(EDocumentPurchaseHeader: Record "E-Document Purchase Header"): Record "Purchase Header" +``` + +The default looks up `"Purchase Order No."` from the draft header. Override to implement custom PO matching logic -- for example, matching by a combination of vendor and date range. + +## Customize invoice creation + +``` +interface IEDocumentCreatePurchaseInvoice + procedure CreatePurchaseInvoice(EDocument: Record "E-Document"): Record "Purchase Header" +``` + +The `"E-Doc. Create Purchase Invoice"` enum is extensible and defaults to `EDocCreatePurchaseInvoice.Codeunit.al`. Override to change how purchase invoices are created -- for example, to set custom fields, apply different discount logic, or create credit memos instead. + +The Finish Draft step also uses: + +``` +interface IEDocumentFinishDraft + procedure ApplyDraftToBC(EDocument: Record "E-Document"; EDocImportParameters: Record "E-Doc. Import Parameters"): RecordId + procedure RevertDraftActions(EDocument: Record "E-Document") +``` + +This is controlled by the `"E-Document Type"` enum set during Prepare Draft. Currently only `"Purchase Invoice"` is implemented, routing to `EDocCreatePurchaseInvoice.Codeunit.al`. + +## Register AI tools for line matching + +The Copilot matching subsystem uses `IEDocAISystem` to register AI-powered processing tools: + +``` +interface IEDocAISystem + procedure GetSystemPrompt(UserLanguage: Text): SecretText + procedure GetTools(): List of [Interface "AOAI Function"] + procedure GetFeatureName(): Text +``` + +Extensions can register new AI systems by extending the `"E-Doc. AI System"` enum. Each system provides a system prompt, a set of AOAI Function tool implementations (for function-calling), and a feature name for telemetry. The built-in systems cover historical matching, GL account matching, and deferral matching. Add your own to support custom matching scenarios -- for example, matching lines to projects or jobs. + +## Add a new draft preparation strategy + +Extend the `"E-Doc. Process Draft"` enum to add a new `IProcessStructuredData` implementation: + +``` +interface IProcessStructuredData + procedure PrepareDraft(EDocument: Record "E-Document"; EDocImportParameters: Record "E-Doc. Import Parameters"): Enum "E-Document Type" + procedure GetVendor(EDocument: Record "E-Document"; Customizations: Enum "E-Doc. Proc. Customizations"): Record Vendor + procedure OpenDraftPage(var EDocument: Record "E-Document") + procedure CleanUpDraft(EDocument: Record "E-Document") +``` + +Currently only `"Purchase Document"` exists. A future value could handle general journal lines or service invoices. Your `IStructuredFormatReader.ReadIntoDraft()` returns the appropriate enum value to route to your preparation logic. + +## Extension patterns summary + +| Goal | Extend this enum | Implement this interface | +|------|-----------------|------------------------| +| New file type detection | `"E-Doc. File Format"` | `IEDocFileFormat` | +| New structuring method (OCR, etc.) | `"Structure Received E-Doc."` | `IStructureReceivedEDocument` + `IStructuredDataType` | +| New format reader | `"E-Doc. Read into Draft"` | `IStructuredFormatReader` | +| New draft preparation | `"E-Doc. Process Draft"` | `IProcessStructuredData` | +| Custom providers (vendor, item, UOM, PO, invoice) | `"E-Doc. Proc. Customizations"` | Any combination of 5 provider interfaces | +| New AI matching tool | `"E-Doc. AI System"` | `IEDocAISystem` | +| Custom invoice creation | `"E-Doc. Create Purchase Invoice"` | `IEDocumentCreatePurchaseInvoice` | + +Two interfaces in `src/Processing/Interfaces/` are not part of the import pipeline: `IExportEligibilityEvaluator` (outbound filtering) and `IBlobToStructuredDataConverter` / `IBlobType` (obsolete as of v26, replaced by `IEDocFileFormat` and `IStructureReceivedEDocument`). diff --git a/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IEDocumentCreatePurchaseCreditMemo.Interface.al b/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IEDocumentCreatePurchaseCreditMemo.Interface.al new file mode 100644 index 0000000000..ee78778410 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/Interfaces/IEDocumentCreatePurchaseCreditMemo.Interface.al @@ -0,0 +1,21 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Processing.Interfaces; + +using Microsoft.eServices.EDocument; +using Microsoft.Purchases.Document; + +/// +/// Interface for changing the way that purchase credit memos get created from an E-Document. +/// +interface IEDocumentCreatePurchaseCreditMemo +{ + /// + /// Creates a purchase credit memo from an E-Document with a draft ready. + /// + /// The E-Document to create the credit memo from. + /// The created Purchase Header record. + procedure CreatePurchaseCreditMemo(EDocument: Record "E-Document"): Record "Purchase Header"; +} diff --git a/src/Apps/W1/EDocument/App/src/Processing/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Processing/docs/CLAUDE.md new file mode 100644 index 0000000000..76651f6080 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/docs/CLAUDE.md @@ -0,0 +1,29 @@ +# Processing + +The Processing module orchestrates what happens to E-Documents between creation and delivery (outbound) or between receipt and BC document creation (inbound). It owns the export pipeline, event subscribers that hook into BC posting, background job scheduling, order matching for purchase documents, and AI-assisted line matching via Copilot. The import pipeline lives in `Import/` and has its own docs. + +## How it works + +**Outbound flow.** `EDocumentSubscribers` listens to `OnAfterPostSalesDoc`, `OnAfterPostPurchaseDoc`, `OnAfterPostServiceDoc`, and similar events. When a document posts and its Document Sending Profile is set to `"Extended E-Document Service Flow"`, the subscriber calls `EDocExport.CreateEDocument()`. This creates an E-Document record, evaluates export eligibility per service via `IExportEligibilityEvaluator`, runs field mapping, invokes the format interface's `Create()` method (via `EDocumentCreate.Codeunit.al`) to produce a TempBlob, and logs the result. Finally, `EDocumentBackgroundJobs.StartEDocumentCreatedFlow()` enqueues a job that triggers the workflow -- which in turn decides whether to send, email, or route for approval. + +**Batch processing.** When a service has `"Use Batch Processing"` enabled, individual documents are not exported immediately. Instead they get status `Created` and wait. If the batch mode is `Recurrent`, a scheduled job (`"E-Doc. Recurrent Batch Send"`) collects all pending-batch documents grouped by document type, exports them as a batch, and sends them together. + +**Order matching.** For incoming purchase orders, `EDocLineMatching.Codeunit.al` matches imported e-document lines to existing purchase order lines. Automatic matching filters on UOM, unit cost, and discount, then uses `CalculateStringNearness()` above 80% for description matching, plus Item Reference and Text-to-Account Mapping lookups. The Copilot subfolder adds AI-assisted matching via Azure OpenAI when automatic matching leaves unmatched lines. + +**AI tools.** `EDocAIToolProcessor.Codeunit.al` is a generic Copilot orchestrator that configures Azure OpenAI (GPT-4.1), registers AI tools as function calls, and processes responses. The `Tools/` subfolder provides implementations for historical matching, G/L account matching, deferral matching, and similar-description lookups. + +## Things to know + +- Export eligibility is pluggable: the `"Export Eligibility Evaluator"` enum on the service record controls which `IExportEligibilityEvaluator` runs. The default implementation allows all documents. Extend the enum to filter by document attributes, customer, or any other criteria. + +- `EDocumentCreate.Codeunit.al` is a thin runner that delegates to the format interface's `Create()` or `CreateBatch()`. It exists solely to be wrapped in `Codeunit.Run()` for error isolation. + +- `EDocumentSubscribers` also subscribes to release events (`OnBeforeReleaseSalesDoc`, etc.) and posting-check events to run `CheckEDocument()` before the document is committed, ensuring format-specific validation happens early. + +- Order matching only applies to incoming purchase orders (`"Document Type" = "Purchase Order"`, `Direction = Incoming`, `Status = "Order Linked"`). The matching page lets users match manually, run automatic matching, or invoke Copilot. Accepted matches persist to the `"E-Doc. Order Match"` table and update `"Qty. to Invoice"` on purchase lines. + +- The Copilot PO matching (`EDocPOCopilotMatching.Codeunit.al`) builds a user prompt from imported line and PO line descriptions, sends it to GPT-4.1 with function-calling tools, and grounds the result by verifying cost/quantity thresholds before surfacing proposals. + +- `EDocumentBackgroundJobs` manages three job types: the one-shot "created flow" trigger, the recurring 5-minute `GetResponse` poller, and the recurrent batch send/import jobs with configurable frequency. + +- Do not confuse `EDocImport.Codeunit.al` in this folder with the full import pipeline -- it is the entry point that delegates to `Processing/Import/` for V2 import processing. See the Import docs for that pipeline. diff --git a/src/Apps/W1/EDocument/App/src/Processing/docs/business-logic.md b/src/Apps/W1/EDocument/App/src/Processing/docs/business-logic.md new file mode 100644 index 0000000000..c42bd1c89d --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Processing/docs/business-logic.md @@ -0,0 +1,101 @@ +# Business logic + +## Overview + +Processing owns two major flows: outbound export (posting a BC document into an E-Document and handing it to the Integration layer) and inbound order matching (reconciling imported e-document lines against purchase order lines). The export flow is event-driven and largely automatic; order matching is interactive with optional AI assistance. Error handling throughout uses the `Commit(); if not Codeunit.Run()` pattern -- every interface call is isolated so a failure logs an error without rolling back the surrounding transaction. + +## Export flow + +The outbound pipeline starts when a BC document is posted and ends when the E-Document is queued for sending. + +```mermaid +flowchart TD + A[BC document posted] --> B{Document Sending Profile = Extended E-Document Service Flow?} + B -- No --> Z[No E-Document created] + B -- Yes --> C[EDocExport.CreateEDocument] + C --> D[Create E-Document record with Status = In Progress] + D --> E{For each service in workflow} + E --> F{IExportEligibilityEvaluator.ShouldExport?} + F -- No --> E + F -- Yes --> G{Service uses batch processing?} + G -- Yes --> H[Set status = Created, wait for batch job] + G -- No --> I[MapEDocument + format.Create = TempBlob] + I --> J{Export succeeded?} + J -- No --> K[Status = Export Error, log error] + J -- Yes --> L[Status = Exported, store blob in log] + L --> M[StartEDocumentCreatedFlow -- enqueue background job] + M --> N[Workflow evaluates -- triggers Send / Email / Approval] + H --> O[Recurrent batch job collects pending-batch docs] + O --> P[ExportEDocumentBatch + SendBatch] +``` + +Key decision points in the flow: + +- **Document Sending Profile.** The subscriber checks whether the customer/vendor has a profile with `"Electronic Document" = "Extended E-Document Service Flow"` and a valid, enabled workflow. If not, no E-Document is created. + +- **Export eligibility.** Each service in the workflow is checked individually via `IExportEligibilityEvaluator.ShouldExport()`. The default implementation allows all documents, but extensions can filter by document type, amount, customer attributes, or any other criteria. The service's `"E-Doc. Service Supported Type"` table is also checked before the evaluator runs. + +- **Batch vs. immediate.** If the service has `"Use Batch Processing"` enabled, the document gets status `Created` and is not exported inline. A recurrent job queue entry (`"E-Doc. Recurrent Batch Send"`) picks up all pending-batch documents at the configured interval, groups them by document type, exports them as a single batch blob, and sends the batch. + +- **Field mapping.** Before calling the format interface's `Create()`, the framework applies field-level mappings defined in `"E-Doc. Mapping"`. Source document headers and lines are copied to temporary records with mapped field values, and a mapping log is written. This happens for both individual and batch export. + +- **Error isolation.** `EDocumentCreate.Codeunit.al` is a runner codeunit invoked with `Codeunit.Run()`. If the format interface throws, `GetLastErrorText()` is captured and logged against the E-Document without aborting the caller. + +## Order matching (two separate systems) + +There are two distinct order matching systems in the codebase. They serve different purposes and use different data models. Do not confuse them. + +### V2 import pipeline PO matching (automatic, during Prepare Draft) + +This is the **newer** system, part of the V2.0 import pipeline in `Import/Purchase/PurchaseOrderMatching/`. It runs automatically during the "Prepare draft" stage when an incoming e-document references a purchase order number. + +The flow: +1. During Prepare Draft, `PreparePurchaseEDocDraft` calls `IPurchaseOrderProvider.GetPurchaseOrder()` to look up a PO by order number from the e-document header. +2. If found, `EDocPOMatching.MatchPOLinesToEDocumentLine()` matches e-document purchase lines to PO lines. +3. After matching, `CalculatePOMatchWarnings()` generates warnings for over-receipt, under-receipt, quantity mismatches, etc. +4. During Finish Draft, `SuggestReceiptsForMatchedOrderLines()` proposes receipt lines, and `TransferPOMatchesFromEDocumentToInvoice()` writes matches to the created purchase invoice. + +**Key data:** Matches are stored in `"E-Doc. Purchase Line PO Match"` (table 6114) -- a junction table linking e-document lines to PO lines and receipt lines via SystemIds. Warnings go in `"E-Doc. PO Match Warning"` (table 6115). Receipt behavior is configurable per vendor in `"E-Doc. PO Matching Setup"` (Always Ask / Always Receive / Never Receive). + +**Main codeunit:** `EDocPOMatching.Codeunit.al` (codeunit 6196) in `Import/Purchase/PurchaseOrderMatching/`. + +**Extensibility:** Override `IPurchaseOrderProvider.GetPurchaseOrder()` to customize how POs are looked up (e.g., match by vendor + date range instead of order number). + +### V1 interactive order matching (user-driven, post-import) + +This is the **older** system in `OrderMatching/`. It applies after the import pipeline has linked an E-Document to a purchase order (status `"Order Linked"`). The goal is to interactively reconcile imported e-document lines with PO lines so that `"Qty. to Invoice"` is set correctly before posting. + +**Automatic matching** (`EDocLineMatching.MatchAutomatically`) filters PO lines to those with the same unit of measure, direct unit cost, and line discount as the imported line, then applies three matching strategies in order: + +1. **Item Reference lookup** -- if the PO line is type Item, check whether an Item Reference exists for the vendor + imported line number. +2. **Text-to-Account Mapping** -- if the PO line is type G/L Account, check for a mapping from the imported line's number to the PO line's G/L account for this vendor. +3. **String nearness** -- if neither reference matches, compare descriptions with `CalculateStringNearness()`. A score above 80% counts as a match. + +Each successful match creates an `"E-Doc. Order Match"` record linking the e-document line to the PO line with a precise quantity. The `"Matched Quantity"` on the imported line and `"Qty. to Invoice"` on the PO line are updated accordingly. + +**Manual matching** lets users select one or more imported lines and one or more PO lines on the `"E-Doc. Order Line Matching"` page. The framework validates that all selected lines share the same unit cost, discount, and UOM before creating the match. + +**Learn matching rule.** When a match is accepted with the "Learn" flag, the framework creates an Item Reference (for items) or a Text-to-Account Mapping (for G/L accounts) so future automatic matching will recognize the same pattern. + +**Apply to purchase order.** `ApplyToPurchaseOrder()` validates that all imported lines are fully matched, then writes the matched unit costs and discounts to the actual PO lines and links the purchase header to the E-Document via `"E-Document Link"`. + +### Common gotcha: which matching system applies? + +If you are working on the V2.0 import pipeline (Prepare Draft / Finish Draft stages, `"E-Doc. Purchase Line PO Match"` table), you are in the **new** system. If you are working with `"E-Doc. Order Match"` records or the `"E-Doc. Order Line Matching"` page, you are in the **old** system. The two do not share data models, codeunits, or flow paths. Code changes to one should not be applied to the other without understanding which pipeline the document is going through. + +## Copilot PO matching + +When automatic matching (V1 interactive system) leaves unmatched lines, users can invoke Copilot from the matching page. `EDocPOCopilotMatching.MatchWithCopilot()` builds a prompt containing imported line and PO line descriptions, sends it to Azure OpenAI (GPT-4.1 via the `"E-Document Matching Assistance"` Copilot capability), and interprets the response through function-calling tools. + +The Copilot result is **grounded** before being shown: the framework verifies that proposed matches respect the cost difference threshold configured in `"Purchases & Payables Setup"."E-Document Matching Difference"`. Proposals that exceed the threshold are discarded. Accepted proposals are surfaced on a proposal page where the user reviews and confirms. + +## AI tools for import processing + +`EDocAIToolProcessor` is a reusable Copilot orchestrator used during import processing (not order matching). It configures Azure OpenAI with a system prompt, registers tools from `IEDocAISystem` implementations, and executes function calls from the model's response. The `Tools/` subfolder provides four tools: + +- **Historical matching** -- suggests line mappings based on previously accepted matches for the same vendor. +- **G/L account matching** -- proposes G/L accounts based on description similarity to the chart of accounts. +- **Deferral matching** -- suggests deferral codes for lines that appear to represent recurring charges. +- **Similar descriptions** -- finds items or G/L accounts with descriptions similar to the imported line text. + +Each tool implements the `IEDocAISystem` interface and registers via the `OnAfterRegister*` event pattern. The `EDocAIToolProcessor.Process()` method handles token counting (125k input limit), API error handling, and function call dispatch. diff --git a/src/Apps/W1/EDocument/App/src/Service/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Service/docs/CLAUDE.md new file mode 100644 index 0000000000..74cb942c5b --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Service/docs/CLAUDE.md @@ -0,0 +1,27 @@ +# Service + +The E-Document Service module defines how documents are formatted, transmitted, and scheduled. Each service record (`EDocumentService.Table.al`, table 6103) pairs a document format implementation with an integration endpoint, then layers on batch processing, auto-import scheduling, and inbound document processing configuration. + +## How it works + +A service is identified by a `Code[20]` primary key and configures two pluggable dimensions: `Document Format` (an enum selecting the serialization format like PEPPOL or UBL) and `Service Integration V2` (an enum selecting the transport mechanism -- API, file exchange, etc.). When a user selects an integration that is not "No Integration", the system invokes the `IConsentManager` interface on the integration enum to obtain privacy consent before persisting the change. + +For outbound documents, the service controls batch processing via `Use Batch Processing`, `Batch Mode`, `Batch Threshold`, and scheduling fields (`Batch Start Time`, `Batch Minutes between runs`). Enabling batch processing automatically creates a recurrent job queue entry (tracked by `Batch Recurrent Job Id`). For inbound documents, `Auto Import` plus `Import Start Time` and `Import Minutes between runs` configure a separate recurrent job (tracked by `Import Recurrent Job Id`). Both job queue entries are cleaned up automatically when the service is deleted. + +The service also carries extensive inbound processing configuration: `Import Process` selects between Version 1.0 and 2.0 pipelines, `Automatic Import Processing` controls whether documents are fully processed on arrival or parked for manual review, and `Read into Draft Impl.` selects the strategy for converting structured content into draft purchase documents. A set of boolean flags (`Validate Receiving Company`, `Resolve Unit Of Measure`, `Lookup Item Reference`, `Lookup Item GTIN`, `Lookup Account Mapping`, `Validate Line Discount`, `Apply Invoice Discount`, `Verify Totals`, `Verify Purch. Total Amounts`) governs which validation and enrichment steps run during import. + +The `E-Doc. Service Supported Type` table (`EDocServiceSupportedType.Table.al`, table 6122) bridges services to document types -- a service can handle any subset of the `E-Document Type` enum values. The `Service Participant` table (`Participant/ServiceParticipant.Table.al`, table 6104) links customers or vendors to specific services with per-participant identifiers used for electronic addressing (e.g., PEPPOL participant IDs). + +## Things to know + +- Deleting a service checks `IsServiceUsedInActiveWorkflow` first and blocks deletion if any active workflow references it. It then cascade-deletes all supported type records and removes both recurrent job queue entries. + +- The `GetPDFReaderService` procedure auto-creates a hardcoded service with code 'MSEOCADI' for Azure Document Intelligence PDF processing -- this is an internal bootstrap, not user-configurable. + +- `GetDefaultImportParameters` returns different defaults depending on the import process version. Version 1.0 always runs to "Finish draft" step; Version 2.0 respects `Automatic Import Processing` to decide whether to fully process or stop at Unprocessed. + +- The service's `General Journal Template Name` and `General Journal Batch Name` fields enable routing inbound documents to specific journal batches, with validation that the template type is General, Purchases, Payments, Sales, or Cash Receipts. + +- The `Export Eligibility Evaluator` enum field allows plugging in custom logic to determine whether a document qualifies for export through this service, supporting scenarios like conditional routing based on document attributes. + +- The `Embed PDF in export` flag triggers automatic PDF generation from Report Selection as a background process during posting, embedding it into the export file. diff --git a/src/Apps/W1/EDocument/App/src/Service/docs/data-model.md b/src/Apps/W1/EDocument/App/src/Service/docs/data-model.md new file mode 100644 index 0000000000..428a9b83a8 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Service/docs/data-model.md @@ -0,0 +1,43 @@ +# Service data model + +This describes the service configuration data model. For the full cross-module data model, see [../../docs/data-model.md](../../docs/data-model.md). + +## Service configuration and type bridge + +The `E-Document Service` table (6103) is the configuration hub. The `E-Doc. Service Supported Type` table (6122) creates an N:M bridge between services and the `E-Document Type` enum, with a composite primary key of `(E-Document Service Code, Source Document Type)`. This means a single service can handle Sales Invoices, Purchase Invoices, and Credit Memos, while the same document type can be handled by multiple services. + +```mermaid +erDiagram + E-Document-Service ||--o{ E-Doc-Service-Supported-Type : "accepts document types" + E-Document-Service ||--o{ E-Document-Service-Status : "tracks per-document state" + E-Document-Service ||--o{ Service-Participant : "has participants" +``` + +The `E-Document Service Status` table (6138, covered in the Document module) creates the link back to individual E-Documents, using `(E-Document Entry No, E-Document Service Code)` as its composite key. This is the join table that connects the Document and Service modules at runtime. + +## Participant linking + +The `Service Participant` table (6104) uses a three-part primary key: `(Service, Participant Type, Participant)`. The `Participant Type` field uses the `E-Document Source Type` enum (Customer or Vendor), and `Participant` is a polymorphic `Code[20]` with conditional table relations -- it points to the Customer table when the type is Customer, and to the Vendor table when the type is Vendor. + +```mermaid +erDiagram + E-Document-Service ||--o{ Service-Participant : "registered participants" + Service-Participant }o--|| Customer : "if type = Customer" + Service-Participant }o--|| Vendor : "if type = Vendor" +``` + +The `Participant Identifier` field (Text[200]) stores the external electronic address -- things like PEPPOL IDs or other scheme-specific identifiers. This is the value that gets embedded in the exported document to identify the recipient. The table is Public and Extensible, meaning ISVs can add fields for additional addressing schemes. + +## Job queue integration + +The service stores two Guid fields -- `Batch Recurrent Job Id` and `Import Recurrent Job Id` -- that reference Job Queue Entries. These are not table relations enforced at the schema level; instead, the `E-Document Background Jobs` codeunit manages their lifecycle programmatically. When `Use Batch Processing` or `Auto Import` is toggled, the OnValidate triggers call into the background jobs codeunit to create or update the corresponding job queue entry. On service deletion, both jobs are explicitly removed. + +The batch and import jobs use separate scheduling configurations: batch jobs have `Batch Start Time` and `Batch Minutes between runs` (default 1440 = daily); import jobs have `Import Start Time` and `Import Minutes between runs` (also default daily). This separation means import polling and batch sending can run on completely independent schedules. + +## Design decisions and gotchas + +- The `Service Integration` field (v1) is being replaced by `Service Integration V2`. The old field is pending removal (CLEAN26/29 tags). During the transition, both fields may coexist, and code paths check both. The v2 field uses the `Service Integration` enum which implements `IConsentManager` for privacy consent. + +- The supported type bridge table has `ReplicateData = false`, meaning it is not replicated across environments. This is intentional -- service configurations are environment-specific. + +- The service carries no direct reference to E-Document records. The relationship is always mediated through `E-Document Service Status`, which is owned by the Document module. This keeps the service as a pure configuration entity. diff --git a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al index 3194fe1a74..8bd9a0f5ba 100644 --- a/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Setup/EDocumentUpgrade.Codeunit.al @@ -4,6 +4,9 @@ // ------------------------------------------------------------------------------------------------ namespace Microsoft.eServices.EDocument; +#if not CLEAN29 +using Microsoft.eServices.EDocument.Processing.Import; +#endif using System.Upgrade; codeunit 6168 "E-Document Upgrade" @@ -16,6 +19,9 @@ codeunit 6168 "E-Document Upgrade" trigger OnUpgradePerCompany() begin UpgradeLogURLMaxLength(); +#if not CLEAN29 + UpgradeProcessDraftEnum(); +#endif end; local procedure UpgradeLogURLMaxLength() @@ -39,6 +45,7 @@ codeunit 6168 "E-Document Upgrade" local procedure RegisterPerCompanyTags(var PerCompanyUpgradeTags: List of [Code[250]]) begin PerCompanyUpgradeTags.Add(GetUpgradeLogURLMaxLengthUpgradeTag()); + PerCompanyUpgradeTags.Add(GetUpgradeProcessDraftEnumTag()); end; internal procedure GetUpgradeLogURLMaxLengthUpgradeTag(): Code[250] @@ -46,4 +53,26 @@ codeunit 6168 "E-Document Upgrade" exit('MS-540448-LogURLMaxLength-20240813'); end; +#if not CLEAN29 + local procedure UpgradeProcessDraftEnum() + var + EDocument: Record "E-Document"; + UpgradeTag: Codeunit "Upgrade Tag"; + begin + if UpgradeTag.HasUpgradeTag(GetUpgradeProcessDraftEnumTag()) then + exit; + + EDocument.SetRange("Process Draft Impl.", "E-Doc. Process Draft"::"Purchase Document"); + if not EDocument.IsEmpty() then + EDocument.ModifyAll("Process Draft Impl.", "E-Doc. Process Draft"::"Purchase Invoice"); + + UpgradeTag.SetUpgradeTag(GetUpgradeProcessDraftEnumTag()); + end; +#endif + + internal procedure GetUpgradeProcessDraftEnumTag(): Code[250] + begin + exit('MS-EDoc-ProcessDraftEnum-20260407'); + end; + } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/App/src/Setup/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Setup/docs/CLAUDE.md new file mode 100644 index 0000000000..d4ba7e7f41 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Setup/docs/CLAUDE.md @@ -0,0 +1,21 @@ +# Setup + +Installation, upgrade, and consent management for the E-Document Core app. This module handles one-time setup tasks that run when the extension is first installed or upgraded to a new version. + +## How it works + +**`EDocumentSetup`** (Install codeunit) runs `OnInstallAppPerCompany` and does two things: (1) creates the Workflow Table Relation records that link `E-Document` and `E-Document Service Status` tables bidirectionally by entry number -- this is required for the Workflow engine to navigate between the two tables during event processing; (2) registers four tables (`E-Document Log`, `E-Document Integration Log`, `E-Doc. Data Storage`, `E-Doc. Mapping Log`) with the Retention Policy framework so administrators can configure automatic cleanup of historical data. + +**`EDocumentUpgrade`** runs on version upgrades. Currently it has one migration: `UpgradeLogURLMaxLength`, which copies the old `URL` field to a new `"Request URL"` field on `E-Document Integration Log` using `DataTransfer` (bulk copy, no row-by-row loop). The upgrade is gated by an upgrade tag (`MS-540448-LogURLMaxLength-20240813`) and registered via `OnGetPerCompanyUpgradeTags`. + +**`ConsentManagerDefaultImpl`** implements the `IConsentManager` interface with a standard privacy consent dialog. When a user first configures an E-Document connector, this prompts them to acknowledge that third-party systems may have different compliance and privacy standards. Connector implementations can substitute their own consent manager via the interface. + +## Things to know + +- The install codeunit uses `if Insert() then;` (swallow-failure pattern) for workflow table relations because the install runs on every upgrade, not just first install, and the records may already exist. +- Retention policies are created disabled by default (`Enabled = false`) with `"Apply to all records" = true`. Administrators must explicitly enable them and set retention periods. +- The upgrade tag format includes a work item number and date (`MS-540448-LogURLMaxLength-20240813`), following BC's standard upgrade tag conventions. +- `DataTransfer.CopyFields` is used instead of record-by-record modification for the URL migration -- this is significantly faster on large log tables. +- The consent text is a single hardcoded label. It does not vary by connector -- connectors that need specific consent language should implement their own `IConsentManager`. + +See the [app-level CLAUDE.md](../../docs/CLAUDE.md) for broader architecture context. diff --git a/src/Apps/W1/EDocument/App/src/Workflow/EDocumentWorkFlowProcessing.Codeunit.al b/src/Apps/W1/EDocument/App/src/Workflow/EDocumentWorkFlowProcessing.Codeunit.al index dacc35008a..33bdffbe1a 100644 --- a/src/Apps/W1/EDocument/App/src/Workflow/EDocumentWorkFlowProcessing.Codeunit.al +++ b/src/Apps/W1/EDocument/App/src/Workflow/EDocumentWorkFlowProcessing.Codeunit.al @@ -187,7 +187,7 @@ codeunit 6135 "E-Document WorkFlow Processing" WorkflowManagement: Codeunit "Workflow Management"; EDocumentWorkflowSetup: Codeunit "E-Document Workflow Setup"; Telemetry: Codeunit Telemetry; - NoEDocumentServiceFoundINPrevResponseLbl: Label 'No E-Document Service found in previous Send or Export response step in workflow.'; + NoEDocumentServiceFoundINPrevResponseLbl: Label 'No E-Document Service found in previous Send or Export response step in workflow.', Locked = true; begin PrevWorkflowStepInstance.SetFilter("Function Name", '%1|%2', EDocumentWorkflowSetup.EDocSendEDocResponseCode(), EDocumentWorkflowSetup.ResponseEDocExport()); while WorkflowManagement.FindResponse(PrevWorkflowStepInstance, WorkflowStepInstance) do begin @@ -204,7 +204,7 @@ codeunit 6135 "E-Document WorkFlow Processing" end; end; - internal procedure GetServicesFromEntryPointResponseInWorkflow(WorkFlow: Record Workflow; var EDocumentService: Record "E-Document Service"): Boolean + procedure GetServicesFromEntryPointResponseInWorkflow(WorkFlow: Record Workflow; var EDocumentService: Record "E-Document Service"): Boolean var WorkflowStepArgument: Record "Workflow Step Argument"; WorkflowStep, WorkflowStepEvent : Record "Workflow Step"; @@ -259,7 +259,7 @@ codeunit 6135 "E-Document WorkFlow Processing" var EDocumentServiceStatus: Record "E-Document Service Status"; Telemetry: Codeunit Telemetry; - WrongWorkflowEventRecordTypeErr: Label 'The record type %1 is not supported in E-Document workflow events.', Comment = '%1 - Table ID'; + WrongWorkflowEventRecordTypeErr: Label 'The record type %1 is not supported in E-Document workflow events.', Comment = '%1 - Table ID', Locked = true; begin case RecordRef.Number() of Database::"E-Document": @@ -320,7 +320,7 @@ codeunit 6135 "E-Document WorkFlow Processing" WorkflowManagement: Codeunit "Workflow Management"; EDocumentWorkflowSetup: Codeunit "E-Document Workflow Setup"; Telemetry: Codeunit Telemetry; - EDocTelemetryNoFilterForNextEventLbl: Label 'No filter set on E-Document to execute next workflow step.'; + EDocTelemetryNoFilterForNextEventLbl: Label 'No filter set on E-Document to execute next workflow step.', Locked = true; begin // Commit before execute next workflow step Commit(); diff --git a/src/Apps/W1/EDocument/App/src/Workflow/docs/CLAUDE.md b/src/Apps/W1/EDocument/App/src/Workflow/docs/CLAUDE.md new file mode 100644 index 0000000000..07bb44aa38 --- /dev/null +++ b/src/Apps/W1/EDocument/App/src/Workflow/docs/CLAUDE.md @@ -0,0 +1,24 @@ +# Workflow + +Workflow orchestration for outbound E-Documents -- wires the E-Document lifecycle into BC's general-purpose Workflow engine. This module defines the events, responses, and templates that allow configurable processing chains instead of hardcoded send logic. The boundary is clear: this module handles *when* and *in what order* things happen; the actual export/send logic lives in the Processing and Integration modules. + +## How it works + +E-Documents use Document Sending Profile to select a Workflow Code, which defines the processing chain. When a document is posted, `EDocumentCreatedFlow` runs as a Job Queue Entry, firing the `EDOCCREATEDEVENT` workflow event. The workflow engine then walks the step chain, executing responses in order. + +`EDocumentWorkFlowSetup` registers four events (`EDocCreated`, `EDocStatusChanged`, `EDocImported`, `EDocExported`) and five responses (`Send`, `Export`, `Import`, `EmailEDoc`, `EmailPDFAndEDoc`) into the BC workflow library via event subscribers. It also installs two built-in templates: "Send to one service" (EDOCTOS) and "Send to multiple services" (EDOCTOM). The multi-service template chains two Send responses off the same entry point event, each targeting a different service. + +`EDocumentWorkFlowProcessing` is the response executor. When the workflow engine dispatches a response, the `ExecuteEdocWorkflowResponses` subscriber routes to the appropriate method: `SendEDocument`, `ExportEDocument`, or `SendEDocFromEmail`. Each method resolves the E-Document Service from the `Workflow Step Argument` (extended with an `"E-Document Service"` field via table extension) and delegates to the export/integration layer. After execution, `HandleNextEvent` fires `EDocStatusChanged` to advance the workflow to the next step, enabling multi-step chains like Export -> Send -> Email. + +For async services, after a send returns `IsAsync = true`, a background job is scheduled to poll `GetResponse` until the service confirms delivery. + +## Things to know + +- The `Workflow Step Argument` table extension (field `"E-Document Service"`) is the critical link between a workflow response step and the E-Document Service it targets. Each response step in the chain can target a different service. +- `ValidateFlowStep` ensures the workflow step instance matches the E-Document's stored `"Workflow Code"` -- this prevents cross-workflow execution when multiple workflows are enabled. +- The `DoSend` path has an optimization: if the document was already exported (status = `Exported` from a prior Export step), it skips re-export and goes straight to the integration send. +- Batch processing has three modes: `Recurrent` (exits immediately, a separate scheduled job handles it), `Threshold` (waits until N documents of the same type accumulate), and custom (raises `OnBatchSendWithCustomBatchMode` for extensions). +- Email responses (`SendEDocFromEmail`) look up the previous Send or Export response in the workflow to find which service produced the document -- they don't require their own service argument. +- The `EDocWorkflowStepArgumentArch` table extension mirrors the argument field to the archive table, preserving which service was used after workflow archival. + +See the [app-level CLAUDE.md](../../docs/CLAUDE.md) for broader architecture context. diff --git a/src/Apps/W1/EDocument/Demo Data/3.Transactions/CreateEDocumentTransactions.Codeunit.al b/src/Apps/W1/EDocument/Demo Data/3.Transactions/CreateEDocumentTransactions.Codeunit.al index 8fe4b88f63..20117a3c73 100644 --- a/src/Apps/W1/EDocument/Demo Data/3.Transactions/CreateEDocumentTransactions.Codeunit.al +++ b/src/Apps/W1/EDocument/Demo Data/3.Transactions/CreateEDocumentTransactions.Codeunit.al @@ -430,7 +430,7 @@ codeunit 5376 "Create E-Document Transactions" local procedure CreateEDocument(Filetxt: Text) var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; TempBlob: Codeunit "Temp Blob"; EDocImport: Codeunit "E-Doc. Import"; EDocImportHelper: Codeunit "E-Document Import Helper"; @@ -440,8 +440,8 @@ codeunit 5376 "Create E-Document Transactions" TempBlob.CreateOutStream(XMLOutStream); XMLOutStream.WriteText(StrSubstNo(Filetxt)); EDocument := CreateEDoc(TempBlob); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); end; local procedure CreateEDoc(var TempBlob: Codeunit "Temp Blob"): Record "E-Document"; diff --git a/src/Apps/W1/EDocument/Demo Data/EDocFromResourcesImpl/EDocFromResourceHelper.Codeunit.al b/src/Apps/W1/EDocument/Demo Data/EDocFromResourcesImpl/EDocFromResourceHelper.Codeunit.al index 14b8121898..5e8962abff 100644 --- a/src/Apps/W1/EDocument/Demo Data/EDocFromResourcesImpl/EDocFromResourceHelper.Codeunit.al +++ b/src/Apps/W1/EDocument/Demo Data/EDocFromResourcesImpl/EDocFromResourceHelper.Codeunit.al @@ -64,7 +64,7 @@ codeunit 5405 "E-Doc. From Resource Helper" local procedure ImportDocument(EDocumentService: Record "E-Document Service"; DocumentPath: Text) EDocument: Record "E-Document"; var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; ResInStream: InStream; FileName: Text; @@ -80,9 +80,9 @@ codeunit 5405 "E-Doc. From Resource Helper" Enum::"E-Doc. File Format"::PDF, FileName, ResInStream); EDocument."Structure Data Impl." := Enum::"Structure Received E-Doc."::"ADI Mock"; EDocument.Modify(); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Read into Draft"; + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Read into Draft"; EDocImport.ProcessIncomingEDocument( - EDocument, EDocumentService, EDocImportParameters); + EDocument, EDocumentService, TempEDocImportParameters); end; local procedure MapPurchaseDocumentDraftLines(EDocument: Record "E-Document") @@ -147,12 +147,12 @@ codeunit 5405 "E-Doc. From Resource Helper" local procedure FinalizeDraft(EDocumentService: Record "E-Document Service"; EDocument: Record "E-Document") var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; begin - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; EDocImport.ProcessIncomingEDocument( - EDocument, EDocumentService, EDocImportParameters); + EDocument, EDocumentService, TempEDocImportParameters); end; local procedure PostPurchInvoice(EDocument: Record "E-Document") PurchInvHeader: Record "Purch. Inv. Header"; diff --git a/src/Apps/W1/EDocument/Demo Data/EDocumentInvoices/ContosoInbInvHandler.Codeunit.al b/src/Apps/W1/EDocument/Demo Data/EDocumentInvoices/ContosoInbInvHandler.Codeunit.al index c811e2211f..02e9101a50 100644 --- a/src/Apps/W1/EDocument/Demo Data/EDocumentInvoices/ContosoInbInvHandler.Codeunit.al +++ b/src/Apps/W1/EDocument/Demo Data/EDocumentInvoices/ContosoInbInvHandler.Codeunit.al @@ -132,7 +132,7 @@ codeunit 5392 "Contoso Inb.Inv. Handler" implements IStructureReceivedEDocument, local procedure GetDefaultIProcessStructuredDataImplementation(): Interface IProcessStructuredData begin - exit(Enum::"E-Doc. Process Draft"::"Purchase Document"); + exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice"); end; } diff --git a/src/Apps/W1/EDocument/Demo Data/EDocumentInvoices/ContosoInboundEDocument.Codeunit.al b/src/Apps/W1/EDocument/Demo Data/EDocumentInvoices/ContosoInboundEDocument.Codeunit.al index 81be39801f..cdd150b5e7 100644 --- a/src/Apps/W1/EDocument/Demo Data/EDocumentInvoices/ContosoInboundEDocument.Codeunit.al +++ b/src/Apps/W1/EDocument/Demo Data/EDocumentInvoices/ContosoInboundEDocument.Codeunit.al @@ -128,11 +128,11 @@ codeunit 5429 "Contoso Inbound E-Document" local procedure ProcessEDocument(EDocument: Record "E-Document") var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; begin - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); end; local procedure PostPurchaseInvoice(EDocEntryNo: Integer) diff --git a/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-header-full.json b/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-header-full.json new file mode 100644 index 0000000000..38e4584d94 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-header-full.json @@ -0,0 +1,81 @@ +{ + "id": "MLLM-INV-001", + "issue_date": "2024-03-15", + "due_date": "2024-04-15", + "document_currency_code": "XYZ", + "order_reference": { + "id": "PO-5678" + }, + "payment_terms": { + "note": "Net 30" + }, + "accounting_supplier_party": { + "party": { + "party_name": { + "name": "Contoso Supplies Ltd." + }, + "postal_address": { + "street_name": "123 Bill Ave", + "city_name": "Seattle", + "postal_zone": "98101", + "country": { + "identification_code": "US" + } + }, + "party_tax_scheme": { + "company_id": "US-VAT-12345" + }, + "contact": { + "name": "John Doe" + } + } + }, + "accounting_customer_party": { + "party": { + "party_name": { + "name": "Microsoft Corporation" + }, + "postal_address": { + "street_name": "456 Main St", + "city_name": "Redmond", + "postal_zone": "98052", + "country": { + "identification_code": "US" + } + }, + "party_tax_scheme": { + "company_id": "US-VAT-67890" + } + } + }, + "delivery": { + "delivery_location": { + "address": { + "street_name": "789 Ship Rd", + "city_name": "Bellevue", + "postal_zone": "98004", + "country": { + "identification_code": "US" + } + } + }, + "delivery_party": { + "party_name": { + "name": "Warehouse Team" + } + } + }, + "payment_means": { + "payee_financial_account": { + "name": "Contoso Billing Dept" + } + }, + "tax_total": { + "tax_amount": 37.5 + }, + "legal_monetary_total": { + "tax_exclusive_amount": 250.0, + "allowance_total_amount": 5.0, + "payable_amount": 287.5 + } +} diff --git a/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-invoice-empty.json b/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-invoice-empty.json new file mode 100644 index 0000000000..05641b2c6c --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-invoice-empty.json @@ -0,0 +1,3 @@ +{ + "invoice_line": [] +} diff --git a/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-invoice-valid-0.json b/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-invoice-valid-0.json new file mode 100644 index 0000000000..89a3b10fe9 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-invoice-valid-0.json @@ -0,0 +1,155 @@ +{ + "id": "MLLM-INV-001", + "issue_date": "2024-03-15", + "due_date": "2024-04-15", + "document_currency_code": "XYZ", + "order_reference": { + "id": "PO-5678" + }, + "payment_terms": { + "note": "Net 30" + }, + "accounting_supplier_party": { + "party": { + "party_name": { + "name": "Contoso Supplies Ltd." + }, + "postal_address": { + "street_name": "123 Bill Ave", + "city_name": "Seattle", + "postal_zone": "98101", + "country": { + "identification_code": "US" + } + }, + "party_tax_scheme": { + "company_id": "US-VAT-12345" + }, + "contact": { + "name": "John Doe" + } + } + }, + "accounting_customer_party": { + "party": { + "party_name": { + "name": "Microsoft Corporation" + }, + "postal_address": { + "street_name": "456 Main St", + "city_name": "Redmond", + "postal_zone": "98052", + "country": { + "identification_code": "US" + } + }, + "party_tax_scheme": { + "company_id": "US-VAT-67890" + } + } + }, + "delivery": { + "delivery_location": { + "address": { + "street_name": "789 Ship Rd", + "city_name": "Bellevue", + "postal_zone": "98004", + "country": { + "identification_code": "US" + } + } + }, + "delivery_party": { + "party_name": { + "name": "Warehouse Team" + } + } + }, + "payment_means": { + "payee_financial_account": { + "name": "Contoso Billing Dept" + } + }, + "tax_total": { + "tax_amount": 37.50 + }, + "legal_monetary_total": { + "tax_exclusive_amount": 250.00, + "allowance_total_amount": 5.00, + "payable_amount": 287.50 + }, + "invoice_line": [ + { + "item": { + "name": "Consulting Services", + "sellers_item_identification": { + "id": "SVC-001" + }, + "classified_tax_category": { + "percent": 15.0 + } + }, + "invoiced_quantity": { + "value": 5, + "unit_code": "HRS" + }, + "price": { + "price_amount": 40.00 + }, + "line_extension_amount": 200.00, + "allowance_charge": { + "amount": { + "value": 5.00 + } + } + }, + { + "item": { + "name": "Office Supplies", + "sellers_item_identification": { + "id": "MAT-002" + }, + "classified_tax_category": { + "percent": 10.0 + } + }, + "invoiced_quantity": { + "value": 10, + "unit_code": "PCS" + }, + "price": { + "price_amount": 3.00 + }, + "line_extension_amount": 30.00, + "allowance_charge": { + "amount": { + "value": 0.00 + } + } + }, + { + "item": { + "name": "Express Delivery", + "sellers_item_identification": { + "id": "DLV-003" + }, + "classified_tax_category": { + "percent": 15.0 + } + }, + "invoiced_quantity": { + "value": 1, + "unit_code": "EA" + }, + "price": { + "price_amount": 20.00 + }, + "line_extension_amount": 20.00, + "allowance_charge": { + "amount": { + "value": 0.00 + } + } + } + ] +} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-lines-three.json b/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-lines-three.json new file mode 100644 index 0000000000..b08b843850 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/mllm/mllm-lines-three.json @@ -0,0 +1,74 @@ +[ + { + "item": { + "name": "Consulting Services", + "sellers_item_identification": { + "id": "SVC-001" + }, + "classified_tax_category": { + "percent": 15.0 + } + }, + "invoiced_quantity": { + "value": 5, + "unit_code": "HRS" + }, + "price": { + "price_amount": 40.0 + }, + "line_extension_amount": 200.0, + "allowance_charge": { + "amount": { + "value": 5.0 + } + } + }, + { + "item": { + "name": "Office Supplies", + "sellers_item_identification": { + "id": "MAT-002" + }, + "classified_tax_category": { + "percent": 10.0 + } + }, + "invoiced_quantity": { + "value": 10, + "unit_code": "PCS" + }, + "price": { + "price_amount": 3.0 + }, + "line_extension_amount": 30.0, + "allowance_charge": { + "amount": { + "value": 0.0 + } + } + }, + { + "item": { + "name": "Express Delivery", + "sellers_item_identification": { + "id": "DLV-003" + }, + "classified_tax_category": { + "percent": 15.0 + } + }, + "invoiced_quantity": { + "value": 1, + "unit_code": "EA" + }, + "price": { + "price_amount": 20.0 + }, + "line_extension_amount": 20.0, + "allowance_charge": { + "amount": { + "value": 0.0 + } + } + } +] diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-creditnote-0.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-creditnote-0.xml new file mode 100644 index 0000000000..357def3f92 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-creditnote-0.xml @@ -0,0 +1,133 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + CN-5001 + 2026-02-15 + 381 + XYZ + 1 + + 5 + + + + 103033 + 2026-01-22 + + + + + 1234567890128 + + CRONUS International + + + Main Street, 14 + Birmingham + B27 4KT + + GB + + + + GB123456789 + + VAT + + + + CRONUS International + 123456789 + + + Jim Olive + JO@contoso.com + + + + + + 789456278 + + 8712345000004 + + + The Cannon Group PLC + + + 192 Market Square + Birmingham + B27 4KT + + GB + + + + GB789456278 + + VAT + + + + The Cannon Group PLC + 789456278 + + + Mr. Andy Teal + + + + + 30 + 2026-03-15 + + + 500 + + 2000 + 500 + + S + 25 + + VAT + + + + + + 2000 + 2000 + 2500 + 0 + 2500 + + + 10000 + 1 + 2000 + + Bicycle - Return + + 1000 + + + S + 25 + + VAT + + + + + 2000.00 + 1 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-creditnote-no-duedate.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-creditnote-no-duedate.xml new file mode 100644 index 0000000000..ba95607394 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-creditnote-no-duedate.xml @@ -0,0 +1,215 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + Snippet1 + 2017-11-13 + 381 + Please note we have a new phone number: 22 22 22 22 + EUR + 4025:123:4343 + 0150abc + + + Snippet1 + + + + + 9482348239847239874 + + 99887766 + + + SupplierTradingName Ltd. + + + Main street 1 + Postbox 123 + London + GB 123 EW + + GB + + + + GB1232434 + + VAT + + + + SupplierOfficialName Ltd + GB983294 + + + + + + FR23342 + + FR23342 + + + BuyerTradingName AS + + + Hovedgatan 32 + Po box 878 + Stockholm + 456 34 + + SE + + + + SE4598375937 + + VAT + + + + Buyer Official Name + 39937423947 + + + Lisa Johnson + 23434234 + lj@buyer.se + + + + + 2017-11-01 + + 9483759475923478 + + Delivery street 2 + Building 56 + Stockholm + 21234 + + SE + + + + + + Delivery party Name + + + + + 30 + Snippet1 + + IBAN32423940 + AccountName + + BIC324098 + + + + + Payment within 10 days, 2% discount + + + true + Insurance + 25 + + S + 25.0 + + VAT + + + + + 331.25 + + 1325 + 331.25 + + S + 25.0 + + VAT + + + + + + 1300 + 1325 + 1656.25 + 25 + 1656.25 + + + + 1 + 7 + 2800 + Konteringsstreng + + 123 + + + Description of item + item name + + 21382183120983 + + + NO + + + 09348023 + + + S + 25.0 + + VAT + + + + + 400 + + + + 2 + -3 + -1500 + + 123 + + + Description 2 + item name 2 + + 21382183120983 + + + NO + + + 09348023 + + + S + 25.0 + + VAT + + + + + 500 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-allowance.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-allowance.xml new file mode 100644 index 0000000000..2632750efd --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-allowance.xml @@ -0,0 +1,370 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + Snippet1 + 2017-11-13 + 2017-12-01 + 380 + Please note we have a new phone number: 22 22 22 22 + 2017-12-01 + EUR + SEK + 4025:123:4343 + 0150abc + + 2017-12-01 + 2017-12-31 + + + framework no 1 + + + DR35141 + 130 + + + ts12345 + Technical specification + + + www.techspec.no + + + + + + 7300010000001 + + 99887766 + + + SupplierTradingName Ltd. + + + Main street 1 + Postbox 123 + London + GB 123 EW + + GB + + + + GB1232434 + + VAT + + + + + SupplierOfficialName Ltd + GB983294 + AdditionalLegalInformation + + + + + + + + 4598375937 + + 4598375937 + + + BuyerTradingName AS + + + Hovedgatan 32 + Po box 878 + Stockholm + 456 34 + Södermalm + + + SE + + + + SE4598375937 + + VAT + + + + Buyer Official Name + 39937423947 + + + Lisa Johnson + 23434234 + lj@buyer.se + + + + + 2017-11-01 + + 7300010000001 + + Delivery street 2 + Building 56 + Stockholm + 21234 + Södermalm + + Gate 15 + + + SE + + + + + + Delivery party Name + + + + + 30 + Snippet1 + + IBAN32423940 + AccountName + + BIC324098 + + + + + Payment within 10 days, 2% discount + + + + true + CG + Cleaning + 20 + 200 + 1000 + + S + 25 + + VAT + + + + + + false + 95 + Discount + 200 + + S + 25 + + VAT + + + + + + 1225.00 + + 4900.0 + 1225 + + S + 25 + + VAT + + + + + 1000.0 + 0 + + E + 0 + Reason for tax exempt + + VAT + + + + + + 9324.00 + + + 5900 + 5900 + 7125 + 200 + 200 + 1000 + 6125.00 + + + 1 + Testing note on line level + 10 + 4000.00 + Konteringsstreng + + true + CG + Cleaning + 1 + 1 + 100 + + + false + 95 + Discount + 101 + + + Description of item + item name + + + 97iugug876 + + + NO + + + 09348023 + + + S + 25.0 + + VAT + + + + + + + 410 + 1 + + false + 40 + 450 + + + + + + 2 + Testing note on line level + + 10 + 1000.00 + + Konteringsstreng + + 2017-12-01 + 2017-12-05 + + + 124 + + + + Description of item + item name + + 97iugug876 + + + 86776 + + + E + 0.0 + + VAT + + + + AdditionalItemName + AdditionalItemValue + + + + 200 + 2 + + + + 3 + Testing note on line level + 10 + 900.00 + Konteringsstreng + + 2017-12-01 + 2017-12-05 + + + 124 + + + + true + CG + Charge + 1 + 1 + 100 + + + false + 95 + Discount + 101 + + + + Description of item + item name + + 97iugug876 + + + + 86776 + + + S + 25.0 + + VAT + + + + AdditionalItemName + AdditionalItemValue + + + + + 100 + + + + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-attachment.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-attachment.xml new file mode 100644 index 0000000000..2932a22371 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-attachment.xml @@ -0,0 +1,132 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + INV-ATT-001 + 2026-03-01 + 2026-04-01 + 380 + XYZ + 1 + + + att-001 + Invoice PDF copy + + JVBERi0xLjQKMSAwIG9iago8PCAvVHlwZSAvQ2F0YWxvZyAvUGFnZXMgMiAwIFIgPj4KZW5kb2Jq + + + + + att-002 + Photo evidence + + iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg== + + + + + ext-001 + External spec + + + https://example.com/spec.pdf + + + + + + DR99999 + 130 + + + + 1234567890128 + + Attachment Supplier Ltd. + + + Test Street 1 + London + EC1A 1BB + + GB + + + + GB111222333 + + VAT + + + + Attachment Supplier Ltd. + + + + + + 8712345000004 + + Test Buyer Corp + + + Buyer Street 2 + Birmingham + B27 4KT + + GB + + + + GB444555666 + + VAT + + + + Test Buyer Corp + + + + + 125 + + 500 + 125 + + S + 25 + + VAT + + + + + + 500 + 500 + 625 + 625 + + + 1 + 5 + 500 + + Test Item + + S + 25 + + VAT + + + + + 100.00 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-basic.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-basic.xml new file mode 100644 index 0000000000..db325c80ca --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-basic.xml @@ -0,0 +1,210 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + Snippet1 + 2017-11-13 + 2017-12-01 + 380 + EUR + 4025:123:4343 + 0150abc + + + 9482348239847 + + 99887766 + + + SupplierTradingName Ltd. + + + Main street 1 + Postbox 123 + London + GB 123 EW + + GB + + + + GB1232434 + + VAT + + + + SupplierOfficialName Ltd + GB983294 + + + + + + FR23342 + + FR23342 + + + BuyerTradingName AS + + + Hovedgatan 32 + Po box 878 + Stockholm + 456 34 + + SE + + + + SE4598375937 + + VAT + + + + Buyer Official Name + 39937423947 + + + Lisa Johnson + 23434234 + lj@buyer.se + + + + + 2017-11-01 + + 9483759475923478 + + Delivery street 2 + Building 56 + Stockholm + 21234 + + SE + + + + + + Delivery party Name + + + + + 30 + Snippet1 + + IBAN32423940 + AccountName + + BIC324098 + + + + + Payment within 10 days, 2% discount + + + true + Insurance + 25 + + S + 25.0 + + VAT + + + + + 331.25 + + 1325 + 331.25 + + S + 25.0 + + VAT + + + + + + 1300 + 1325 + 1656.25 + 25 + 1656.25 + + + + 1 + 7 + 2800 + Konteringsstreng + + 123 + + + Description of item + item name + + 21382183120983 + + + NO + + + 09348023 + + + S + 25.0 + + VAT + + + + + 400 + + + + 2 + -3 + -1500 + + 123 + + + Description 2 + item name 2 + + 21382183120983 + + + NO + + + 09348023 + + + S + 25.0 + + VAT + + + + + 500 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-charges.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-charges.xml new file mode 100644 index 0000000000..e9063f5f9b --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-charges.xml @@ -0,0 +1,147 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + INV-CHARGE-001 + 2026-03-01 + 2026-04-01 + 380 + XYZ + 1 + + PO-100 + + + + 1234567890128 + + CRONUS International + + + Main Street, 14 + Birmingham + B27 4KT + + GB + + + + GB123456789 + + VAT + + + + CRONUS International + 123456789 + + + Jim Olive + + + + + + 8712345000004 + + The Cannon Group PLC + + + 192 Market Square + Birmingham + B27 4KT + + GB + + + + GB789456278 + + VAT + + + + The Cannon Group PLC + + + + + + false + 95 + Early Payment Discount + 200 + + S + 25 + + VAT + + + + + + true + FC + Freight charge + 150 + + S + 25 + + VAT + + + + + 250 + + 1000 + 250 + + S + 25 + + VAT + + + + + + 1000 + 950 + 1200 + 200 + 150 + 1200 + + + 10000 + 2 + 1000 + + Widget + + WIDGET-001 + + + 7350053850019 + + + S + 25 + + VAT + + + + + 500.00 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-description-fallback.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-description-fallback.xml new file mode 100644 index 0000000000..2cb5af0543 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-description-fallback.xml @@ -0,0 +1,125 @@ + + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + INV-DESC-001 + 2026-03-01 + 2026-04-01 + 380 + XYZ + 1 + + + 1234567890128 + + Description Test Supplier + + + Test Street 1 + London + EC1A 1BB + + GB + + + + GB111222333 + + VAT + + + + Description Test Supplier + + + + + + 8712345000004 + + Test Buyer Corp + + + Buyer Street 2 + Birmingham + B27 4KT + + GB + + + + Test Buyer Corp + + + + + 75 + + + 300 + 300 + 375 + 375 + + + + 1 + 1 + 100 + + Widget Alpha + + S + 25 + + VAT + + + + + 100.00 + + + + + 2 + 1 + 100 + + Detailed description of Widget Beta for testing fallback + + S + 25 + + VAT + + + + + 100.00 + + + + + 3 + 1 + 100 + + This longer description should NOT be used when Name is present + Widget Gamma + + S + 25 + + VAT + + + + + 100.00 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-hierarchical-lineids.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-hierarchical-lineids.xml new file mode 100644 index 0000000000..0ed9e1978b --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-hierarchical-lineids.xml @@ -0,0 +1,177 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + 103033 + 2026-01-22 + 2026-02-22 + 380 + XYZ + 1 + + 2 + + + 103033 + + + 103033 + + + dGVzdA== + + + + + 1234567890128 + + CRONUS International + + + Main Street, 14 + Birmingham + B27 4KT + + GB + + + + GB123456789 + + VAT + + + + CRONUS International + 123456789 + + + Jim Olive + JO@contoso.com + + + + + + 789456278 + + 8712345000004 + + + The Cannon Group PLC + + + 192 Market Square + Birmingham + B27 4KT + + GB + + + + GB789456278 + + VAT + + + + The Cannon Group PLC + + + + + + CRONUS International + + + GB123456789 + + + + 30 + 2026-02-22 + + GB12CPBK08929965044991 + + BG99999 + + + + + 1 Month/2% 8 days + + + 1000 + + 4000 + 1000 + + S + 25 + + VAT + + + + + + 14000 + 14000 + 14140 + 0 + 0.00 + 0 + 14140 + + + 1.1 + Item + 1 + 4000 + + Bicycle + + 1000 + + + S + 25 + + VAT + + + + + 4000.00 + 1 + + + + 1.2 + Item + 2 + 10000 + + Bicycle v2 + + 2000 + + + S + 25 + + VAT + + + + + 5000.00 + 2 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-payee-party.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-payee-party.xml new file mode 100644 index 0000000000..44aa3abada --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-payee-party.xml @@ -0,0 +1,94 @@ + + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + INV-PAYEE-001 + 2026-03-01 + 2026-04-01 + 380 + XYZ + 1 + + + 1234567890128 + + Original Supplier Name + + + Supplier Street 1 + London + EC1A 1BB + + GB + + + + GB111222333 + + VAT + + + + Original Supplier Name + + + + + + + Factoring Company GmbH + + + DE999888777 + + + + + 8712345000004 + + Test Buyer Corp + + + Buyer Street 2 + Birmingham + B27 4KT + + GB + + + + Test Buyer Corp + + + + + 50 + + + 200 + 200 + 250 + 250 + + + 1 + 2 + 200 + + Test Item + + S + 25 + + VAT + + + + + 100.00 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-prefixed-ns.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-prefixed-ns.xml new file mode 100644 index 0000000000..4dde654048 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-prefixed-ns.xml @@ -0,0 +1,201 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + 103033 + 2026-01-22 + 2026-02-22 + 380 + XYZ + 1 + + 2 + + + 103033 + + + 103033 + + + + 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MzAxMjE5MDBaKS9Nb2REYXRlKEQ6MjAyMjA2MTUxMzEzMDBaKT4+DQplbmRvYmoNCjIgMCBvYmoNCjw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAzIDAgUi9MYW5nKGVuLVVTKS9NZXRhZGF0YSA0IDAgUj4+DQplbmRvYmoNCjMgMCBvYmoNCjw8L1R5cGUvUGFnZXMvQ291bnQgMS9LaWRzWzUgMCBSXT4+DQplbmRvYmoNCjQgMCBvYmoNCjw8L1R5cGUvTWV0YWRhdGEvU3VidHlwZS9YTUwvTGVuZ3RoIDIxIDAgUj4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJQREZOZXQiPgo8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgo8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgo8eG1wOkNyZWF0ZURhdGU+MjAxNy0wMS0zMFQxMjoxOTowMFo8L3htcDpDcmVhdGVEYXRlPgo8eG1wOk1vZGlmeURhdGU+MjAyMi0wNi0xNVQxMzoxMzowMFo8L3htcDpNb2RpZnlEYXRlPgo8eG1wOkNyZWF0b3JUb29sPk1pY3Jvc29mdCBPZmZpY2UgV29yZDwveG1wOkNyZWF0b3JUb29sPgo8L3JkZjpEZXNjcmlwdGlvbj4KPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KPGRjOnRpdGxlPgo8cmRmOkFsdD4KPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5TYWxlcyAtIEludm9pY2U8L3JkZjpsaT4KPC9yZGY6QWx0Pgo8L2RjOnRpdGxlPgo8L3JkZjpEZXNjcmlwdGlvbj4KPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIj4KPHBkZjpQcm9kdWNlcj5Bc3Bvc2UuV29yZHMgZm9yIC5ORVQgMjMuOS4wPC9wZGY6UHJvZHVjZXI+CjwvcmRmOkRlc2NyaXB0aW9uPgo8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJ3Ij8+Cg0KZW5kc3RyZWFtDQplbmRvYmoNCjIxIDAgb2JqDQo4NTQNCmVuZG9iag0KMTYgMCBvYmoNCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1RydWVUeXBlL0Jhc2VGb250L0ZBQUFCRytTZWdvZVVJLUJvbGQvRW5jb2RpbmcvV2luQW5zaUVuY29kaW5nL0ZpcnN0Q2hhciAzMi9MYXN0Q2hhciAxNjMvV2lkdGhzIDE3IDAgUi9Gb250RGVzY3JpcHRvciAxOCAwIFI+Pg0KZW5kb2JqDQoxNyAwIG9iag0KWzI3NiAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMjcxIDAgMjcxIDAgNTc1IDU3NSA1NzUgMCA1NzUgNTc1IDAgNTc1IDAgNTc1IDAgMCAwIDAgMCAwIDAgNzAzIDY0MSA2MjQgMCAwIDAgNzExIDAgMCAwIDY0OSA1MTEgOTU3IDAgMCA2MTQgMCAwIDU2MSA1ODYgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgNTM4IDAgMCA2MTkgNTQxIDAgNjE5IDYwMiAyODQgMCA1NTkgMjg0IDkxNiA2MDUgNjExIDYyMCA2MTkgMzk4IDAgMzg5IDYwNSAwIDAgMCA1MzggMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDU3NV0NCmVuZG9iag0KMTggMCBvYmoNCjw8L1R5cGUvRm9udERlc2NyaXB0b3IvRm9udE5hbWUvRkFBQUJHK1NlZ29lVUktQm9sZC9TdGVtViA4MC9EZXNjZW50IC0yNTEvQXNjZW50IDEwNzkvQ2FwSGVpZ2h0IDcwMC9GbGFncyAyNjIxNzYvSXRhbGljQW5nbGUgMC9Gb250QkJveFstNTczIC00MzEgMTk5OSAxMjk4XS9Gb250RmlsZTIgMTUgMCBSPj4NCmVuZG9iag0KMTIgMCBvYmoNCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1RydWVUeXBlL0Jhc2VGb250L0ZBQUFCQytTZWdvZVVJLUxpZ2h0L0VuY29kaW5nL1dpbkFuc2lFbmNvZGluZy9GaXJzdENoYXIgMzIvTGFzdENoYXIgMTE4L1dpZHRocyAxMyAwIFIvRm9udERlc2NyaXB0b3IgMTQgMCBSPj4NCmVuZG9iag0KMTMgMCBvYmoNClsyNzQgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDIyMiAwIDIyMiAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCA2MjkgNTQ0IDYyMSAwIDAgMCAwIDAgMjI4IDAgMCAwIDAgNzA5IDc2MSAwIDAgNTU1IDQ5NyAwIDY0OCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgNDk0IDAgNDQ0IDAgNTA1IDAgNTYwIDUzNSAyMDUgMCAwIDAgODIyIDUzNSA1NjEgMCAwIDMzMCAwIDAgMCA0NTNdDQplbmRvYmoNCjE0IDAgb2JqDQo8PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnROYW1lL0ZBQUFCQytTZWdvZVVJLUxpZ2h0L1N0ZW1WIDgwL0Rlc2NlbnQgLTI1MS9Bc2NlbnQgMTA3OS9DYXBIZWlnaHQgNzAwL0ZsYWdzIDMyL0l0YWxpY0FuZ2xlIDAvRm9udEJCb3hbLTU4NyAtMzk2IDE5OTkgMTI5OV0vRm9udEZpbGUyIDExIDAgUj4+DQplbmRvYmoNCjggMCBvYmoNCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1RydWVUeXBlL0Jhc2VGb250L0ZBQUFBSStTZWdvZVVJL0VuY29kaW5nL1dpbkFuc2lFbmNvZGluZy9GaXJzdENoYXIgMzIvTGFzdENoYXIgMTIxL1dpZHRocyA5IDAgUi9Gb250RGVzY3JpcHRvciAxMCAwIFI+Pg0KZW5kb2JqDQo5IDAgb2JqDQpbMjc0IDAgMCAwIDAgODE4IDAgMCAwIDAgMCA2ODQgMjE3IDQwMCAyMTcgMzkwIDUzOSA1MzkgNTM5IDUzOSA1MzkgNTM5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MODW9nYEFJ/+03vN/EfC/wC1cf8ACIPH+01vN/EfCv7Vx/wiC9HFLTNYMWxuxVMjJqiy2qjoJ5oteR76aBkTnN1AOhLdRqEGNd+9i8S6h9v59vcxrLjbrca2G5UdwtckcdRDV07ZGRP+VjkY5ukjgWlvEd4PFBULn/lQbnWuaon243CsOW0DdXQ0d4jntNby6ahg8MVUL3Ds1L2A9ug7EEart5fvVtaJSx21Elxi1+TqaC62qoY7QAk8ravnHbp7TRr3IOtUvRP1U1kvhQ7LX1jg0u5pzTQN0H5807G6+rVBlPFvLW6qMhkjF0xuzYVBIW/9zervTPAaeOpZbjWvGnoLdfUgnPtB5VeB49U0l33izCpz2ohIe7F7Sx9ttpcO1k1Rzmpmb62GE/1haTjuOWDEbLb8cxay0WPWG1R+DbrPb4GU9NCzUnRkcYDRqSSeHE8TxQc0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIP/2Q0KZW5kc3RyZWFtDQplbmRvYmoNCjIyIDAgb2JqDQoxNTE2OA0KZW5kb2JqDQoxNSAwIG9iag0KPDwvTGVuZ3RoMSAyMyAwIFIvTGVuZ3RoIDI0IDAgUi9GaWx0ZXIvRmxhdGVEZWNvZGU+PnN0cmVhbQ0KeJzsfAl4VFWW8LlvqX1PVVJJBfIqlbAVppJUAokgKcjCEoghCZgCgilIQgKBBBJAVBaHIBpXRFRwaVptRntorWCPHVFbnHZp/XXcp8Vuu22l3RqQ7kb0Q/Jqzr3vVVJhcfz7m+lv5vt4lXffufeee+/Z7jnnPqoAAgBm2AI8VF9eG8hvvDX0Abb8Du/GpSsjnbpb9Q4AMhnvuqXruqWNk9paALjbAQw5LZ3LVs6seX8ZgOkQ1hcta9/Q8tD8f3oMICUdwHWktTnSdB9kcACX4HQwoRUbnBbdp1gvwnpW68ruqx5/0erF+iKs39fesTQC5KHrsf4t1veujFzVyT9t/hAg5x6sS6siK5s/tq57A+v9AGOyOju6umNzYT3AfEqv1LmmuXPFX9J1WD8DIFwCvNDPPQMi6MQ9YhA58ChP/i1o4Rw6kTPqOAE/HH8EcmKH4MhmnEVPSZ1TK0kggTk2IL4jzyVm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 + + + + + + + 103033 + + + + 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MzAxMjE5MDBaKS9Nb2REYXRlKEQ6MjAyMjA2MTUxMzEzMDBaKT4+DQplbmRvYmoNCjIgMCBvYmoNCjw8L1R5cGUvQ2F0YWxvZy9QYWdlcyAzIDAgUi9MYW5nKGVuLVVTKS9NZXRhZGF0YSA0IDAgUj4+DQplbmRvYmoNCjMgMCBvYmoNCjw8L1R5cGUvUGFnZXMvQ291bnQgMS9LaWRzWzUgMCBSXT4+DQplbmRvYmoNCjQgMCBvYmoNCjw8L1R5cGUvTWV0YWRhdGEvU3VidHlwZS9YTUwvTGVuZ3RoIDIxIDAgUj4+c3RyZWFtDQo8P3hwYWNrZXQgYmVnaW49Iu+7vyIgaWQ9Ilc1TTBNcENlaGlIenJlU3pOVGN6a2M5ZCI/Pgo8eDp4bXBtZXRhIHhtbG5zOng9ImFkb2JlOm5zOm1ldGEvIiB4OnhtcHRrPSJQREZOZXQiPgo8cmRmOlJERiB4bWxuczpyZGY9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkvMDIvMjItcmRmLXN5bnRheC1ucyMiPgo8cmRmOkRlc2NyaXB0aW9uIHJkZjphYm91dD0iIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iPgo8eG1wOkNyZWF0ZURhdGU+MjAxNy0wMS0zMFQxMjoxOTowMFo8L3htcDpDcmVhdGVEYXRlPgo8eG1wOk1vZGlmeURhdGU+MjAyMi0wNi0xNVQxMzoxMzowMFo8L3htcDpNb2RpZnlEYXRlPgo8eG1wOkNyZWF0b3JUb29sPk1pY3Jvc29mdCBPZmZpY2UgV29yZDwveG1wOkNyZWF0b3JUb29sPgo8L3JkZjpEZXNjcmlwdGlvbj4KPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6ZGM9Imh0dHA6Ly9wdXJsLm9yZy9kYy9lbGVtZW50cy8xLjEvIj4KPGRjOmZvcm1hdD5hcHBsaWNhdGlvbi9wZGY8L2RjOmZvcm1hdD4KPGRjOnRpdGxlPgo8cmRmOkFsdD4KPHJkZjpsaSB4bWw6bGFuZz0ieC1kZWZhdWx0Ij5TYWxlcyAtIEludm9pY2U8L3JkZjpsaT4KPC9yZGY6QWx0Pgo8L2RjOnRpdGxlPgo8L3JkZjpEZXNjcmlwdGlvbj4KPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6cGRmPSJodHRwOi8vbnMuYWRvYmUuY29tL3BkZi8xLjMvIj4KPHBkZjpQcm9kdWNlcj5Bc3Bvc2UuV29yZHMgZm9yIC5ORVQgMjMuOS4wPC9wZGY6UHJvZHVjZXI+CjwvcmRmOkRlc2NyaXB0aW9uPgo8L3JkZjpSREY+CjwveDp4bXBtZXRhPgo8P3hwYWNrZXQgZW5kPSJ3Ij8+Cg0KZW5kc3RyZWFtDQplbmRvYmoNCjIxIDAgb2JqDQo4NTQNCmVuZG9iag0KMTYgMCBvYmoNCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1RydWVUeXBlL0Jhc2VGb250L0ZBQUFCRytTZWdvZVVJLUJvbGQvRW5jb2RpbmcvV2luQW5zaUVuY29kaW5nL0ZpcnN0Q2hhciAzMi9MYXN0Q2hhciAxNjMvV2lkdGhzIDE3IDAgUi9Gb250RGVzY3JpcHRvciAxOCAwIFI+Pg0KZW5kb2JqDQoxNyAwIG9iag0KWzI3NiAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMjcxIDAgMjcxIDAgNTc1IDU3NSA1NzUgMCA1NzUgNTc1IDAgNTc1IDAgNTc1IDAgMCAwIDAgMCAwIDAgNzAzIDY0MSA2MjQgMCAwIDAgNzExIDAgMCAwIDY0OSA1MTEgOTU3IDAgMCA2MTQgMCAwIDU2MSA1ODYgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgNTM4IDAgMCA2MTkgNTQxIDAgNjE5IDYwMiAyODQgMCA1NTkgMjg0IDkxNiA2MDUgNjExIDYyMCA2MTkgMzk4IDAgMzg5IDYwNSAwIDAgMCA1MzggMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDU3NV0NCmVuZG9iag0KMTggMCBvYmoNCjw8L1R5cGUvRm9udERlc2NyaXB0b3IvRm9udE5hbWUvRkFBQUJHK1NlZ29lVUktQm9sZC9TdGVtViA4MC9EZXNjZW50IC0yNTEvQXNjZW50IDEwNzkvQ2FwSGVpZ2h0IDcwMC9GbGFncyAyNjIxNzYvSXRhbGljQW5nbGUgMC9Gb250QkJveFstNTczIC00MzEgMTk5OSAxMjk4XS9Gb250RmlsZTIgMTUgMCBSPj4NCmVuZG9iag0KMTIgMCBvYmoNCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1RydWVUeXBlL0Jhc2VGb250L0ZBQUFCQytTZWdvZVVJLUxpZ2h0L0VuY29kaW5nL1dpbkFuc2lFbmNvZGluZy9GaXJzdENoYXIgMzIvTGFzdENoYXIgMTE4L1dpZHRocyAxMyAwIFIvRm9udERlc2NyaXB0b3IgMTQgMCBSPj4NCmVuZG9iag0KMTMgMCBvYmoNClsyNzQgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDIyMiAwIDIyMiAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgMCA2MjkgNTQ0IDYyMSAwIDAgMCAwIDAgMjI4IDAgMCAwIDAgNzA5IDc2MSAwIDAgNTU1IDQ5NyAwIDY0OCAwIDAgMCAwIDAgMCAwIDAgMCAwIDAgNDk0IDAgNDQ0IDAgNTA1IDAgNTYwIDUzNSAyMDUgMCAwIDAgODIyIDUzNSA1NjEgMCAwIDMzMCAwIDAgMCA0NTNdDQplbmRvYmoNCjE0IDAgb2JqDQo8PC9UeXBlL0ZvbnREZXNjcmlwdG9yL0ZvbnROYW1lL0ZBQUFCQytTZWdvZVVJLUxpZ2h0L1N0ZW1WIDgwL0Rlc2NlbnQgLTI1MS9Bc2NlbnQgMTA3OS9DYXBIZWlnaHQgNzAwL0ZsYWdzIDMyL0l0YWxpY0FuZ2xlIDAvRm9udEJCb3hbLTU4NyAtMzk2IDE5OTkgMTI5OV0vRm9udEZpbGUyIDExIDAgUj4+DQplbmRvYmoNCjggMCBvYmoNCjw8L1R5cGUvRm9udC9TdWJ0eXBlL1RydWVUeXBlL0Jhc2VGb250L0ZBQUFBSStTZWdvZVVJL0VuY29kaW5nL1dpbkFuc2lFbmNvZGluZy9GaXJzdENoYXIgMzIvTGFzdENoYXIgMTIxL1dpZHRocyA5IDAgUi9Gb250RGVzY3JpcHRvciAxMCAwIFI+Pg0KZW5kb2JqDQo5IDAgb2JqDQpbMjc0IDAgMCAwIDAgODE4IDAgMCAwIDAgMCA2ODQgMjE3IDQwMCAyMTcgMzkwIDUzOSA1MzkgNTM5IDUzOSA1MzkgNTM5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MODW9nYEFJ/+03vN/EfC/wC1cf8ACIPH+01vN/EfCv7Vx/wiC9HFLTNYMWxuxVMjJqiy2qjoJ5oteR76aBkTnN1AOhLdRqEGNd+9i8S6h9v59vcxrLjbrca2G5UdwtckcdRDV07ZGRP+VjkY5ukjgWlvEd4PFBULn/lQbnWuaon243CsOW0DdXQ0d4jntNby6ahg8MVUL3Ds1L2A9ug7EEart5fvVtaJSx21Elxi1+TqaC62qoY7QAk8ravnHbp7TRr3IOtUvRP1U1kvhQ7LX1jg0u5pzTQN0H5807G6+rVBlPFvLW6qMhkjF0xuzYVBIW/9zervTPAaeOpZbjWvGnoLdfUgnPtB5VeB49U0l33izCpz2ohIe7F7Sx9ttpcO1k1Rzmpmb62GE/1haTjuOWDEbLb8cxay0WPWG1R+DbrPb4GU9NCzUnRkcYDRqSSeHE8TxQc0gICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIP/2Q0KZW5kc3RyZWFtDQplbmRvYmoNCjIyIDAgb2JqDQoxNTE2OA0KZW5kb2JqDQoxNSAwIG9iag0KPDwvTGVuZ3RoMSAyMyAwIFIvTGVuZ3RoIDI0IDAgUi9GaWx0ZXIvRmxhdGVEZWNvZGU+PnN0cmVhbQ0KeJzsfAl4VFWW8LlvqX1PVVJJBfIqlbAVppJUAokgKcjCEoghCZgCgilIQgKBBBJAVBaHIBpXRFRwaVptRntorWCPHVFbnHZp/XXcp8Vuu22l3RqQ7kb0Q/Jqzr3vVVJhcfz7m+lv5vt4lXffufeee+/Z7jnnPqoAAgBm2AI8VF9eG8hvvDX0Abb8Du/GpSsjnbpb9Q4AMhnvuqXruqWNk9paALjbAQw5LZ3LVs6seX8ZgOkQ1hcta9/Q8tD8f3oMICUdwHWktTnSdB9kcACX4HQwoRUbnBbdp1gvwnpW68ruqx5/0erF+iKs39fesTQC5KHrsf4t1veujFzVyT9t/hAg5x6sS6siK5s/tq57A+v9AGOyOju6umNzYT3AfEqv1LmmuXPFX9J1WD8DIFwCvNDPPQMi6MQ9YhA58ChP/i1o4Rw6kTPqOAE/HH8EcmKH4MhmnEVPSZ1TK0kggTk2IL4jzyVm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 + + + + + + + + 1234567890128 + + CRONUS International + + + Main Street, 14 + Birmingham + B27 4KT + + GB + + + + GB123456789 + + VAT + + + + CRONUS International + 123456789 + + + Jim Olive + JO@contoso.com + + + + + + 789456278 + + 8712345000004 + + + The Cannon Group PLC + + + 192 Market Square + Birmingham + B27 4KT + + GB + + + + GB789456278 + + VAT + + + + The Cannon Group PLC + 789456278 + + + Mr. Andy Teal + + + + + 2026-01-22 + + 8712345000004 + + 192 Market Square + Birmingham + B27 4KT + + GB + + + + + + 31 + + GB12CPBK08929965044991 + + BG99999 + + + + + 1 Month/2% 8 days + + + 1000 + + 4000 + 1000 + + S + 25 + + VAT + + + + + + 14000 + 14000 + 14140 + 0 + 0.00 + 0 + 14140 + + + 10000 + Item + 1 + 4000 + + Bicycle + + 1000 + + + S + 25 + + VAT + + + + + 4000.00 + 1 + + + + 20000 + Item + 2 + 10000 + + Bicycle v2 + + 2000 + + + S + 25 + + VAT + + + + + 5000.00 + 2 + + + \ No newline at end of file diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-textonly-docref.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-textonly-docref.xml new file mode 100644 index 0000000000..269a1d908c --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-textonly-docref.xml @@ -0,0 +1,185 @@ + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + 103033 + 2026-01-22 + 2026-02-22 + 380 + XYZ + 1 + + 2 + + + 103033 + + + DOC-REF-001 + Text-only reference without attachment + + + DOC-REF-002 + Another text-only reference + + + 103033 + + + dGVzdA== + + + + + 1234567890128 + + CRONUS International + + + Main Street, 14 + Birmingham + B27 4KT + + GB + + + + GB123456789 + + VAT + + + + CRONUS International + 123456789 + + + Jim Olive + JO@contoso.com + + + + + + 789456278 + + 8712345000004 + + + The Cannon Group PLC + + + 192 Market Square + Birmingham + B27 4KT + + GB + + + + GB789456278 + + VAT + + + + The Cannon Group PLC + + + + + + CRONUS International + + + GB123456789 + + + + 30 + 2026-02-22 + + GB12CPBK08929965044991 + + BG99999 + + + + + 1 Month/2% 8 days + + + 1000 + + 4000 + 1000 + + S + 25 + + VAT + + + + + + 14000 + 14000 + 14140 + 0 + 0.00 + 0 + 14140 + + + 10000 + Item + 1 + 4000 + + Bicycle + + 1000 + + + S + 25 + + VAT + + + + + 4000.00 + 1 + + + + 20000 + Item + 2 + 10000 + + Bicycle v2 + + 2000 + + + S + 25 + + VAT + + + + + 5000.00 + 2 + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-s.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-s.xml new file mode 100644 index 0000000000..122be59628 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-s.xml @@ -0,0 +1,297 @@ + + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + Snippet1 + 2017-11-13 + 2017-12-01 + 380 + EUR + 4025:123:4343 + 0150abc + + + 7300010000001 + + 99887766 + + + SupplierTradingName Ltd. + + + Main street 1 + Postbox 123 + London + GB 123 EW + + GB + + + + GB1232434 + + VAT + + + + SupplierOfficialName Ltd + GB983294 + AdditionalLegalInformation + + + John Doe + 9384203984 + john.doe@foo.bar + + + + + + + FR23342 + + FR23342 + + + BuyerTradingName AS + + + Hovedgatan 32 + Po box 878 + Stockholm + 456 34 + + SE + + + + SE4598375937 + + VAT + + + + Buyer Official Name + 39937423947 + + + + + 2017-11-01 + + 7300010000001 + + Delivery street 2 + Building 56 + Stockholm + 21234 + Södermalm + + Gate 15 + + + SE + + + + + + Delivery party Name + + + + + 30 + Snippet1 + + IBAN32423940 + AccountName + + BIC324098 + + + + + Payment within 10 days, 2% discount + + + + true + Cleaning + 200 + + S + 25 + + VAT + + + + + + false + Discount + 100 + + S + 25 + + VAT + + + + + + 1550.00 + + + 5000.0 + 1250 + + S + 25 + + VAT + + + + + + 2000.0 + 300 + + S + 15 + + VAT + + + + + + + 6900 + 7000 + 8550 + 100 + 200 + 8550 + + + + + 1 + Testing note on line level + 10 + 4000.00 + + Konteringsstreng + + 2017-12-01 + 2017-12-05 + + + 123 + + + Description of item + item name + + 97iugug876 + + + 7300010000001 + + + NO + + + 09348023 + + + S + 25.0 + + VAT + + + + + + 400 + + + + + 2 + 10 + 2000.00 + + Konteringsstreng + + Description of item + item name + + 97iugug876 + + + 7300010000001 + + + 86776 + + + + S + 15.0 + + VAT + + + + + 200 + + + + + 3 + 10 + 900.00 + + Konteringsstreng + + Description of item + item name + + 97iugug876 + + + 873649827489 + + + 86776 + + + S + 25.0 + + VAT + + + + AdditionalItemName + AdditionalItemValue + + + + 90 + + + + + + diff --git a/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-z.xml b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-z.xml new file mode 100644 index 0000000000..d9b8a0adc7 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/.resources/peppol/peppol-invoice-vat-category-z.xml @@ -0,0 +1,113 @@ + + + + urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0 + urn:fdc:peppol.eu:2017:poacc:billing:01:1.0 + Vat-Z + 2018-08-30 + 380 + GBP + test reference + + + 7300010000001 + + 7300010000001 + + + Main street 2, Building 4 + Big city + 54321 + + GB + + + + GB928741974 + + VAT + + + + The Sellercompany Incorporated + + + + + + DK12345678 + + Anystreet 8 + Back door + Anytown + 101 + RegionB + + DK + + + + The Buyercompany + + + + + 30 + + SE1212341234123412 + + SEXDABCD + + + + + Payment within 30 days + + + 0.00 + + 1200.00 + 0.00 + + Z + 0 + + VAT + + + + + + 1200.00 + 1200.00 + 1200.00 + 1200.00 + + + 1 + 10 + 1200.00 + + 1 + + + Test item, category Z + + 192387129837129873 + + + Z + 0 + + VAT + + + + + 120.00 + + + + diff --git a/src/Apps/W1/EDocument/Test/app.json b/src/Apps/W1/EDocument/Test/app.json index c0fd2b44ee..dcc263b2e6 100644 --- a/src/Apps/W1/EDocument/Test/app.json +++ b/src/Apps/W1/EDocument/Test/app.json @@ -75,6 +75,10 @@ { "from": 135575, "to": 135575 + }, + { + "from": 135647, + "to": 135649 } ], "resourceExposurePolicy": { diff --git a/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al b/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al index 047fc89157..adf51d6adf 100644 --- a/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/LibraryEDocument.Codeunit.al @@ -289,14 +289,14 @@ codeunit 139629 "Library - E-Document" exit(EDocumentPurchaseLine); end; - procedure CreateInboundPEPPOLDocumentToState(var EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; FileName: Text; EDocImportParams: Record "E-Doc. Import Parameters"): Boolean + procedure CreateInboundPEPPOLDocumentToState(var EDocument: Record "E-Document"; EDocumentService: Record "E-Document Service"; FileName: Text; TempEDocImportParams: Record "E-Doc. Import Parameters"): Boolean var EDocImport: Codeunit "E-Doc. Import"; InStream: InStream; begin NavApp.GetResource(FileName, InStream, TextEncoding::UTF8); EDocImport.CreateFromType(EDocument, EDocumentService, Enum::"E-Doc. File Format"::XML, 'TestFile', InStream); - exit(EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParams)); + exit(EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParams)); end; /// diff --git a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al index 4133a2fb53..83f23def22 100644 --- a/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Matching/EDocPOMatchingUnitTests.Codeunit.al @@ -561,8 +561,12 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected MissingInformationForMatch warning for line with non-existent unit of measure'); end; + // Quantity warning tests use the following notation: + // I = Invoice quantity (from the e-document line) + // R = Remaining to invoice on the PO (Ordered - Previously Invoiced) + // J = Invoiceable quantity (Received - Previously Invoiced) [Test] - procedure CalculatePOMatchWarningsGeneratesQuantityMismatchWarning() + procedure CalculatePOMatchWarningsNoWarningsWhenInvoiceWithinOrderAndReceipts() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; @@ -573,40 +577,85 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin Initialize(); - // [SCENARIO] Calculating PO match warnings generates quantity mismatch warning when quantities don't match - // [GIVEN] An E-Document with lines where calculated quantity differs from original quantity + // [SCENARIO] I < R and I < J — partial invoice within order and receipts generates no warnings + // [GIVEN] PO=100, I=30, Rcv=50, PrevInv=0 → R=100, J=50 LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); + EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + EDocumentPurchaseHeader.Modify(); - // Create a purchase order line with 10 units LibraryEDocument.GetGenericItem(Item); + EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); + EDocumentPurchaseLine."[BC] Purchase Line Type" := Enum::"Purchase Line Type"::Item; + EDocumentPurchaseLine."[BC] Purchase Type No." := Item."No."; + EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; + EDocumentPurchaseLine.Quantity := 30; + EDocumentPurchaseLine.Modify(); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 10); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + PurchaseLine."Qty. Invoiced (Base)" := 0; + PurchaseLine."Qty. Received (Base)" := 50; + PurchaseLine.Modify(); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); - // Create E-Document Purchase Header + // [WHEN] CalculatePOMatchWarnings is called + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + // [THEN] No warnings should be generated + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no warnings for (I < R, I < J)'); + end; + + [Test] + procedure CalculatePOMatchWarningsGeneratesExceedsInvoiceableQtyWarning() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + // [SCENARIO] I < R but I > J — invoice within order but exceeds uninvoiced receipts + // [GIVEN] PO=100, I=50, Rcv=60, PrevInv=20 → R=80, J=40 + LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; EDocumentPurchaseHeader.Modify(); - // Set up E-Document line to create quantity mismatch + LibraryEDocument.GetGenericItem(Item); EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); EDocumentPurchaseLine."[BC] Purchase Line Type" := Enum::"Purchase Line Type"::Item; EDocumentPurchaseLine."[BC] Purchase Type No." := Item."No."; - EDocumentPurchaseLine.Quantity := 100; + EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; + EDocumentPurchaseLine.Quantity := 50; EDocumentPurchaseLine.Modify(); + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + PurchaseLine."Qty. Invoiced (Base)" := 20; + PurchaseLine."Qty. Received (Base)" := 60; + PurchaseLine.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - // [THEN] QuantityMismatch warnings should be generated + // [THEN] ExceedsInvoiceableQty warning should be generated TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::QuantityMismatch); - Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected QuantityMismatch warning to be generated'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning for (I < R, I > J)'); + + // [THEN] ExceedsRemainingToInvoice warning should NOT be generated + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect ExceedsRemainingToInvoice warning'); end; [Test] - procedure CalculatePOMatchWarningsGeneratesNotYetReceivedWarning() + procedure CalculatePOMatchWarningsGeneratesOverReceiptWarning() var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; @@ -617,39 +666,134 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; begin Initialize(); - // [SCENARIO] Calculating PO match warnings generates not yet received warning when trying to invoice more than received - // [GIVEN] An E-Document with lines where E-Doc quantity plus already invoiced quantity exceeds received quantity + // [SCENARIO] I = R and I < J — invoice closes out order but there is an over-receipt + // [GIVEN] PO=100, I=50, Rcv=120, PrevInv=50 → R=50, J=70 LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); + EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + EDocumentPurchaseHeader.Modify(); - // Create E-Document Purchase Header and Line + LibraryEDocument.GetGenericItem(Item); + EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); + EDocumentPurchaseLine."[BC] Purchase Line Type" := Enum::"Purchase Line Type"::Item; + EDocumentPurchaseLine."[BC] Purchase Type No." := Item."No."; + EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; + EDocumentPurchaseLine.Quantity := 50; + EDocumentPurchaseLine.Modify(); + + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + PurchaseLine."Qty. Invoiced (Base)" := 50; + PurchaseLine."Qty. Received (Base)" := 120; + PurchaseLine.Modify(); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + // [WHEN] CalculatePOMatchWarnings is called + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + // [THEN] OverReceipt warning should be generated + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::OverReceipt); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected OverReceipt warning for (I = R, I < J)'); + + // [THEN] No other quantity warnings should be generated + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect ExceedsInvoiceableQty warning'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect ExceedsRemainingToInvoice warning'); + end; + + [Test] + procedure CalculatePOMatchWarningsGeneratesExceedsRemainingToInvoiceWarning() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + // [SCENARIO] I > R but I < J — invoice exceeds order but within receipts (over-receipt charged) + // [GIVEN] PO=100, I=60, Rcv=120, PrevInv=50 → R=50, J=70 + LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; EDocumentPurchaseHeader.Modify(); + + LibraryEDocument.GetGenericItem(Item); EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); + EDocumentPurchaseLine."[BC] Purchase Line Type" := Enum::"Purchase Line Type"::Item; + EDocumentPurchaseLine."[BC] Purchase Type No." := Item."No."; + EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; + EDocumentPurchaseLine.Quantity := 60; + EDocumentPurchaseLine.Modify(); + + LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + PurchaseLine."Qty. Invoiced (Base)" := 50; + PurchaseLine."Qty. Received (Base)" := 120; + PurchaseLine.Modify(); + MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); + + // [WHEN] CalculatePOMatchWarnings is called + EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); + + // [THEN] ExceedsRemainingToInvoice warning should be generated + TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsRemainingToInvoice warning (I > R, I < J)'); + + // [THEN] ExceedsInvoiceableQty warning should NOT be generated + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Did not expect ExceedsInvoiceableQty warning'); + end; + + [Test] + procedure CalculatePOMatchWarningsGeneratesBothWarningsWhenExceedingBothRemainingAndInvoiceable() + var + EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + Item: Record Item; + TempPOMatchWarnings: Record "E-Doc PO Match Warning" temporary; + begin + Initialize(); + // [SCENARIO] I > R and I > J — invoice exceeds both order and receipts + // [GIVEN] PO=100, I=80, Rcv=100, PrevInv=50 → R=50, J=50 + LibraryEDocument.CreateInboundEDocument(EDocument, EDocumentService); + EDocumentPurchaseHeader := LibraryEDocument.MockPurchaseDraftPrepared(EDocument); + EDocumentPurchaseHeader."[BC] Vendor No." := Vendor."No."; + EDocumentPurchaseHeader.Modify(); - // Set up E-Document line LibraryEDocument.GetGenericItem(Item); + EDocumentPurchaseLine := LibraryEDocument.InsertPurchaseDraftLine(EDocument); EDocumentPurchaseLine."[BC] Purchase Line Type" := Enum::"Purchase Line Type"::Item; EDocumentPurchaseLine."[BC] Purchase Type No." := Item."No."; EDocumentPurchaseLine."[BC] Unit of Measure" := Item."Base Unit of Measure"; - EDocumentPurchaseLine.Quantity := 15; // More than what's received (10) + EDocumentPurchaseLine.Quantity := 80; EDocumentPurchaseLine.Modify(); - // Create purchase order line with some invoiced and received quantities LibraryPurchase.CreatePurchHeader(PurchaseHeader, PurchaseHeader."Document Type"::Order, Vendor."No."); - LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 20); - PurchaseLine."Qty. Invoiced (Base)" := 5; // Already invoiced 5 - PurchaseLine."Qty. Received (Base)" := 10; // Only received 10, so trying to invoice 15 + 5 = 20 > 10 received + LibraryPurchase.CreatePurchaseLine(PurchaseLine, PurchaseHeader, PurchaseLine.Type::Item, Item."No.", 100); + PurchaseLine."Qty. Invoiced (Base)" := 50; + PurchaseLine."Qty. Received (Base)" := 100; PurchaseLine.Modify(); MatchEDocumentLineToPOLine(EDocumentPurchaseLine, PurchaseLine); // [WHEN] CalculatePOMatchWarnings is called EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); - // [THEN] NotYetReceived warnings should be generated + // [THEN] Both ExceedsInvoiceableQty and ExceedsRemainingToInvoice warnings should be generated TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected NotYetReceived warning to be generated'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning (I > R, I > J)'); + + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsRemainingToInvoice); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsRemainingToInvoice warning (I > R, I > J)'); end; [Test] @@ -1879,11 +2023,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should be generated + // [THEN] ExceedsInvoiceableQty warning should be generated EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected NotYetReceived warning to be generated'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning to be generated'); end; [Test] @@ -1929,11 +2073,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should NOT be generated + // [THEN] ExceedsInvoiceableQty warning should NOT be generated EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no NotYetReceived warning to be generated'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no ExceedsInvoiceableQty warning to be generated'); end; [Test] @@ -2020,11 +2164,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should NOT be generated for specified vendor + // [THEN] ExceedsInvoiceableQty warning should NOT be generated for specified vendor EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no NotYetReceived warning for specified vendor'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no ExceedsInvoiceableQty warning for specified vendor'); end; [Test] @@ -2072,11 +2216,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should be generated for non-specified vendor (default behavior is "Always ask") + // [THEN] ExceedsInvoiceableQty warning should be generated for non-specified vendor (default behavior is "Always ask") EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected NotYetReceived warning for non-specified vendor'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsFalse(TempPOMatchWarnings.IsEmpty(), 'Expected ExceedsInvoiceableQty warning for non-specified vendor'); end; [Test] @@ -2165,11 +2309,11 @@ codeunit 133508 "E-Doc. PO Matching Unit Tests" // [THEN] Matching should succeed Assert.IsTrue(EDocPOMatching.IsPOLineMatchedToEDocumentLine(PurchaseLine, EDocumentPurchaseLine), 'PO line should be matched to E-Document line'); - // [THEN] NotYetReceived warning should NOT be generated for non-specified vendor (default behavior is "Always receive at posting") + // [THEN] ExceedsInvoiceableQty warning should NOT be generated for non-specified vendor (default behavior is "Always receive at posting") EDocPOMatching.CalculatePOMatchWarnings(EDocumentPurchaseHeader, TempPOMatchWarnings); TempPOMatchWarnings.SetRange("E-Doc. Purchase Line SystemId", EDocumentPurchaseLine.SystemId); - TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::NotYetReceived); - Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no NotYetReceived warning for non-specified vendor'); + TempPOMatchWarnings.SetRange("Warning Type", Enum::"E-Doc PO Match Warning"::ExceedsInvoiceableQty); + Assert.IsTrue(TempPOMatchWarnings.IsEmpty(), 'Expected no ExceedsInvoiceableQty warning for non-specified vendor'); end; [Test] diff --git a/src/Apps/W1/EDocument/Test/src/Processing/CAPIStructuredValidations.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/CAPIStructuredValidations.Codeunit.al deleted file mode 100644 index 55ff412073..0000000000 --- a/src/Apps/W1/EDocument/Test/src/Processing/CAPIStructuredValidations.Codeunit.al +++ /dev/null @@ -1,107 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.eServices.EDocument.Test; - -using Microsoft.eServices.EDocument.Processing.Import.Purchase; - -codeunit 139894 "CAPI Structured Validations" -{ - - var - Assert: Codeunit Assert; - - internal procedure AssertFullEDocumentContentExtracted(EDocumentEntryNo: Integer) - var - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - begin - EDocumentPurchaseHeader.Get(EDocumentEntryNo); - - Assert.AreEqual('MICROSOFT CORPORATION', EDocumentPurchaseHeader."Customer Company Name", 'The customer company name does not allign with the mock data.'); - Assert.AreEqual('CID-12345', EDocumentPurchaseHeader."Customer Company Id", 'The customer company id does not allign with the mock data.'); - Assert.AreEqual('PO-3333', EDocumentPurchaseHeader."Purchase Order No.", 'The purchase order number does not allign with the mock data.'); - Assert.AreEqual('INV-100', EDocumentPurchaseHeader."Sales Invoice No.", 'The sales invoice number does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(15, 12, 2019), EDocumentPurchaseHeader."Due Date", 'The due date does not allign with the mock data.'); - Assert.AreEqual('CONTOSO LTD.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not allign with the mock data.'); - Assert.AreEqual('123 456th St New York, NY, 10001', EDocumentPurchaseHeader."Vendor Address", 'The vendor address does not allign with the mock data.'); - Assert.AreEqual('Contoso Headquarters', EDocumentPurchaseHeader."Vendor Address Recipient", 'The vendor address recipient does not allign with the mock data.'); - Assert.AreEqual('123 Other St, Redmond WA, 98052', EDocumentPurchaseHeader."Customer Address", 'The customer address does not allign with the mock data.'); - Assert.AreEqual('Microsoft Corp', EDocumentPurchaseHeader."Customer Address Recipient", 'The customer address recipient does not allign with the mock data.'); - Assert.AreEqual('123 Bill St, Redmond WA, 98052', EDocumentPurchaseHeader."Billing Address", 'The billing address does not allign with the mock data.'); - Assert.AreEqual('Microsoft Finance', EDocumentPurchaseHeader."Billing Address Recipient", 'The billing address recipient does not allign with the mock data.'); - Assert.AreEqual('123 Ship St, Redmond WA, 98052', EDocumentPurchaseHeader."Shipping Address", 'The shipping address does not allign with the mock data.'); - Assert.AreEqual('Microsoft Delivery', EDocumentPurchaseHeader."Shipping Address Recipient", 'The shipping address recipient does not allign with the mock data.'); - Assert.AreEqual(100, EDocumentPurchaseHeader."Sub Total", 'The sub total does not allign with the mock data.'); - Assert.AreEqual(10, EDocumentPurchaseHeader."Total VAT", 'The total tax does not allign with the mock data.'); - Assert.AreEqual(110, EDocumentPurchaseHeader.Total, 'The total does not allign with the mock data.'); - Assert.AreEqual(610, EDocumentPurchaseHeader."Amount Due", 'The amount due does not allign with the mock data.'); - Assert.AreEqual(500, EDocumentPurchaseHeader."Previous Unpaid Balance", 'The previous unpaid balance does not allign with the mock data.'); - Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not allign with the mock data.'); - Assert.AreEqual('123 Remit St New York, NY, 10001', EDocumentPurchaseHeader."Remittance Address", 'The remittance address does not allign with the mock data.'); - Assert.AreEqual('Contoso Billing', EDocumentPurchaseHeader."Remittance Address Recipient", 'The remittance address recipient does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(14, 10, 2019), EDocumentPurchaseHeader."Service Start Date", 'The service start date does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(14, 11, 2019), EDocumentPurchaseHeader."Service End Date", 'The service end date does not allign with the mock data.'); - - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); - EDocumentPurchaseLine.FindSet(); - Assert.AreEqual(60, EDocumentPurchaseLine."Sub Total", 'The amount in the purchase line does not allign with the mock data.'); - Assert.AreEqual('Consulting Services', EDocumentPurchaseLine.Description, 'The description in the purchase line does not allign with the mock data.'); - Assert.AreEqual(30, EDocumentPurchaseLine."Unit Price", 'The unit price in the purchase line does not allign with the mock data.'); - Assert.AreEqual(2, EDocumentPurchaseLine.Quantity, 'The quantity in the purchase line does not allign with the mock data.'); - Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the purchase line does not allign with the mock data.'); - Assert.AreEqual('A123', EDocumentPurchaseLine."Product Code", 'The product code in the purchase line does not allign with the mock data.'); - Assert.AreEqual('hours', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the purchase line does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(4, 3, 2021), EDocumentPurchaseLine.Date, 'The date in the purchase line does not allign with the mock data.'); - Assert.AreEqual(6, EDocumentPurchaseLine."VAT Rate", 'The amount in the purchase line does not allign with the mock data.'); - - EDocumentPurchaseLine.Next(); - Assert.AreEqual(30, EDocumentPurchaseLine."Sub Total", 'The amount in the purchase line does not allign with the mock data.'); - Assert.AreEqual('Document Fee', EDocumentPurchaseLine.Description, 'The description in the purchase line does not allign with the mock data.'); - Assert.AreEqual(10, EDocumentPurchaseLine."Unit Price", 'The unit price in the purchase line does not allign with the mock data.'); - Assert.AreEqual(3, EDocumentPurchaseLine.Quantity, 'The quantity in the purchase line does not allign with the mock data.'); - Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the purchase line does not allign with the mock data.'); - Assert.AreEqual('B456', EDocumentPurchaseLine."Product Code", 'The product code in the purchase line does not allign with the mock data.'); - Assert.AreEqual('', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the purchase line does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(5, 3, 2021), EDocumentPurchaseLine.Date, 'The date in the purchase line does not allign with the mock data.'); - Assert.AreEqual(3, EDocumentPurchaseLine."VAT Rate", 'The amount in the purchase line does not allign with the mock data.'); - - EDocumentPurchaseLine.Next(); - Assert.AreEqual(10, EDocumentPurchaseLine."Sub Total", 'The amount does not allign with the mock data.'); - Assert.AreEqual('Printing Fee', EDocumentPurchaseLine.Description, 'The description does not allign with the mock data.'); - Assert.AreEqual(1, EDocumentPurchaseLine."Unit Price", 'The unit price does not allign with the mock data.'); - Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'The quantity does not allign with the mock data.'); - Assert.AreEqual('C789', EDocumentPurchaseLine."Product Code", 'The product code does not allign with the mock data.'); - Assert.AreEqual('pages', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(6, 3, 2021), EDocumentPurchaseLine.Date, 'The date does not allign with the mock data.'); - Assert.AreEqual(1, EDocumentPurchaseLine."VAT Rate", 'The amount does not allign with the mock data.'); - end; - - internal procedure AssertMinimalEDocumentContentParsed(EDocumentEntryNo: Integer) - var - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - begin - EDocumentPurchaseHeader.Get(EDocumentEntryNo); - - Assert.AreEqual('INV-100', EDocumentPurchaseHeader."Sales Invoice No.", 'The sales invoice number does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(15, 12, 2019), EDocumentPurchaseHeader."Due Date", 'The due date does not allign with the mock data.'); - Assert.AreEqual('CONTOSO LTD.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not allign with the mock data.'); - Assert.AreEqual(110, EDocumentPurchaseHeader.Total, 'The total does not allign with the mock data.'); - Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not allign with the mock data.'); - - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); - EDocumentPurchaseLine.FindSet(); - Assert.AreEqual(60, EDocumentPurchaseLine."Sub Total", 'The amount in the purchase line does not allign with the mock data.'); - Assert.AreEqual('Consulting Services', EDocumentPurchaseLine.Description, 'The description in the purchase line does not allign with the mock data.'); - - EDocumentPurchaseLine.Next(); - Assert.AreEqual(30, EDocumentPurchaseLine."Sub Total", 'The amount in the purchase line does not allign with the mock data.'); - Assert.AreEqual('Document Fee', EDocumentPurchaseLine.Description, 'The description in the purchase line does not allign with the mock data.'); - - EDocumentPurchaseLine.Next(); - Assert.AreEqual(10, EDocumentPurchaseLine."Sub Total", 'The amount does not allign with the mock data.'); - Assert.AreEqual('Printing Fee', EDocumentPurchaseLine.Description, 'The description does not allign with the mock data.'); - end; -} \ No newline at end of file diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocAttachmentTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocAttachmentTest.Codeunit.al new file mode 100644 index 0000000000..42a7e9e305 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocAttachmentTest.Codeunit.al @@ -0,0 +1,89 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Test; + +using Microsoft.eServices.EDocument; +using Microsoft.Foundation.Attachment; +using System.Utilities; + +codeunit 139896 "E-Doc. Attachment Test" +{ + Subtype = Test; + TestType = IntegrationTest; + Permissions = tabledata "E-Document" = rimd, + tabledata "Document Attachment" = rimd; + + var + Assert: Codeunit Assert; + + [Test] + procedure UploadAttachmentToEDocSetsEDocFields() + var + EDocument: Record "E-Document"; + DocumentAttachment: Record "Document Attachment"; + TempBlob: Codeunit "Temp Blob"; + RecRef: RecordRef; + OutStream: OutStream; + InStream: InStream; + begin + // [FEATURE] [E-Document] [Attachment] + // [SCENARIO] Bug 619590: When uploading an attachment to an E-Document via the factbox, + // the Document Attachment record must have "E-Document Attachment" = true and + // "E-Document Entry No." set. Without OnBeforeInsertAttachment subscriber, these + // fields are not populated and the attachment won't appear in the factbox. + + // [GIVEN] An E-Document record exists + EDocument.Init(); + EDocument."Document Type" := "E-Document Type"::"Purchase Invoice"; + EDocument.Direction := "E-Document Direction"::Incoming; + EDocument.Insert(true); + + // [GIVEN] A RecRef pointing to the E-Document + // (In the real flow, OnAfterGetRecRefFail constructs this from the factbox SubPageLink filters) + RecRef.GetTable(EDocument); + + // [WHEN] We save an attachment via SaveAttachmentFromStream (same path as factbox upload) + TempBlob.CreateOutStream(OutStream); + OutStream.WriteText('Test attachment content for bug 619590'); + TempBlob.CreateInStream(InStream); + DocumentAttachment.Init(); + DocumentAttachment.SaveAttachmentFromStream(InStream, RecRef, 'test-edoc-attachment.txt'); + + // [THEN] The Document Attachment is created with Table ID = E-Document + DocumentAttachment.Reset(); + DocumentAttachment.SetRange("Table ID", Database::"E-Document"); + DocumentAttachment.SetRange("No.", Format(EDocument."Entry No")); + DocumentAttachment.FindFirst(); + + // [THEN] E-Document fields are set by the OnBeforeInsertAttachment subscriber + Assert.IsTrue(DocumentAttachment."E-Document Attachment", + 'E-Document Attachment should be true — OnBeforeInsertAttachment subscriber must set this field'); + Assert.AreEqual(EDocument."Entry No", DocumentAttachment."E-Document Entry No.", + 'E-Document Entry No. should match the E-Document — OnBeforeInsertAttachment subscriber must set this field'); + end; + + [Test] + procedure GetRefTableReturnsFalseForTableIdZero() + var + DocumentAttachment: Record "Document Attachment"; + DocumentAttachmentMgmt: Codeunit "Document Attachment Mgmt"; + RecRef: RecordRef; + begin + // [FEATURE] [E-Document] [Attachment] + // [SCENARIO] Bug 619590: Verify precondition — GetRefTable returns false when Table ID = 0 + // This is the state of Rec in the factbox when no attachments exist (SubPageLink + // does not set Table ID). + + // [GIVEN] A Document Attachment with Table ID = 0 + DocumentAttachment.Init(); + DocumentAttachment."Table ID" := 0; + + // [WHEN] GetRefTable is called + // [THEN] It returns false because Table ID = 0 is not handled + Assert.IsFalse( + DocumentAttachmentMgmt.GetRefTable(RecRef, DocumentAttachment), + 'GetRefTable should return false for Table ID = 0'); + end; +} diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocE2ETest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocE2ETest.Codeunit.al index 70f194ea57..e98eebd299 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocE2ETest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocE2ETest.Codeunit.al @@ -1725,7 +1725,7 @@ codeunit 139624 "E-Doc E2E Test" var EDocument: Record "E-Document"; PurchaseHeader: Record "Purchase Header"; - EDocImportParams: Record "E-Doc. Import Parameters"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; begin // [FEATURE] [E-Document] [Processing] // [SCENARIO] @@ -1735,8 +1735,8 @@ codeunit 139624 "E-Doc E2E Test" EDocumentService.Modify(); // [GIVEN] An inbound e-document is received and fully processed - EDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; - Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', EDocImportParams), 'The e-document should be processed'); + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', TempEDocImportParams), 'The e-document should be processed'); EDocument.SetRecFilter(); EDocument.FindLast(); Assert.AreEqual(Enum::"E-Document Status"::Processed, EDocument.Status, 'E-Document should be in Processed status.'); @@ -1750,6 +1750,59 @@ codeunit 139624 "E-Doc E2E Test" Assert.AreEqual(Enum::"E-Document Status"::"In Progress", EDocument.Status, 'E-Document should be in In Progress status.'); end; + [Test] + procedure ImportPEPPOLInvoiceWithTextOnlyDocumentReferences() + var + EDocument: Record "E-Document"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; + begin + // [SCENARIO] Import a PEPPOL invoice with AdditionalDocumentReference elements + // that have no child (text-only references). + // Previously this caused "Please choose a file to attach" error. + Initialize(Enum::"Service Integration"::"Mock"); + + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + WorkDate(DMY2Date(1, 1, 2027)); + Assert.IsTrue( + LibraryEDoc.CreateInboundPEPPOLDocumentToState( + EDocument, EDocumentService, 'peppol/peppol-invoice-textonly-docref.xml', TempEDocImportParams), + 'The e-document should be processed'); + + EDocument.Get(EDocument."Entry No"); + PurchaseHeader.Get(EDocument."Document Record ID"); + PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type"); + PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); + Assert.AreEqual(2, PurchaseLine.Count(), 'Expected 2 purchase lines to be imported.'); + end; + + [Test] + procedure ImportPEPPOLInvoiceWithHierarchicalLineIds() + var + EDocument: Record "E-Document"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; + begin + // [SCENARIO] Import a PEPPOL invoice with non-integer line IDs (e.g., "1.1", "1.2"). + // Previously this caused "The value '1.1' can't be evaluated into type Integer" error. + Initialize(Enum::"Service Integration"::"Mock"); + + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + WorkDate(DMY2Date(1, 1, 2027)); + Assert.IsTrue( + LibraryEDoc.CreateInboundPEPPOLDocumentToState( + EDocument, EDocumentService, 'peppol/peppol-invoice-hierarchical-lineids.xml', TempEDocImportParams), + 'The e-document should be processed'); + + EDocument.Get(EDocument."Entry No"); + PurchaseHeader.Get(EDocument."Document Record ID"); + PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type"); + PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); + Assert.AreEqual(2, PurchaseLine.Count(), 'Expected 2 purchase lines to be imported.'); + end; + local procedure CheckPDFEmbedToXML(TempBlob: Codeunit "Temp Blob") var TempXMLBuffer: Record "XML Buffer" temporary; @@ -1864,7 +1917,7 @@ codeunit 139624 "E-Doc E2E Test" internal procedure PurchaseDocumentsCreatedFromEDocumentsUseDocumentTotalsValidation() var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; PurchaseHeader: Record "Purchase Header"; PurchasesPayablesSetup: Record "Purchases & Payables Setup"; EDocImport: Codeunit "E-Doc. Import"; @@ -1880,8 +1933,8 @@ codeunit 139624 "E-Doc E2E Test" LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); LibraryEDoc.MockPurchaseDraftPrepared(EDocument); // [WHEN] Processing into a purchase invoice - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); // [THEN] The purchase invoice should have the totals from the E-Document PurchaseHeader.SetRange("E-Document Link", EDocument.SystemId); PurchaseHeader.FindFirst(); @@ -1895,7 +1948,7 @@ codeunit 139624 "E-Doc E2E Test" internal procedure PurchaseDocumentsCreatedFromStructuredEDocumentCantEditTotals() var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; PurchaseHeader: Record "Purchase Header"; EDocImport: Codeunit "E-Doc. Import"; PurchaseInvoice: TestPage "Purchase Invoice"; @@ -1907,8 +1960,8 @@ codeunit 139624 "E-Doc E2E Test" LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); LibraryEDoc.MockPurchaseDraftPrepared(EDocument); // [WHEN] Processing into a purchase invoice - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); PurchaseHeader.SetRange("E-Document Link", EDocument.SystemId); PurchaseHeader.FindFirst(); // [THEN] The purchase invoice page should not allow editing of the totals @@ -2890,7 +2943,7 @@ codeunit 139624 "E-Doc E2E Test" var EDocument: Record "E-Document"; PurchaseHeader: Record "Purchase Header"; - EDocImportParams: Record "E-Doc. Import Parameters"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; begin // [FEATURE] [E-Document] [Processing] // [SCENARIO] @@ -2900,8 +2953,8 @@ codeunit 139624 "E-Doc E2E Test" EDocumentService.Modify(); // [GIVEN] An inbound e-document is received and fully processed - EDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; - Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', EDocImportParams), 'The e-document should be processed'); + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', TempEDocImportParams), 'The e-document should be processed'); EDocument.SetRecFilter(); EDocument.FindLast(); Assert.AreEqual(Enum::"E-Document Status"::Processed, EDocument.Status, 'E-Document should be in Processed status.'); diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocEmailTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocEmailTests.Codeunit.al index 1d3ba45a3d..545e6c4d7f 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocEmailTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocEmailTests.Codeunit.al @@ -26,7 +26,7 @@ codeunit 139746 "E-Doc. Email Tests" Access = Internal; var - Account: Record "Email Account"; + TempAccount: Record "Email Account"; Customer: Record Customer; Vendor: Record Vendor; EDocument: Record "E-Document"; @@ -76,8 +76,8 @@ codeunit 139746 "E-Doc. Email Tests" DocumentSendingProfile.Modify(); EmailConnectorMock.Initialize(); - EmailConnectorMock.AddAccount(Account); - EmailScenario.SetDefaultEmailAccount(Account); + EmailConnectorMock.AddAccount(TempAccount); + EmailScenario.SetDefaultEmailAccount(TempAccount); Customer."E-Mail" := 'Test123@example.com'; Customer.Modify(); @@ -150,8 +150,8 @@ codeunit 139746 "E-Doc. Email Tests" DocumentSendingProfile.Modify(); EmailConnectorMock.Initialize(); - EmailConnectorMock.AddAccount(Account); - EmailScenario.SetDefaultEmailAccount(Account); + EmailConnectorMock.AddAccount(TempAccount); + EmailScenario.SetDefaultEmailAccount(TempAccount); Customer."E-Mail" := 'Test123@example.com'; Customer.Modify(); diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocInvtPickTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocInvtPickTest.Codeunit.al new file mode 100644 index 0000000000..0ce1899699 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocInvtPickTest.Codeunit.al @@ -0,0 +1,321 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Test; + +using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Integration; +using Microsoft.Finance.GeneralLedger.Setup; +using Microsoft.Inventory.Item; +using Microsoft.Inventory.Journal; +using Microsoft.Inventory.Ledger; +using Microsoft.Inventory.Location; +using Microsoft.Inventory.Tracking; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Document; +using Microsoft.Sales.History; +using Microsoft.Warehouse.Activity; +using Microsoft.Warehouse.Request; +using Microsoft.Warehouse.Setup; +using Microsoft.Warehouse.Structure; +using System.TestLibraries.Utilities; + +codeunit 139863 "E-Doc. Invt. Pick Test" +{ + Subtype = Test; + TestType = IntegrationTest; + + var + Assert: Codeunit Assert; + LibraryEDoc: Codeunit "Library - E-Document"; + LibraryWarehouse: Codeunit "Library - Warehouse"; + LibrarySales: Codeunit "Library - Sales"; + LibraryInventory: Codeunit "Library - Inventory"; + LibraryItemTracking: Codeunit "Library - Item Tracking"; + LibraryRandom: Codeunit "Library - Random"; + LibraryJobQueue: Codeunit "Library - Job Queue"; + LibraryUtility: Codeunit "Library - Utility"; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; + LibraryLowerPermission: Codeunit "Library - Lower Permissions"; + + [Test] + [HandlerFunctions('ItemTrackingLinesPageHandler')] + procedure InvtPickWithWrongLotOnBinDoesNotCreateShipmentOrILE() + var + Customer: Record Customer; + EDocumentService: Record "E-Document Service"; + Location: Record Location; + Bin: Record Bin; + WrongBin: Record Bin; + Item: Record Item; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + SalesShipmentHeader: Record "Sales Shipment Header"; + ItemLedgerEntry: Record "Item Ledger Entry"; + EDocument: Record "E-Document"; + WarehouseEmployee: Record "Warehouse Employee"; + SalesOrderNo: Code[20]; + LotNo: Code[50]; + Qty: Decimal; + begin + // [FEATURE] [E-Document] [Inventory Pick] [Warehouse] + // [SCENARIO 625438] Posting Inventory Pick with wrong Lot No. on Bin does not create + // Posted Sales Shipment, ILE, or E-Document when E-Document Service Flow is active. + // Previously, E-Document subscriber caused a premature Commit() that persisted the + // Sales Shipment and ILE even when the warehouse bin check failed. + + // [GIVEN] E-Document service configured for Sales Shipment + Initialize(Customer, EDocumentService); + Qty := LibraryRandom.RandIntInRange(5, 10); + + // [GIVEN] Location with Bin Mandatory and Require Pick + CreateLocationWithBinMandatoryAndRequirePick(Location, Bin, WrongBin, WarehouseEmployee); + + // [GIVEN] Lot-tracked item with stock on BIN-A + CreateLotTrackedItem(Item); + LotNo := LibraryUtility.GenerateGUID(); + CreateAndPostInvtAdjustmentWithLotTracking(Item."No.", Location.Code, Bin.Code, Qty, LotNo); + + // [GIVEN] Sales Order with the item on this location + CreateSalesOrderWithItemOnLocation(SalesHeader, SalesLine, Customer, Item, Location, Qty); + SalesOrderNo := SalesHeader."No."; + + // [GIVEN] Inventory Pick created for the Sales Order + LibrarySales.ReleaseSalesDocument(SalesHeader); + CreateInventoryPickFromSalesOrder(SalesHeader); + + // [GIVEN] Pick line set to take Lot from BIN-B where the Lot does NOT exist + // Assign Bin Code directly to bypass OnValidate bin content check — simulates + // the invalid state that the posting engine must catch and roll back. + FindInventoryPickLine(WarehouseActivityHeader, WarehouseActivityLine, SalesOrderNo); + WarehouseActivityLine."Lot No." := LotNo; + WarehouseActivityLine."Bin Code" := WrongBin.Code; + WarehouseActivityLine.Modify(true); + LibraryWarehouse.SetQtyToHandleWhseActivity(WarehouseActivityHeader, WarehouseActivityLine.Quantity); + + // [WHEN] Post the Inventory Pick — should fail because lot is not on BIN-B + Commit(); + asserterror LibraryWarehouse.PostInventoryActivity(WarehouseActivityHeader, false); + + // [THEN] No Posted Sales Shipment was created + SalesShipmentHeader.SetRange("Order No.", SalesOrderNo); + Assert.RecordIsEmpty(SalesShipmentHeader); + + // [THEN] No Sale Item Ledger Entry was created + ItemLedgerEntry.SetRange("Item No.", Item."No."); + ItemLedgerEntry.SetRange("Location Code", Location.Code); + ItemLedgerEntry.SetRange("Entry Type", ItemLedgerEntry."Entry Type"::Sale); + Assert.RecordIsEmpty(ItemLedgerEntry); + + // [THEN] No E-Document was created + EDocument.SetRange("Document Type", Enum::"E-Document Type"::"Sales Shipment"); + EDocument.SetRange("Bill-to/Pay-to No.", Customer."No."); + Assert.RecordIsEmpty(EDocument); + end; + + [Test] + [HandlerFunctions('ItemTrackingLinesPageHandler')] + procedure InvtPickWithCorrectLotOnBinCreatesShipmentAndEDocument() + var + Customer: Record Customer; + EDocumentService: Record "E-Document Service"; + Location: Record Location; + Bin: Record Bin; + WrongBin: Record Bin; + Item: Record Item; + SalesHeader: Record "Sales Header"; + SalesLine: Record "Sales Line"; + WarehouseActivityHeader: Record "Warehouse Activity Header"; + WarehouseActivityLine: Record "Warehouse Activity Line"; + SalesShipmentHeader: Record "Sales Shipment Header"; + ItemLedgerEntry: Record "Item Ledger Entry"; + EDocument: Record "E-Document"; + WarehouseEmployee: Record "Warehouse Employee"; + SalesOrderNo: Code[20]; + LotNo: Code[50]; + Qty: Decimal; + begin + // [FEATURE] [E-Document] [Inventory Pick] [Warehouse] + // [SCENARIO 625438] Posting Inventory Pick with correct Lot No. on Bin creates + // Posted Sales Shipment, ILE and E-Document consistently. + // The E-Document is created via deferred OnAfterPostWhseActivityCompleted event. + + // [GIVEN] E-Document service configured for Sales Shipment + Initialize(Customer, EDocumentService); + Qty := LibraryRandom.RandIntInRange(5, 10); + + // [GIVEN] Location with Bin Mandatory and Require Pick + CreateLocationWithBinMandatoryAndRequirePick(Location, Bin, WrongBin, WarehouseEmployee); + + // [GIVEN] Lot-tracked item with stock on BIN-A + CreateLotTrackedItem(Item); + LotNo := LibraryUtility.GenerateGUID(); + CreateAndPostInvtAdjustmentWithLotTracking(Item."No.", Location.Code, Bin.Code, Qty, LotNo); + + // [GIVEN] Sales Order with the item on this location + CreateSalesOrderWithItemOnLocation(SalesHeader, SalesLine, Customer, Item, Location, Qty); + SalesOrderNo := SalesHeader."No."; + + // [GIVEN] Inventory Pick created for the Sales Order + LibrarySales.ReleaseSalesDocument(SalesHeader); + CreateInventoryPickFromSalesOrder(SalesHeader); + + // [GIVEN] Pick line with correct Lot and correct Bin + FindInventoryPickLine(WarehouseActivityHeader, WarehouseActivityLine, SalesOrderNo); + WarehouseActivityLine.Validate("Lot No.", LotNo); + WarehouseActivityLine.Validate("Bin Code", Bin.Code); + WarehouseActivityLine.Modify(true); + LibraryWarehouse.SetQtyToHandleWhseActivity(WarehouseActivityHeader, WarehouseActivityLine.Quantity); + + // [WHEN] Post the Inventory Pick + LibraryWarehouse.PostInventoryActivity(WarehouseActivityHeader, false); + + // [THEN] Posted Sales Shipment exists + SalesShipmentHeader.SetRange("Order No.", SalesOrderNo); + Assert.RecordIsNotEmpty(SalesShipmentHeader); + + // [THEN] Item Ledger Entry exists for the sale + ItemLedgerEntry.SetRange("Item No.", Item."No."); + ItemLedgerEntry.SetRange("Location Code", Location.Code); + ItemLedgerEntry.SetRange("Entry Type", ItemLedgerEntry."Entry Type"::Sale); + Assert.RecordIsNotEmpty(ItemLedgerEntry); + + // [THEN] E-Document was created for the shipment + SalesShipmentHeader.FindFirst(); + EDocument.SetRange("Document Type", Enum::"E-Document Type"::"Sales Shipment"); + EDocument.SetRange("Document No.", SalesShipmentHeader."No."); + Assert.RecordIsNotEmpty(EDocument); + end; + + local procedure Initialize(var Customer: Record Customer; var EDocumentService: Record "E-Document Service") + var + EDocument: Record "E-Document"; + EDocumentServiceStatus: Record "E-Document Service Status"; + GLSetup: Record "General Ledger Setup"; + begin + LibraryLowerPermission.SetOutsideO365Scope(); + LibraryVariableStorage.Clear(); + + EDocument.DeleteAll(); + EDocumentServiceStatus.DeleteAll(); + EDocumentService.DeleteAll(); + + GLSetup.GetRecordOnce(); + GLSetup."VAT Reporting Date Usage" := GLSetup."VAT Reporting Date Usage"::Disabled; + GLSetup.Modify(); + + LibraryEDoc.SetupStandardVAT(); + LibraryEDoc.SetupStandardSalesScenario(Customer, EDocumentService, Enum::"E-Document Format"::Mock, Enum::"Service Integration"::"Mock"); + LibraryEDoc.AddEDocServiceSupportedType(EDocumentService, Enum::"E-Document Type"::"Sales Shipment"); + + LibraryJobQueue.SetDoNotHandleCodeunitJobQueueEnqueueEvent(true); + end; + + local procedure CreateLocationWithBinMandatoryAndRequirePick( + var Location: Record Location; + var Bin: Record Bin; + var WrongBin: Record Bin; + var WarehouseEmployee: Record "Warehouse Employee") + begin + LibraryWarehouse.CreateLocationWMS(Location, true, false, true, false, false); + LibraryWarehouse.CreateBin(Bin, Location.Code, 'BIN-A', '', ''); + LibraryWarehouse.CreateBin(WrongBin, Location.Code, 'BIN-B', '', ''); + LibraryWarehouse.CreateWarehouseEmployee(WarehouseEmployee, Location.Code, false); + end; + + local procedure CreateLotTrackedItem(var Item: Record Item) + var + ItemTrackingCode: Record "Item Tracking Code"; + StandardItem: Record Item; + begin + LibraryItemTracking.CreateItemTrackingCode(ItemTrackingCode, false, true); + ItemTrackingCode.Validate("Lot Warehouse Tracking", true); + ItemTrackingCode.Modify(true); + + // Use the standard E-Doc item's VAT Prod. Posting Group to avoid blocked VAT setup + LibraryEDoc.GetGenericItem(StandardItem); + + LibraryInventory.CreateItem(Item); + Item.Validate("Item Tracking Code", ItemTrackingCode.Code); + Item.Validate("VAT Prod. Posting Group", StandardItem."VAT Prod. Posting Group"); + Item.Validate("Unit Price", LibraryRandom.RandDec(100, 2)); + Item.Modify(true); + end; + + local procedure CreateAndPostInvtAdjustmentWithLotTracking( + ItemNo: Code[20]; + LocationCode: Code[10]; + BinCode: Code[20]; + Qty: Decimal; + LotNo: Code[50]) + var + ItemJournalLine: Record "Item Journal Line"; + begin + LibraryInventory.CreateItemJournalLineInItemTemplate(ItemJournalLine, ItemNo, LocationCode, BinCode, Qty); + + LibraryVariableStorage.Enqueue(LotNo); + LibraryVariableStorage.Enqueue(Qty); + ItemJournalLine.OpenItemTrackingLines(false); + + LibraryInventory.PostItemJournalLine(ItemJournalLine."Journal Template Name", ItemJournalLine."Journal Batch Name"); + end; + + local procedure CreateSalesOrderWithItemOnLocation( + var SalesHeader: Record "Sales Header"; + var SalesLine: Record "Sales Line"; + Customer: Record Customer; + Item: Record Item; + Location: Record Location; + Qty: Decimal) + begin + LibrarySales.CreateSalesHeader(SalesHeader, SalesHeader."Document Type"::Order, Customer."No."); + SalesHeader.Validate("Location Code", Location.Code); + SalesHeader.Modify(true); + + LibrarySales.CreateSalesLine(SalesLine, SalesHeader, SalesLine.Type::Item, Item."No.", Qty); + SalesLine.Validate("Location Code", Location.Code); + SalesLine.Modify(true); + end; + + local procedure CreateInventoryPickFromSalesOrder(var SalesHeader: Record "Sales Header") + var + WhseRequest: Record "Warehouse Request"; + CreateInvtPutPick: Report "Create Invt Put-away/Pick/Mvmt"; + begin + WhseRequest.SetCurrentKey("Source Document", "Source No."); + WhseRequest.SetRange("Source Document", WhseRequest."Source Document"::"Sales Order"); + WhseRequest.SetRange("Source No.", SalesHeader."No."); + CreateInvtPutPick.SetTableView(WhseRequest); + CreateInvtPutPick.InitializeRequest(false, true, false, false, false); + CreateInvtPutPick.UseRequestPage(false); + CreateInvtPutPick.SuppressMessages(true); + CreateInvtPutPick.RunModal(); + end; + + local procedure FindInventoryPickLine( + var WarehouseActivityHeader: Record "Warehouse Activity Header"; + var WarehouseActivityLine: Record "Warehouse Activity Line"; + SalesOrderNo: Code[20]) + begin + WarehouseActivityHeader.SetRange(Type, WarehouseActivityHeader.Type::"Invt. Pick"); + WarehouseActivityHeader.SetRange("Source No.", SalesOrderNo); + WarehouseActivityHeader.FindFirst(); + + WarehouseActivityLine.SetRange("Activity Type", WarehouseActivityLine."Activity Type"::"Invt. Pick"); + WarehouseActivityLine.SetRange("No.", WarehouseActivityHeader."No."); + WarehouseActivityLine.FindFirst(); + end; + + [ModalPageHandler] + procedure ItemTrackingLinesPageHandler(var ItemTrackingLines: TestPage "Item Tracking Lines") + begin + ItemTrackingLines.New(); + ItemTrackingLines."Lot No.".SetValue(LibraryVariableStorage.DequeueText()); + ItemTrackingLines."Quantity (Base)".SetValue(LibraryVariableStorage.DequeueDecimal()); + ItemTrackingLines.OK().Invoke(); + end; + +} diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocLinkToExistingTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocLinkToExistingTest.Codeunit.al index 700fa146e3..a09a4ab419 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocLinkToExistingTest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocLinkToExistingTest.Codeunit.al @@ -417,7 +417,7 @@ codeunit 139886 "E-Doc Link To Existing Test" CreatedPurchaseHeader: Record "Purchase Header"; ExistingPurchaseHeader: Record "Purchase Header"; ICPartner: Record "IC Partner"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; EDocPurchaseDraftTestPage: TestPage "E-Document Purchase Draft"; CreatedDocNo: Code[20]; @@ -438,8 +438,8 @@ codeunit 139886 "E-Doc Link To Existing Test" EDocumentPurchaseHeader.Modify(); // [GIVEN] Finalize draft to create a new PI from e-document - EDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Finish draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := Enum::"Import E-Document Steps"::"Finish draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocument.Get(EDocument."Entry No"); // [GIVEN] Find the created purchase invoice @@ -594,4 +594,4 @@ codeunit 139886 "E-Doc Link To Existing Test" LibraryPurchase: Codeunit "Library - Purchase"; LibraryERM: Codeunit "Library - ERM"; LibraryVariableStorage: Codeunit "Library - Variable Storage"; -} \ No newline at end of file +} diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMLLMTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMLLMTests.Codeunit.al new file mode 100644 index 0000000000..f3daf6bae7 --- /dev/null +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMLLMTests.Codeunit.al @@ -0,0 +1,293 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Test; + +using Microsoft.eServices.EDocument.Format; +using Microsoft.eServices.EDocument.Processing.Import; +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.Finance.GeneralLedger.Setup; +using System.TestLibraries.Config; + +codeunit 135647 "EDoc MLLM Tests" +{ + Subtype = Test; + EventSubscriberInstance = Manual; + + var + Assert: Codeunit Assert; + LibraryLowerPermission: Codeunit "Library - Lower Permissions"; + + [Test] + procedure MapHeader_FullInvoice() + var + TempHeader: Record "E-Document Purchase Header" temporary; + EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper"; + HeaderObj: JsonObject; + begin + // [SCENARIO] MapHeaderFromJson maps all fields from a complete UBL invoice JSON + LibraryLowerPermission.SetOutsideO365Scope(); + EnsureGLSetup(); + + HeaderObj := BuildFullHeaderJson(); + + EDocMLLMSchemaHelper.MapHeaderFromJson(HeaderObj, TempHeader); + + Assert.AreEqual('MLLM-INV-001', TempHeader."Sales Invoice No.", 'Sales Invoice No.'); + Assert.AreEqual(DMY2Date(15, 3, 2024), TempHeader."Document Date", 'Document Date'); + Assert.AreEqual(DMY2Date(15, 4, 2024), TempHeader."Due Date", 'Due Date'); + Assert.AreEqual('XYZ', TempHeader."Currency Code", 'Currency Code'); + Assert.AreEqual('PO-5678', TempHeader."Purchase Order No.", 'Purchase Order No.'); + Assert.AreEqual('Net 30', TempHeader."Payment Terms", 'Payment Terms'); + Assert.AreEqual('Contoso Supplies Ltd.', TempHeader."Vendor Company Name", 'Vendor Company Name'); + Assert.AreEqual('123 Bill Ave, Seattle 98101, US', TempHeader."Vendor Address", 'Vendor Address'); + Assert.AreEqual('US-VAT-12345', TempHeader."Vendor VAT Id", 'Vendor VAT Id'); + Assert.AreEqual('John Doe', TempHeader."Vendor Contact Name", 'Vendor Contact Name'); + Assert.AreEqual('Microsoft Corporation', TempHeader."Customer Company Name", 'Customer Company Name'); + Assert.AreEqual('456 Main St, Redmond 98052, US', TempHeader."Customer Address", 'Customer Address'); + Assert.AreEqual('US-VAT-67890', TempHeader."Customer VAT Id", 'Customer VAT Id'); + Assert.AreEqual('789 Ship Rd, Bellevue 98004, US', TempHeader."Shipping Address", 'Shipping Address'); + Assert.AreEqual('Warehouse Team', TempHeader."Shipping Address Recipient", 'Shipping Address Recipient'); + Assert.AreEqual('Contoso Billing Dept', TempHeader."Remittance Address Recipient", 'Remittance Address Recipient'); + Assert.AreEqual(37.5, TempHeader."Total VAT", 'Total VAT'); + Assert.AreEqual(250.0, TempHeader."Sub Total", 'Sub Total'); + Assert.AreEqual(5.0, TempHeader."Total Discount", 'Total Discount'); + Assert.AreEqual(287.5, TempHeader.Total, 'Total'); + Assert.AreEqual(287.5, TempHeader."Amount Due", 'Amount Due'); + end; + + [Test] + procedure MapHeader_MissingOptionalFields() + var + TempHeader: Record "E-Document Purchase Header" temporary; + EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper"; + HeaderObj: JsonObject; + begin + // [SCENARIO] Missing nested objects (delivery, payment_means) leave fields empty without error + LibraryLowerPermission.SetOutsideO365Scope(); + EnsureGLSetup(); + + HeaderObj.Add('id', 'INV-MINIMAL'); + HeaderObj.Add('issue_date', '2024-01-01'); + + EDocMLLMSchemaHelper.MapHeaderFromJson(HeaderObj, TempHeader); + + Assert.AreEqual('INV-MINIMAL', TempHeader."Sales Invoice No.", 'Sales Invoice No.'); + Assert.AreEqual(DMY2Date(1, 1, 2024), TempHeader."Document Date", 'Document Date'); + Assert.AreEqual(0D, TempHeader."Due Date", 'Due Date should be empty'); + Assert.AreEqual('', TempHeader."Currency Code", 'Currency Code should be empty'); + Assert.AreEqual('', TempHeader."Purchase Order No.", 'Purchase Order No. should be empty'); + Assert.AreEqual('', TempHeader."Payment Terms", 'Payment Terms should be empty'); + Assert.AreEqual('', TempHeader."Vendor Company Name", 'Vendor Company Name should be empty'); + Assert.AreEqual('', TempHeader."Vendor Address", 'Vendor Address should be empty'); + Assert.AreEqual('', TempHeader."Customer Company Name", 'Customer Company Name should be empty'); + Assert.AreEqual('', TempHeader."Shipping Address", 'Shipping Address should be empty'); + Assert.AreEqual('', TempHeader."Shipping Address Recipient", 'Shipping Address Recipient should be empty'); + Assert.AreEqual('', TempHeader."Remittance Address Recipient", 'Remittance Address Recipient should be empty'); + Assert.AreEqual(0, TempHeader."Total VAT", 'Total VAT should be zero'); + Assert.AreEqual(0, TempHeader."Sub Total", 'Sub Total should be zero'); + Assert.AreEqual(0, TempHeader.Total, 'Total should be zero'); + end; + + [Test] + procedure MapHeader_EmptyObject() + var + TempHeader: Record "E-Document Purchase Header" temporary; + EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper"; + HeaderObj: JsonObject; + begin + // [SCENARIO] Empty JSON object leaves all fields empty/zero without error + LibraryLowerPermission.SetOutsideO365Scope(); + EnsureGLSetup(); + + EDocMLLMSchemaHelper.MapHeaderFromJson(HeaderObj, TempHeader); + + Assert.AreEqual('', TempHeader."Sales Invoice No.", 'Sales Invoice No. should be empty'); + Assert.AreEqual(0D, TempHeader."Document Date", 'Document Date should be empty'); + Assert.AreEqual(0D, TempHeader."Due Date", 'Due Date should be empty'); + Assert.AreEqual('', TempHeader."Currency Code", 'Currency Code should be empty'); + Assert.AreEqual('', TempHeader."Vendor Company Name", 'Vendor Company Name should be empty'); + Assert.AreEqual('', TempHeader."Customer Company Name", 'Customer Company Name should be empty'); + Assert.AreEqual(0, TempHeader."Sub Total", 'Sub Total should be zero'); + Assert.AreEqual(0, TempHeader.Total, 'Total should be zero'); + Assert.AreEqual(0, TempHeader."Amount Due", 'Amount Due should be zero'); + end; + + [Test] + procedure MapLines_MultipleLines() + var + TempLine: Record "E-Document Purchase Line" temporary; + EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper"; + LinesArray: JsonArray; + begin + // [SCENARIO] Three invoice lines produce correct line numbers and field values + LibraryLowerPermission.SetOutsideO365Scope(); + + LinesArray := BuildThreeLineArray(); + + EDocMLLMSchemaHelper.MapLinesFromJson(LinesArray, 1, TempLine); + + TempLine.FindSet(); + Assert.AreEqual(10000, TempLine."Line No.", 'First line number'); + Assert.AreEqual('Consulting Services', TempLine.Description, 'Line 1 Description'); + Assert.AreEqual('SVC-001', TempLine."Product Code", 'Line 1 Product Code'); + Assert.AreEqual(5, TempLine.Quantity, 'Line 1 Quantity'); + Assert.AreEqual('HRS', TempLine."Unit of Measure", 'Line 1 Unit of Measure'); + Assert.AreEqual(40, TempLine."Unit Price", 'Line 1 Unit Price'); + Assert.AreEqual(200, TempLine."Sub Total", 'Line 1 Sub Total'); + Assert.AreEqual(15, TempLine."VAT Rate", 'Line 1 VAT Rate'); + Assert.AreEqual(5, TempLine."Total Discount", 'Line 1 Total Discount'); + + TempLine.Next(); + Assert.AreEqual(20000, TempLine."Line No.", 'Second line number'); + Assert.AreEqual('Office Supplies', TempLine.Description, 'Line 2 Description'); + Assert.AreEqual('MAT-002', TempLine."Product Code", 'Line 2 Product Code'); + Assert.AreEqual(10, TempLine.Quantity, 'Line 2 Quantity'); + Assert.AreEqual('PCS', TempLine."Unit of Measure", 'Line 2 Unit of Measure'); + Assert.AreEqual(3, TempLine."Unit Price", 'Line 2 Unit Price'); + Assert.AreEqual(30, TempLine."Sub Total", 'Line 2 Sub Total'); + Assert.AreEqual(10, TempLine."VAT Rate", 'Line 2 VAT Rate'); + + TempLine.Next(); + Assert.AreEqual(30000, TempLine."Line No.", 'Third line number'); + Assert.AreEqual('Express Delivery', TempLine.Description, 'Line 3 Description'); + Assert.AreEqual('DLV-003', TempLine."Product Code", 'Line 3 Product Code'); + Assert.AreEqual(1, TempLine.Quantity, 'Line 3 Quantity'); + Assert.AreEqual('EA', TempLine."Unit of Measure", 'Line 3 Unit of Measure'); + Assert.AreEqual(20, TempLine."Unit Price", 'Line 3 Unit Price'); + Assert.AreEqual(20, TempLine."Sub Total", 'Line 3 Sub Total'); + Assert.AreEqual(15, TempLine."VAT Rate", 'Line 3 VAT Rate'); + end; + + [Test] + procedure MapLines_ZeroQuantity() + var + TempLine: Record "E-Document Purchase Line" temporary; + EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper"; + LinesArray: JsonArray; + LineObj: JsonObject; + ItemObj: JsonObject; + QuantityObj: JsonObject; + begin + // [SCENARIO] Line with quantity 0 defaults to 1 + LibraryLowerPermission.SetOutsideO365Scope(); + + ItemObj.Add('name', 'Zero Qty Item'); + QuantityObj.Add('value', 0); + QuantityObj.Add('unit_code', 'PCS'); + LineObj.Add('item', ItemObj); + LineObj.Add('invoiced_quantity', QuantityObj); + LinesArray.Add(LineObj); + + EDocMLLMSchemaHelper.MapLinesFromJson(LinesArray, 1, TempLine); + + TempLine.FindFirst(); + Assert.AreEqual(1, TempLine.Quantity, 'Zero quantity should default to 1'); + Assert.AreEqual('Zero Qty Item', TempLine.Description, 'Description'); + end; + + [Test] + procedure MapLines_EmptyArray() + var + TempLine: Record "E-Document Purchase Line" temporary; + EDocMLLMSchemaHelper: Codeunit "E-Doc. MLLM Schema Helper"; + LinesArray: JsonArray; + begin + // [SCENARIO] Empty lines array produces no line records + LibraryLowerPermission.SetOutsideO365Scope(); + + EDocMLLMSchemaHelper.MapLinesFromJson(LinesArray, 1, TempLine); + + Assert.IsTrue(TempLine.IsEmpty(), 'No lines should be inserted for empty array'); + end; + + [Test] + procedure PreferredImpl_ControlAllocation_ReturnsADI() + var + EDocPDFFileFormat: Codeunit "E-Doc. PDF File Format"; + FeatureConfigTestLib: Codeunit "Feature Config Test Lib."; + begin + // [SCENARIO] With control allocation, PreferredStructureDataImplementation returns ADI + LibraryLowerPermission.SetOutsideO365Scope(); + + FeatureConfigTestLib.UseControlAllocation(); + + Assert.AreEqual( + "Structure Received E-Doc."::ADI, + EDocPDFFileFormat.PreferredStructureDataImplementation(), + 'Control allocation should return ADI'); + end; + + [Test] + procedure PreferredImpl_TreatmentAllocation_ReturnsMLLM() + // var + // EDocPDFFileFormat: Codeunit "E-Doc. PDF File Format"; + // FeatureConfigTestLib: Codeunit "Feature Config Test Lib."; + begin + // Bug #624677: ECS must be enabled for this test to pass. See wiki for ECS configuration. + // [SCENARIO] With treatment allocation, PreferredStructureDataImplementation returns MLLM + // LibraryLowerPermission.SetOutsideO365Scope(); + + // FeatureConfigTestLib.UseTreatmentAllocation(); + + // Assert.AreEqual( + // "Structure Received E-Doc."::MLLM, + // EDocPDFFileFormat.PreferredStructureDataImplementation(), + // 'Treatment allocation should return MLLM'); + end; + + [Test] + procedure PreferredImpl_EventOverride_TakesPrecedence() + var + EDocPDFFileFormat: Codeunit "E-Doc. PDF File Format"; + FeatureConfigTestLib: Codeunit "Feature Config Test Lib."; + EDocMLLMTests: Codeunit "EDoc MLLM Tests"; + begin + // [SCENARIO] An event subscriber can override the result regardless of experiment allocation + LibraryLowerPermission.SetOutsideO365Scope(); + + FeatureConfigTestLib.UseControlAllocation(); // Would normally return ADI + BindSubscription(EDocMLLMTests); + + Assert.AreEqual( + "Structure Received E-Doc."::MLLM, + EDocPDFFileFormat.PreferredStructureDataImplementation(), + 'Event override should take precedence over experiment allocation'); + + UnbindSubscription(EDocMLLMTests); + end; + + [EventSubscriber(ObjectType::Codeunit, Codeunit::"E-Doc. PDF File Format", OnAfterSetIStructureReceivedEDocumentForPdf, '', false, false)] + local procedure OverrideToMLLM(var Result: Enum "Structure Received E-Doc.") + begin + Result := "Structure Received E-Doc."::MLLM; + end; + + local procedure EnsureGLSetup() + var + GeneralLedgerSetup: Record "General Ledger Setup"; + begin + if not GeneralLedgerSetup.Get() then begin + GeneralLedgerSetup.Init(); + GeneralLedgerSetup."LCY Code" := 'USD'; + GeneralLedgerSetup.Insert(); + end; + end; + + local procedure BuildFullHeaderJson(): JsonObject + var + Result: JsonObject; + begin + Result.ReadFrom(NavApp.GetResourceAsText('mllm/mllm-header-full.json')); + exit(Result); + end; + + local procedure BuildThreeLineArray(): JsonArray + var + Result: JsonArray; + begin + Result.ReadFrom(NavApp.GetResourceAsText('mllm/mllm-lines-three.json')); + exit(Result); + end; +} diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocMockCustomizations.EnumExt.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocMockCustomizations.EnumExt.al index 35eab277e6..d55253dc74 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocMockCustomizations.EnumExt.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocMockCustomizations.EnumExt.al @@ -11,6 +11,7 @@ enumextension 133501 "E-Doc. Mock Customizations" extends "E-Doc. Proc. Customiz { value(133501; "Mock Create Purchase Invoice") { - Implementation = IEDocumentCreatePurchaseInvoice = "E-Doc. Processing Mocks"; + Implementation = IEDocumentCreatePurchaseInvoice = "E-Doc. Processing Mocks", + IEDocumentCreatePurchaseCreditMemo = "E-Doc. Processing Mocks"; } } \ No newline at end of file diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocPDFMock.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocPDFMock.Codeunit.al index c1adc40b13..6c7376fd77 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocPDFMock.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocPDFMock.Codeunit.al @@ -22,7 +22,7 @@ codeunit 139782 "E-Doc PDF Mock" implements IStructureReceivedEDocument, IStruct EDocumentPurchaseHeader."Vendor VAT Id" := '1111111111234'; EDocumentPurchaseHeader.Insert(); end; - exit(Enum::"E-Doc. Process Draft"::"Purchase Document"); + exit(Enum::"E-Doc. Process Draft"::"Purchase Invoice"); end; procedure View(EDocument: Record "E-Document"; TempBlob: Codeunit "Temp Blob") diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al index c6700ea65f..8a391d8117 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessTest.Codeunit.al @@ -11,6 +11,7 @@ using Microsoft.eServices.EDocument.Processing; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; using Microsoft.Finance.Currency; +using Microsoft.Finance.Dimension; using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Setup; using Microsoft.Foundation.Company; @@ -48,7 +49,7 @@ codeunit 139883 "E-Doc Process Test" procedure ProcessStructureReceivedData() var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocDataStorage: Record "E-Doc. Data Storage"; EDocLogRecord: Record "E-Document Log"; EDocImport: Codeunit "E-Doc. Import"; @@ -70,11 +71,11 @@ codeunit 139883 "E-Doc Process Test" EDocument.Modify(); EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::Unprocessed); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Structure received data"; + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Structure received data"; EDocument.CalcFields("Import Processing Status"); Assert.AreEqual(Enum::"Import E-Doc. Proc. Status"::Unprocessed, EDocument."Import Processing Status", 'The status should be updated to the one after the step executed.'); - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocument.CalcFields("Import Processing Status"); Assert.AreEqual(Enum::"Import E-Doc. Proc. Status"::Readable, EDocument."Import Processing Status", 'The status should be updated to the one after the step executed.'); EDocument.Get(EDocument."Entry No"); @@ -90,7 +91,7 @@ codeunit 139883 "E-Doc Process Test" procedure ProcessingDoesSequenceOfSteps() var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocLogRecord: Record "E-Document Log"; EDocImport: Codeunit "E-Doc. Import"; EDocumentProcessing: Codeunit "E-Document Processing"; @@ -111,18 +112,18 @@ codeunit 139883 "E-Doc Process Test" EDocument.Modify(); EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::Unprocessed); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocument.CalcFields("Import Processing Status"); - Assert.AreEqual(ImportEDocumentProcess.GetStatusForStep(EDocImportParameters."Step to Run", false), EDocument."Import Processing Status", 'The status should be updated to the one after the step executed.'); + Assert.AreEqual(ImportEDocumentProcess.GetStatusForStep(TempEDocImportParameters."Step to Run", false), EDocument."Import Processing Status", 'The status should be updated to the one after the step executed.'); end; [Test] procedure ProcessingUndoesSteps() var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocLogRecord: Record "E-Document Log"; EDocImport: Codeunit "E-Doc. Import"; EDocumentProcessing: Codeunit "E-Document Processing"; @@ -143,17 +144,17 @@ codeunit 139883 "E-Doc Process Test" EDocument.Modify(); EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::Unprocessed); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocument.CalcFields("Import Processing Status"); - Assert.AreEqual(ImportEDocumentProcess.GetStatusForStep(EDocImportParameters."Step to Run", false), EDocument."Import Processing Status", 'The status should be updated to the one after the step executed.'); + Assert.AreEqual(ImportEDocumentProcess.GetStatusForStep(TempEDocImportParameters."Step to Run", false), EDocument."Import Processing Status", 'The status should be updated to the one after the step executed.'); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Structure received data"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Structure received data"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocument.CalcFields("Import Processing Status"); - Assert.AreEqual(ImportEDocumentProcess.GetStatusForStep(EDocImportParameters."Step to Run", false), EDocument."Import Processing Status", 'The status should be updated to the one after the step executed.'); + Assert.AreEqual(ImportEDocumentProcess.GetStatusForStep(TempEDocImportParameters."Step to Run", false), EDocument."Import Processing Status", 'The status should be updated to the one after the step executed.'); end; [Test] @@ -161,7 +162,7 @@ codeunit 139883 "E-Doc Process Test" var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocLogRecord: Record "E-Document Log"; PurchaseHeader: Record "Purchase Header"; EDocumentLog: Codeunit "E-Document Log"; @@ -187,8 +188,8 @@ codeunit 139883 "E-Doc Process Test" EDocument.Modify(); EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Ready for draft"); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocumentPurchaseHeader.SetRecFilter(); EDocumentPurchaseHeader.FindFirst(); @@ -204,7 +205,7 @@ codeunit 139883 "E-Doc Process Test" var EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; Vendor2: Record Vendor; CompanyInformation: Record "Company Information"; EDocumentProcessing: Codeunit "E-Document Processing"; @@ -223,8 +224,8 @@ codeunit 139883 "E-Doc Process Test" EDocumentPurchaseHeader.Insert(); EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Ready for draft"); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocumentPurchaseHeader.SetRecFilter(); EDocumentPurchaseHeader.FindFirst(); @@ -240,7 +241,7 @@ codeunit 139883 "E-Doc Process Test" EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; Vendor2: Record Vendor; CompanyInformation: Record "Company Information"; GLAccount: Record "G/L Account"; @@ -272,8 +273,8 @@ codeunit 139883 "E-Doc Process Test" EDocumentPurchaseLine.Insert(); EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Ready for draft"); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocumentPurchaseLine.SetRecFilter(); EDocumentPurchaseLine.FindFirst(); @@ -296,7 +297,7 @@ codeunit 139883 "E-Doc Process Test" procedure FinishDraftCanBeUndone() var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; PurchaseHeader: Record "Purchase Header"; EDocLogRecord: Record "E-Document Log"; EDocImport: Codeunit "E-Doc. Import"; @@ -319,15 +320,15 @@ codeunit 139883 "E-Doc Process Test" EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Draft Ready"); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImportParameters."Processing Customizations" := "E-Doc. Proc. Customizations"::"Mock Create Purchase Invoice"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + TempEDocImportParameters."Processing Customizations" := "E-Doc. Proc. Customizations"::"Mock Create Purchase Invoice"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); PurchaseHeader.SetRange("E-Document Link", EDocument.SystemId); PurchaseHeader.FindFirst(); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Structure received data"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Structure received data"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); Assert.RecordIsEmpty(PurchaseHeader); end; @@ -336,7 +337,7 @@ codeunit 139883 "E-Doc Process Test" procedure FinishDraftFromReadyForDraftStateSucceeds() var EDocument: Record "E-Document"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; PurchaseHeader: Record "Purchase Header"; EDocLogRecord: Record "E-Document Log"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; @@ -374,9 +375,9 @@ codeunit 139883 "E-Doc Process Test" Assert.AreEqual(Enum::"Import E-Doc. Proc. Status"::"Ready for draft", EDocument."Import Processing Status", 'The status should be Ready for draft before processing.'); // [WHEN] Finish draft step is executed (simulating finalize action) - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; - EDocImportParameters."Processing Customizations" := "E-Doc. Proc. Customizations"::"Mock Create Purchase Invoice"; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + TempEDocImportParameters."Processing Customizations" := "E-Doc. Proc. Customizations"::"Mock Create Purchase Invoice"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); // [THEN] The document is processed (the system ran Prepare draft automatically and then Finish draft) EDocument.CalcFields("Import Processing Status"); @@ -392,7 +393,7 @@ codeunit 139883 "E-Doc Process Test" procedure ProcessingInboundDocumentCreatesLinks() var EDocument: Record "E-Document"; - EDocImportParams: Record "E-Doc. Import Parameters"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; PurchaseHeader: Record "Purchase Header"; PurchaseLine: Record "Purchase Line"; EDocRecordLink: Record "E-Doc. Record Link"; @@ -405,9 +406,9 @@ codeunit 139883 "E-Doc Process Test" EDocRecordLink.DeleteAll(); // [GIVEN] An inbound e-document is received and fully processed - EDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; WorkDate(DMY2Date(1, 1, 2027)); // Peppol document date is in 2026 - Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', EDocImportParams), 'The e-document should be processed'); + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', TempEDocImportParams), 'The e-document should be processed'); EDocument.Get(EDocument."Entry No"); PurchaseHeader.Get(EDocument."Document Record ID"); @@ -431,7 +432,7 @@ codeunit 139883 "E-Doc Process Test" procedure PostingInboundDocumentCreatesHistoricalRecords() var EDocument: Record "E-Document"; - EDocImportParams: Record "E-Doc. Import Parameters"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; PurchaseHeader: Record "Purchase Header"; PurchaseInvoiceHeader: Record "Purch. Inv. Header"; EDocVendorAssignmentHistory: Record "E-Doc. Vendor Assign. History"; @@ -445,9 +446,9 @@ codeunit 139883 "E-Doc Process Test" EDocumentService.Modify(); // [GIVEN] An inbound e-document is received and fully processed - EDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; WorkDate(DMY2Date(1, 1, 2027)); // Peppol document date is in 2026 - Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', EDocImportParams), 'The e-document should be processed'); + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', TempEDocImportParams), 'The e-document should be processed'); EDocument.Get(EDocument."Entry No"); PurchaseHeader.Get(EDocument."Document Record ID"); @@ -481,7 +482,7 @@ codeunit 139883 "E-Doc Process Test" PurchaseLine: Record "Purchase Line"; PurchaseInvoiceLine: Record "Purch. Inv. Line"; EDocument: Record "E-Document"; - EDocImportParams: Record "E-Doc. Import Parameters"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; PurchaseHeader: Record "Purchase Header"; EDocPurchLineField: Record "E-Document Line - Field"; EDocPurchaseLine: Record "E-Document Purchase Line"; @@ -498,9 +499,9 @@ codeunit 139883 "E-Doc Process Test" EDocPurchLineFieldSetup."Field No." := PurchaseInvoiceLine.FieldNo("IC Partner Code"); EDocPurchLineFieldSetup.Insert(); // [GIVEN] An inbound e-document is received and a draft created - EDocImportParams."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Prepare draft"; WorkDate(DMY2Date(1, 1, 2027)); // Peppol document date is in 2026 - Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', EDocImportParams), 'The draft for the e-document should be created'); + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', TempEDocImportParams), 'The draft for the e-document should be created'); // [WHEN] Storing custom values for the additional fields of the first line EDocPurchLineField."E-Document Entry No." := EDocument."Entry No"; @@ -514,8 +515,8 @@ codeunit 139883 "E-Doc Process Test" EDocPurchLineField.Insert(); // [WHEN] Creating a purchase invoice from the draft - EDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; - Assert.IsTrue(EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParams), 'The e-document should be processed'); + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + Assert.IsTrue(EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParams), 'The e-document should be processed'); // [THEN] The additional fields should be set on the purchase invoice line EDocument.Get(EDocument."Entry No"); @@ -533,7 +534,7 @@ codeunit 139883 "E-Doc Process Test" PurchaseLine: Record "Purchase Line"; PurchaseInvoiceLine: Record "Purch. Inv. Line"; EDocument: Record "E-Document"; - EDocImportParams: Record "E-Doc. Import Parameters"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; PurchaseHeader: Record "Purchase Header"; EDocPurchLineField: Record "E-Document Line - Field"; EDocPurchaseLine: Record "E-Document Purchase Line"; @@ -550,9 +551,9 @@ codeunit 139883 "E-Doc Process Test" EDocPurchLineFieldSetup."Field No." := PurchaseInvoiceLine.FieldNo("IC Partner Code"); EDocPurchLineFieldSetup.Insert(); // [GIVEN] An inbound e-document is received and a draft created - EDocImportParams."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Prepare draft"; WorkDate(DMY2Date(1, 1, 2027)); // Peppol document date is in 2026 - Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', EDocImportParams), 'The draft for the e-document should be created'); + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', TempEDocImportParams), 'The draft for the e-document should be created'); // [GIVEN] Custom values for the additional fields of the first line are configured EDocPurchLineField."E-Document Entry No." := EDocument."Entry No"; @@ -568,8 +569,8 @@ codeunit 139883 "E-Doc Process Test" // [WHEN] Removing the general setup for the additional fields EDocPurchLineFieldSetup.DeleteAll(); // [WHEN] Creating a purchase invoice from the draft - EDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; - Assert.IsTrue(EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParams), 'The e-document should be processed'); + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + Assert.IsTrue(EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParams), 'The e-document should be processed'); // [THEN] The additional fields should not be set on the purchase invoice line EDocument.Get(EDocument."Entry No"); @@ -586,7 +587,7 @@ codeunit 139883 "E-Doc Process Test" EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; Vendor2: Record Vendor; CompanyInformation: Record "Company Information"; Item: Record Item; @@ -615,10 +616,10 @@ codeunit 139883 "E-Doc Process Test" EDocumentPurchaseLine.Insert(); EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Ready for draft"); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; // [WHEN] Filling in the draft - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocumentPurchaseLine.SetRecFilter(); EDocumentPurchaseLine.FindFirst(); @@ -645,7 +646,7 @@ codeunit 139883 "E-Doc Process Test" EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; Vendor2: Record Vendor; CompanyInformation: Record "Company Information"; Item: Record Item; @@ -680,10 +681,10 @@ codeunit 139883 "E-Doc Process Test" ItemReference.Modify(); EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Ready for draft"); - EDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Prepare draft"; // [WHEN] Filling in the draft - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocumentPurchaseLine.SetRecFilter(); EDocumentPurchaseLine.FindFirst(); @@ -703,6 +704,186 @@ codeunit 139883 "E-Doc Process Test" if ItemReference.Delete() then; end; + [Test] + procedure FinishDraftCreditMemoCanBeUndone() + var + EDocument: Record "E-Document"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; + PurchaseHeader: Record "Purchase Header"; + EDocLogRecord: Record "E-Document Log"; + EDocImport: Codeunit "E-Doc. Import"; + EDocumentLog: Codeunit "E-Document Log"; + EDocumentProcessing: Codeunit "E-Document Processing"; + begin + // [SCENARIO] A credit memo created via FinishDraft can be reverted + Initialize(Enum::"Service Integration"::"Mock"); + LibraryEDoc.CreateInboundEDocument(EDocument, EDocumentService); + EDocument."Document Type" := "E-Document Type"::"Purchase Credit Memo"; + EDocument.Modify(); + EDocumentService."Import Process" := "E-Document Import Process"::"Version 2.0"; + EDocumentService.Modify(); + + EDocumentLog.SetBlob('Test', Enum::"E-Doc. File Format"::XML, 'Data'); + EDocumentLog.SetFields(EDocument, EDocumentService); + EDocLogRecord := EDocumentLog.InsertLog(Enum::"E-Document Service Status"::Imported, Enum::"Import E-Doc. Proc. Status"::Readable); + + EDocument."Structured Data Entry No." := EDocLogRecord."E-Doc. Data Storage Entry No."; + EDocument.Modify(); + + // [GIVEN] A credit memo is created via FinishDraft + EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::"Draft Ready"); + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Finish draft"; + TempEDocImportParameters."Processing Customizations" := "E-Doc. Proc. Customizations"::"Mock Create Purchase Invoice"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); + + PurchaseHeader.SetRange("E-Document Link", EDocument.SystemId); + PurchaseHeader.FindFirst(); + Assert.AreEqual("Purchase Document Type"::"Credit Memo", PurchaseHeader."Document Type", 'The document type should be Credit Memo.'); + + // [WHEN] Undo is performed + TempEDocImportParameters."Step to Run" := "Import E-Document Steps"::"Structure received data"; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); + + // [THEN] The credit memo is removed + Assert.RecordIsEmpty(PurchaseHeader); + end; + + [Test] + [HandlerFunctions('EditDimensionSetEntriesHandler')] + procedure ManuallyAddedDimensionsOnDraftAreCarriedToPurchaseInvoice() + var + EDocument: Record "E-Document"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; + EDocPurchaseLine: Record "E-Document Purchase Line"; + EDocPurchaseLineReread: Record "E-Document Purchase Line"; + DimensionValue: Record "Dimension Value"; + DimSetEntry: Record "Dimension Set Entry"; + LibraryDimension: Codeunit "Library - Dimension"; + begin + // [SCENARIO] When the user edits dimensions on an e-document draft line via LookupDimensions, + // the changes should be persisted to the database. + Initialize(Enum::"Service Integration"::"Mock"); + EDocumentService."Read into Draft Impl." := "E-Doc. Read into Draft"::PEPPOL; + EDocumentService.Modify(); + + // [GIVEN] An inbound e-document is received and a draft is created + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Prepare draft"; + WorkDate(DMY2Date(1, 1, 2027)); + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', TempEDocImportParams), 'The draft should be created'); + + // [GIVEN] A dimension value to add via the Dimensions lookup + LibraryDimension.CreateDimWithDimValue(DimensionValue); + LibraryVariableStorage.Enqueue(DimensionValue."Dimension Code"); + LibraryVariableStorage.Enqueue(DimensionValue.Code); + + // [WHEN] LookupDimensions is called on the draft line (simulating the Dimensions page action) + EDocPurchaseLine.SetRange("E-Document Entry No.", EDocument."Entry No"); + EDocPurchaseLine.FindFirst(); + EDocPurchaseLine.LookupDimensions(); // Opens modal handled by EditDimensionSetEntriesHandler + + // [THEN] The dimension should be persisted on the e-document purchase line when re-read from the database + EDocPurchaseLineReread.Get(EDocPurchaseLine."E-Document Entry No.", EDocPurchaseLine."Line No."); + + DimSetEntry.SetRange("Dimension Set ID", EDocPurchaseLineReread."[BC] Dimension Set ID"); + DimSetEntry.SetRange("Dimension Code", DimensionValue."Dimension Code"); + DimSetEntry.SetRange("Dimension Value Code", DimensionValue.Code); + Assert.RecordIsNotEmpty(DimSetEntry); + end; + + [ModalPageHandler] + procedure EditDimensionSetEntriesHandler(var EditDimensionSetEntries: TestPage "Edit Dimension Set Entries") + var + DimensionCode: Code[20]; + DimensionValueCode: Code[20]; + begin + DimensionCode := CopyStr(LibraryVariableStorage.DequeueText(), 1, 20); + DimensionValueCode := CopyStr(LibraryVariableStorage.DequeueText(), 1, 20); + EditDimensionSetEntries.New(); + EditDimensionSetEntries."Dimension Code".SetValue(DimensionCode); + EditDimensionSetEntries.DimensionValueCode.SetValue(DimensionValueCode); + EditDimensionSetEntries.OK().Invoke(); + end; + + [Test] + procedure ProcessingInboundCreditNoteCreatesCorrectDocumentType() + var + EDocument: Record "E-Document"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + EDocRecordLink: Record "E-Doc. Record Link"; + begin + // [SCENARIO] A PEPPOL CreditNote processed through the full pipeline creates a Purchase Credit Memo with correct content + Initialize(Enum::"Service Integration"::"Mock"); + EDocumentService."Read into Draft Impl." := "E-Doc. Read into Draft"::PEPPOL; + EDocumentService.Modify(); + + EDocRecordLink.DeleteAll(); + + // [GIVEN] An inbound credit note e-document is received and fully processed + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + WorkDate(DMY2Date(1, 1, 2027)); + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-creditnote-0.xml', TempEDocImportParams), 'The credit note e-document should be processed'); + + // [THEN] The E-Document type is Purchase Credit Memo + EDocument.Get(EDocument."Entry No"); + Assert.AreEqual("E-Document Type"::"Purchase Credit Memo", EDocument."Document Type", 'The document type should be Purchase Credit Memo.'); + + // [THEN] A Purchase Credit Memo header is created with correct fields + PurchaseHeader.Get(EDocument."Document Record ID"); + Assert.AreEqual("Purchase Document Type"::"Credit Memo", PurchaseHeader."Document Type", 'The purchase header document type should be Credit Memo.'); + Assert.AreEqual(EDocument.SystemId, PurchaseHeader."E-Document Link", 'The E-Document link should be set on the purchase header.'); + Assert.AreEqual('CN-5001', PurchaseHeader."Vendor Cr. Memo No.", 'The vendor credit memo number should match the CreditNote ID.'); + Assert.AreEqual(Vendor."No.", PurchaseHeader."Buy-from Vendor No.", 'The vendor should be resolved from the CreditNote.'); + Assert.AreEqual(2500, PurchaseHeader."Doc. Amount Incl. VAT", 'The document amount incl. VAT should match the CreditNote total.'); + Assert.AreEqual('5', PurchaseHeader."Vendor Order No.", 'The Vendor Order No. should match the OrderReference from the CreditNote.'); + + // [THEN] The purchase credit memo has the correct number of lines + PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type"); + PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); + Assert.RecordCount(PurchaseLine, 1); + + // [THEN] Links are created between e-document and purchase records + EDocRecordLink.SetRange("Target Table No.", Database::"Purchase Header"); + EDocRecordLink.SetRange("Target SystemId", PurchaseHeader.SystemId); + Assert.RecordCount(EDocRecordLink, 1); + end; + + [Test] + procedure ProcessingInboundInvoiceStillCreatesCorrectDocumentType() + var + EDocument: Record "E-Document"; + TempEDocImportParams: Record "E-Doc. Import Parameters"; + PurchaseHeader: Record "Purchase Header"; + PurchaseLine: Record "Purchase Line"; + begin + // [SCENARIO] After the refactoring, a PEPPOL Invoice still creates a Purchase Invoice with correct content (regression check) + Initialize(Enum::"Service Integration"::"Mock"); + EDocumentService."Read into Draft Impl." := "E-Doc. Read into Draft"::PEPPOL; + EDocumentService.Modify(); + + TempEDocImportParams."Step to Run" := "Import E-Document Steps"::"Finish draft"; + WorkDate(DMY2Date(1, 1, 2027)); + Assert.IsTrue(LibraryEDoc.CreateInboundPEPPOLDocumentToState(EDocument, EDocumentService, 'peppol/peppol-invoice-0.xml', TempEDocImportParams), 'The invoice e-document should be processed'); + + // [THEN] The E-Document type is Purchase Invoice + EDocument.Get(EDocument."Entry No"); + Assert.AreEqual("E-Document Type"::"Purchase Invoice", EDocument."Document Type", 'The document type should be Purchase Invoice.'); + + // [THEN] A Purchase Invoice header is created with correct fields + PurchaseHeader.Get(EDocument."Document Record ID"); + Assert.AreEqual("Purchase Document Type"::Invoice, PurchaseHeader."Document Type", 'The purchase header document type should be Invoice.'); + Assert.AreEqual('103033', PurchaseHeader."Vendor Invoice No.", 'The vendor invoice number should match the Invoice ID.'); + Assert.AreEqual('2', PurchaseHeader."Vendor Order No.", 'The vendor order number should match the OrderReference from the Invoice.'); + Assert.AreEqual(Vendor."No.", PurchaseHeader."Buy-from Vendor No.", 'The vendor should be resolved from the Invoice.'); + Assert.AreEqual(14140, PurchaseHeader."Doc. Amount Incl. VAT", 'The document amount incl. VAT should match the Invoice total.'); + + // [THEN] The purchase invoice has the correct number of lines (2 from peppol-invoice-0.xml) + PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type"); + PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); + Assert.RecordCount(PurchaseLine, 2); + end; + local procedure Initialize(Integration: Enum "Service Integration") var TransformationRule: Record "Transformation Rule"; diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessingMocks.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessingMocks.Codeunit.al index fbffe590c3..7c6524e113 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessingMocks.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocProcessingMocks.Codeunit.al @@ -8,7 +8,7 @@ using Microsoft.eServices.EDocument; using Microsoft.eServices.EDocument.Processing.Interfaces; using Microsoft.Purchases.Document; -codeunit 133503 "E-Doc. Processing Mocks" implements IEDocumentCreatePurchaseInvoice +codeunit 133503 "E-Doc. Processing Mocks" implements IEDocumentCreatePurchaseInvoice, IEDocumentCreatePurchaseCreditMemo { procedure CreatePurchaseInvoice(EDocument: Record "E-Document") PurchaseHeader: Record "Purchase Header" @@ -18,4 +18,11 @@ codeunit 133503 "E-Doc. Processing Mocks" implements IEDocumentCreatePurchaseInv PurchaseHeader.Insert(); end; + procedure CreatePurchaseCreditMemo(EDocument: Record "E-Document") PurchaseHeader: Record "Purchase Header" + begin + PurchaseHeader."No." := 'CM-' + Format(EDocument."Entry No"); + PurchaseHeader."Document Type" := "Purchase Document Type"::"Credit Memo"; + PurchaseHeader.Insert(); + end; + } diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocStructuredValidations.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocStructuredValidations.Codeunit.al new file mode 100644 index 0000000000..2fef0f05fb --- /dev/null +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocStructuredValidations.Codeunit.al @@ -0,0 +1,597 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.eServices.EDocument.Test; + +using Microsoft.eServices.EDocument.Processing.Import.Purchase; +using Microsoft.Finance.GeneralLedger.Setup; + +codeunit 139894 "EDoc Structured Validations" +{ + + var + Assert: Codeunit Assert; + + #region CAPI + internal procedure AssertFullCAPIDocumentExtracted(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + + Assert.AreEqual('MICROSOFT CORPORATION', EDocumentPurchaseHeader."Customer Company Name", 'The customer company name does not allign with the mock data.'); + Assert.AreEqual('CID-12345', EDocumentPurchaseHeader."Customer Company Id", 'The customer company id does not allign with the mock data.'); + Assert.AreEqual('PO-3333', EDocumentPurchaseHeader."Purchase Order No.", 'The purchase order number does not allign with the mock data.'); + Assert.AreEqual('INV-100', EDocumentPurchaseHeader."Sales Invoice No.", 'The sales invoice number does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(15, 12, 2019), EDocumentPurchaseHeader."Due Date", 'The due date does not allign with the mock data.'); + Assert.AreEqual('CONTOSO LTD.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not allign with the mock data.'); + Assert.AreEqual('123 456th St New York, NY, 10001', EDocumentPurchaseHeader."Vendor Address", 'The vendor address does not allign with the mock data.'); + Assert.AreEqual('Contoso Headquarters', EDocumentPurchaseHeader."Vendor Address Recipient", 'The vendor address recipient does not allign with the mock data.'); + Assert.AreEqual('123 Other St, Redmond WA, 98052', EDocumentPurchaseHeader."Customer Address", 'The customer address does not allign with the mock data.'); + Assert.AreEqual('Microsoft Corp', EDocumentPurchaseHeader."Customer Address Recipient", 'The customer address recipient does not allign with the mock data.'); + Assert.AreEqual('123 Bill St, Redmond WA, 98052', EDocumentPurchaseHeader."Billing Address", 'The billing address does not allign with the mock data.'); + Assert.AreEqual('Microsoft Finance', EDocumentPurchaseHeader."Billing Address Recipient", 'The billing address recipient does not allign with the mock data.'); + Assert.AreEqual('123 Ship St, Redmond WA, 98052', EDocumentPurchaseHeader."Shipping Address", 'The shipping address does not allign with the mock data.'); + Assert.AreEqual('Microsoft Delivery', EDocumentPurchaseHeader."Shipping Address Recipient", 'The shipping address recipient does not allign with the mock data.'); + Assert.AreEqual(100, EDocumentPurchaseHeader."Sub Total", 'The sub total does not allign with the mock data.'); + Assert.AreEqual(10, EDocumentPurchaseHeader."Total VAT", 'The total tax does not allign with the mock data.'); + Assert.AreEqual(110, EDocumentPurchaseHeader.Total, 'The total does not allign with the mock data.'); + Assert.AreEqual(610, EDocumentPurchaseHeader."Amount Due", 'The amount due does not allign with the mock data.'); + Assert.AreEqual(500, EDocumentPurchaseHeader."Previous Unpaid Balance", 'The previous unpaid balance does not allign with the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not allign with the mock data.'); + Assert.AreEqual('123 Remit St New York, NY, 10001', EDocumentPurchaseHeader."Remittance Address", 'The remittance address does not allign with the mock data.'); + Assert.AreEqual('Contoso Billing', EDocumentPurchaseHeader."Remittance Address Recipient", 'The remittance address recipient does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(14, 10, 2019), EDocumentPurchaseHeader."Service Start Date", 'The service start date does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(14, 11, 2019), EDocumentPurchaseHeader."Service End Date", 'The service end date does not allign with the mock data.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.FindSet(); + Assert.AreEqual(60, EDocumentPurchaseLine."Sub Total", 'The amount in the purchase line does not allign with the mock data.'); + Assert.AreEqual('Consulting Services', EDocumentPurchaseLine.Description, 'The description in the purchase line does not allign with the mock data.'); + Assert.AreEqual(30, EDocumentPurchaseLine."Unit Price", 'The unit price in the purchase line does not allign with the mock data.'); + Assert.AreEqual(2, EDocumentPurchaseLine.Quantity, 'The quantity in the purchase line does not allign with the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the purchase line does not allign with the mock data.'); + Assert.AreEqual('A123', EDocumentPurchaseLine."Product Code", 'The product code in the purchase line does not allign with the mock data.'); + Assert.AreEqual('hours', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the purchase line does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(4, 3, 2021), EDocumentPurchaseLine.Date, 'The date in the purchase line does not allign with the mock data.'); + Assert.AreEqual(6, EDocumentPurchaseLine."VAT Rate", 'The amount in the purchase line does not allign with the mock data.'); + + EDocumentPurchaseLine.Next(); + Assert.AreEqual(30, EDocumentPurchaseLine."Sub Total", 'The amount in the purchase line does not allign with the mock data.'); + Assert.AreEqual('Document Fee', EDocumentPurchaseLine.Description, 'The description in the purchase line does not allign with the mock data.'); + Assert.AreEqual(10, EDocumentPurchaseLine."Unit Price", 'The unit price in the purchase line does not allign with the mock data.'); + Assert.AreEqual(3, EDocumentPurchaseLine.Quantity, 'The quantity in the purchase line does not allign with the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the purchase line does not allign with the mock data.'); + Assert.AreEqual('B456', EDocumentPurchaseLine."Product Code", 'The product code in the purchase line does not allign with the mock data.'); + Assert.AreEqual('', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the purchase line does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(5, 3, 2021), EDocumentPurchaseLine.Date, 'The date in the purchase line does not allign with the mock data.'); + Assert.AreEqual(3, EDocumentPurchaseLine."VAT Rate", 'The amount in the purchase line does not allign with the mock data.'); + + EDocumentPurchaseLine.Next(); + Assert.AreEqual(10, EDocumentPurchaseLine."Sub Total", 'The amount does not allign with the mock data.'); + Assert.AreEqual('Printing Fee', EDocumentPurchaseLine.Description, 'The description does not allign with the mock data.'); + Assert.AreEqual(1, EDocumentPurchaseLine."Unit Price", 'The unit price does not allign with the mock data.'); + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'The quantity does not allign with the mock data.'); + Assert.AreEqual('C789', EDocumentPurchaseLine."Product Code", 'The product code does not allign with the mock data.'); + Assert.AreEqual('pages', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(6, 3, 2021), EDocumentPurchaseLine.Date, 'The date does not allign with the mock data.'); + Assert.AreEqual(1, EDocumentPurchaseLine."VAT Rate", 'The amount does not allign with the mock data.'); + end; + + internal procedure AssertMinimalCAPIDocumentParsed(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + + Assert.AreEqual('INV-100', EDocumentPurchaseHeader."Sales Invoice No.", 'The sales invoice number does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(15, 12, 2019), EDocumentPurchaseHeader."Due Date", 'The due date does not allign with the mock data.'); + Assert.AreEqual('CONTOSO LTD.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not allign with the mock data.'); + Assert.AreEqual(110, EDocumentPurchaseHeader.Total, 'The total does not allign with the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not allign with the mock data.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.FindSet(); + Assert.AreEqual(60, EDocumentPurchaseLine."Sub Total", 'The amount in the purchase line does not allign with the mock data.'); + Assert.AreEqual('Consulting Services', EDocumentPurchaseLine.Description, 'The description in the purchase line does not allign with the mock data.'); + + EDocumentPurchaseLine.Next(); + Assert.AreEqual(30, EDocumentPurchaseLine."Sub Total", 'The amount in the purchase line does not allign with the mock data.'); + Assert.AreEqual('Document Fee', EDocumentPurchaseLine.Description, 'The description in the purchase line does not allign with the mock data.'); + + EDocumentPurchaseLine.Next(); + Assert.AreEqual(10, EDocumentPurchaseLine."Sub Total", 'The amount does not allign with the mock data.'); + Assert.AreEqual('Printing Fee', EDocumentPurchaseLine.Description, 'The description does not allign with the mock data.'); + end; + #endregion + + #region PEPPOL + internal procedure AssertFullPEPPOLDocumentExtracted(EDocumentEntryNo: Integer) + var + GLSetup: Record "General Ledger Setup"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + GLSetup.Get(); + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('103033', EDocumentPurchaseHeader."Sales Invoice No.", 'The sales invoice number does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(22, 01, 2026), EDocumentPurchaseHeader."Document Date", 'The invoice date does not allign with the mock data.'); + Assert.AreEqual(DMY2Date(22, 02, 2026), EDocumentPurchaseHeader."Due Date", 'The due date does not allign with the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not allign with the mock data.'); + Assert.AreEqual('2', EDocumentPurchaseHeader."Purchase Order No.", 'The purchase order number does not allign with the mock data.'); + Assert.AreEqual('CRONUS International', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not allign with the mock data.'); + Assert.AreEqual('Main Street, 14', EDocumentPurchaseHeader."Vendor Address", 'The vendor street does not allign with the mock data.'); + Assert.AreEqual('GB123456789', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not allign with the mock data.'); + Assert.AreEqual('Jim Olive', EDocumentPurchaseHeader."Vendor Contact Name", 'The vendor contact name does not allign with the mock data.'); + Assert.AreEqual('The Cannon Group PLC', EDocumentPurchaseHeader."Customer Company Name", 'The customer name does not allign with the mock data.'); + Assert.AreEqual('GB789456278', EDocumentPurchaseHeader."Customer VAT Id", 'The customer VAT id does not allign with the mock data.'); + Assert.AreEqual('192 Market Square', EDocumentPurchaseHeader."Customer Address", 'The customer address does not allign with the mock data.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.FindSet(); + Assert.AreEqual(1, EDocumentPurchaseLine."Quantity", 'The quantity in the purchase line does not allign with the mock data.'); + Assert.AreEqual('PCS', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the purchase line does not allign with the mock data.'); + Assert.AreEqual(4000, EDocumentPurchaseLine."Sub Total", 'The total amount before taxes in the purchase line does not allign with the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the purchase line does not allign with the mock data.'); + Assert.AreEqual(0, EDocumentPurchaseLine."Total Discount", 'The total discount in the purchase line does not allign with the mock data.'); + Assert.AreEqual('Bicycle', EDocumentPurchaseLine.Description, 'The product description in the purchase line does not allign with the mock data.'); + Assert.AreEqual('1000', EDocumentPurchaseLine."Product Code", 'The product code in the purchase line does not allign with the mock data.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'The VAT rate in the purchase line does not allign with the mock data.'); + Assert.AreEqual(4000, EDocumentPurchaseLine."Unit Price", 'The unit price in the purchase line does not allign with the mock data.'); + + EDocumentPurchaseLine.Next(); + Assert.AreEqual(2, EDocumentPurchaseLine."Quantity", 'The quantity in the purchase line does not allign with the mock data.'); + Assert.AreEqual('PCS', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the purchase line does not allign with the mock data.'); + Assert.AreEqual(10000, EDocumentPurchaseLine."Sub Total", 'The total amount before taxes in the purchase line does not allign with the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the purchase line does not allign with the mock data.'); + Assert.AreEqual(0, EDocumentPurchaseLine."Total Discount", 'The total discount in the purchase line does not allign with the mock data.'); + Assert.AreEqual('Bicycle v2', EDocumentPurchaseLine.Description, 'The product description in the purchase line does not allign with the mock data.'); + Assert.AreEqual('2000', EDocumentPurchaseLine."Product Code", 'The product code in the purchase line does not allign with the mock data.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'The VAT rate in the purchase line does not allign with the mock data.'); + Assert.AreEqual(5000, EDocumentPurchaseLine."Unit Price", 'The unit price in the purchase line does not allign with the mock data.'); + + end; + internal procedure AssertFullPEPPOLCreditNoteExtracted(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('CN-5001', EDocumentPurchaseHeader."Sales Invoice No.", 'The credit note number does not match the mock data.'); + Assert.AreEqual(DMY2Date(15, 02, 2026), EDocumentPurchaseHeader."Document Date", 'The document date does not match the mock data.'); + Assert.AreEqual(DMY2Date(15, 03, 2026), EDocumentPurchaseHeader."Due Date", 'The due date does not match the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not match the mock data.'); + Assert.AreEqual('5', EDocumentPurchaseHeader."Purchase Order No.", 'The order reference does not match the mock data.'); + Assert.AreEqual('103033', EDocumentPurchaseHeader."Vendor Invoice No.", 'The billing reference (vendor invoice no.) does not match the mock data.'); + Assert.AreEqual('CRONUS International', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match the mock data.'); + Assert.AreEqual('Main Street, 14', EDocumentPurchaseHeader."Vendor Address", 'The vendor street does not match the mock data.'); + Assert.AreEqual('GB123456789', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not match the mock data.'); + Assert.AreEqual('Jim Olive', EDocumentPurchaseHeader."Vendor Contact Name", 'The vendor contact name does not match the mock data.'); + Assert.AreEqual('The Cannon Group PLC', EDocumentPurchaseHeader."Customer Company Name", 'The customer name does not match the mock data.'); + Assert.AreEqual('GB789456278', EDocumentPurchaseHeader."Customer VAT Id", 'The customer VAT id does not match the mock data.'); + Assert.AreEqual('192 Market Square', EDocumentPurchaseHeader."Customer Address", 'The customer address does not match the mock data.'); + Assert.AreEqual(2500, EDocumentPurchaseHeader.Total, 'The total does not match the mock data.'); + Assert.AreEqual(2000, EDocumentPurchaseHeader."Sub Total", 'The sub total does not match the mock data.'); + Assert.AreEqual(0, EDocumentPurchaseHeader."Total Discount", 'The total discount does not match the mock data.'); + Assert.AreEqual(500, EDocumentPurchaseHeader."Total VAT", 'The total VAT does not match the mock data.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.FindFirst(); + Assert.AreEqual(1, EDocumentPurchaseLine."Quantity", 'The quantity in the credit note line does not match the mock data.'); + Assert.AreEqual('PCS', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the credit note line does not match the mock data.'); + Assert.AreEqual(2000, EDocumentPurchaseLine."Sub Total", 'The line extension amount does not match the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the credit note line does not match the mock data.'); + Assert.AreEqual('Bicycle - Return', EDocumentPurchaseLine.Description, 'The description in the credit note line does not match the mock data.'); + Assert.AreEqual('1000', EDocumentPurchaseLine."Product Code", 'The product code in the credit note line does not match the mock data.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'The VAT rate in the credit note line does not match the mock data.'); + Assert.AreEqual(2000, EDocumentPurchaseLine."Unit Price", 'The unit price in the credit note line does not match the mock data.'); + end; + + internal procedure AssertPEPPOLBaseExampleExtracted(EDocumentEntryNo: Integer) + var + GLSetup: Record "General Ledger Setup"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + GLSetup.Get(); + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('Snippet1', EDocumentPurchaseHeader."Sales Invoice No.", 'The invoice ID does not match.'); + Assert.AreEqual(DMY2Date(13, 11, 2017), EDocumentPurchaseHeader."Document Date", 'The document date does not match.'); + Assert.AreEqual(DMY2Date(01, 12, 2017), EDocumentPurchaseHeader."Due Date", 'The due date does not match.'); + Assert.AreEqual(ExpectedCurrencyCode('EUR', GLSetup."LCY Code"), EDocumentPurchaseHeader."Currency Code", 'The currency code does not match.'); + Assert.AreEqual('SupplierTradingName Ltd.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match.'); + Assert.AreEqual('Main street 1', EDocumentPurchaseHeader."Vendor Address", 'The vendor address does not match.'); + Assert.AreEqual('GB1232434', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not match.'); + Assert.AreEqual('9482348239847', EDocumentPurchaseHeader."Vendor GLN", 'The vendor GLN should be populated for schemeID=0088.'); + Assert.AreEqual('BuyerTradingName AS', EDocumentPurchaseHeader."Customer Company Name", 'The customer name does not match.'); + Assert.AreEqual('SE4598375937', EDocumentPurchaseHeader."Customer VAT Id", 'The customer VAT id does not match.'); + Assert.AreEqual('Hovedgatan 32', EDocumentPurchaseHeader."Customer Address", 'The customer address does not match.'); + Assert.AreEqual('', EDocumentPurchaseHeader."Customer GLN", 'Customer GLN should be empty for schemeID=0002.'); + Assert.AreEqual('0002:FR23342', EDocumentPurchaseHeader."Customer Company Id", 'Customer Company Id should be schemeID:value.'); + Assert.AreEqual(1656.25, EDocumentPurchaseHeader.Total, 'The total does not match.'); + Assert.AreEqual(1325, EDocumentPurchaseHeader."Sub Total", 'The sub total does not match.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + Assert.AreEqual(3, EDocumentPurchaseLine.Count(), 'Expected 2 invoice lines + 1 charge line = 3 lines.'); + + EDocumentPurchaseLine.FindSet(); + // Line 1: 7 x 400 EUR + Assert.AreEqual(7, EDocumentPurchaseLine.Quantity, 'Line 1 quantity does not match.'); + Assert.AreEqual('DAY', EDocumentPurchaseLine."Unit of Measure", 'Line 1 unit of measure does not match.'); + Assert.AreEqual(2800, EDocumentPurchaseLine."Sub Total", 'Line 1 sub total does not match.'); + Assert.AreEqual('item name', EDocumentPurchaseLine.Description, 'Line 1 description does not match.'); + Assert.AreEqual('21382183120983', EDocumentPurchaseLine."Product Code", 'Line 1 product code should be StandardItemIdentification.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Line 1 VAT rate does not match.'); + Assert.AreEqual(400, EDocumentPurchaseLine."Unit Price", 'Line 1 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Line 2: -3 x 500 EUR (negative quantity) + Assert.AreEqual(-3, EDocumentPurchaseLine.Quantity, 'Line 2 quantity does not match (should be negative).'); + Assert.AreEqual(-1500, EDocumentPurchaseLine."Sub Total", 'Line 2 sub total does not match.'); + Assert.AreEqual('item name 2', EDocumentPurchaseLine.Description, 'Line 2 description does not match.'); + Assert.AreEqual(500, EDocumentPurchaseLine."Unit Price", 'Line 2 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Charge line: Insurance, 25 EUR, VAT 25% + Assert.AreEqual(1, EDocumentPurchaseLine.Quantity, 'Charge line quantity should be 1.'); + Assert.AreEqual(25, EDocumentPurchaseLine."Unit Price", 'Charge line unit price does not match.'); + Assert.AreEqual(25, EDocumentPurchaseLine."Sub Total", 'Charge line sub total does not match.'); + Assert.AreEqual('Insurance', EDocumentPurchaseLine.Description, 'Charge line description does not match.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Charge line VAT rate does not match.'); + end; + + internal procedure AssertPEPPOLInvoiceWithChargesExtracted(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('INV-CHARGE-001', EDocumentPurchaseHeader."Sales Invoice No.", 'The invoice ID does not match.'); + Assert.AreEqual(DMY2Date(01, 03, 2026), EDocumentPurchaseHeader."Document Date", 'The document date does not match.'); + Assert.AreEqual(DMY2Date(01, 04, 2026), EDocumentPurchaseHeader."Due Date", 'The due date does not match.'); + Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not match.'); + Assert.AreEqual('PO-100', EDocumentPurchaseHeader."Purchase Order No.", 'The purchase order number does not match.'); + Assert.AreEqual('CRONUS International', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match.'); + Assert.AreEqual(1200, EDocumentPurchaseHeader.Total, 'The total does not match.'); + Assert.AreEqual(950, EDocumentPurchaseHeader."Sub Total", 'The sub total does not match.'); + Assert.AreEqual(200, EDocumentPurchaseHeader."Total Discount", 'The total discount (allowance) does not match.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + Assert.AreEqual(2, EDocumentPurchaseLine.Count(), 'Expected 1 invoice line + 1 charge line (allowance should NOT create a line).'); + + EDocumentPurchaseLine.FindSet(); + // Invoice line: Widget, 2 x 500 XYZ + Assert.AreEqual(2, EDocumentPurchaseLine.Quantity, 'Invoice line quantity does not match.'); + Assert.AreEqual('PCS', EDocumentPurchaseLine."Unit of Measure", 'Invoice line unit of measure does not match.'); + Assert.AreEqual(1000, EDocumentPurchaseLine."Sub Total", 'Invoice line sub total does not match.'); + Assert.AreEqual('Widget', EDocumentPurchaseLine.Description, 'Invoice line description does not match.'); + // StandardItemIdentification (7350053850019) should override SellersItemIdentification (WIDGET-001) + Assert.AreEqual('7350053850019', EDocumentPurchaseLine."Product Code", 'Product code should be StandardItemIdentification, not SellersItemIdentification.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Invoice line VAT rate does not match.'); + Assert.AreEqual(500, EDocumentPurchaseLine."Unit Price", 'Invoice line unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Charge line: Freight charge, 150 XYZ, VAT 25% + Assert.AreEqual(1, EDocumentPurchaseLine.Quantity, 'Charge line quantity should be 1.'); + Assert.AreEqual(150, EDocumentPurchaseLine."Unit Price", 'Charge line unit price does not match.'); + Assert.AreEqual(150, EDocumentPurchaseLine."Sub Total", 'Charge line sub total does not match.'); + Assert.AreEqual('Freight charge', EDocumentPurchaseLine.Description, 'Charge line description does not match.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Charge line VAT rate does not match.'); + Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'Charge line currency code does not match.'); + end; + + internal procedure AssertPEPPOLVatCategorySExtracted(EDocumentEntryNo: Integer) + var + GLSetup: Record "General Ledger Setup"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + GLSetup.Get(); + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('Snippet1', EDocumentPurchaseHeader."Sales Invoice No.", 'The invoice ID does not match.'); + Assert.AreEqual(DMY2Date(13, 11, 2017), EDocumentPurchaseHeader."Document Date", 'The document date does not match.'); + Assert.AreEqual(DMY2Date(01, 12, 2017), EDocumentPurchaseHeader."Due Date", 'The due date does not match.'); + Assert.AreEqual(ExpectedCurrencyCode('EUR', GLSetup."LCY Code"), EDocumentPurchaseHeader."Currency Code", 'The currency code does not match.'); + Assert.AreEqual('SupplierTradingName Ltd.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match.'); + Assert.AreEqual('John Doe', EDocumentPurchaseHeader."Vendor Contact Name", 'The vendor contact name does not match.'); + Assert.AreEqual('7300010000001', EDocumentPurchaseHeader."Vendor GLN", 'The vendor GLN does not match for schemeID=0088.'); + Assert.AreEqual(8550, EDocumentPurchaseHeader.Total, 'The total does not match.'); + Assert.AreEqual(7000, EDocumentPurchaseHeader."Sub Total", 'The sub total does not match.'); + Assert.AreEqual(100, EDocumentPurchaseHeader."Total Discount", 'The total discount does not match.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + Assert.AreEqual(4, EDocumentPurchaseLine.Count(), 'Expected 3 invoice lines + 1 charge line.'); + + EDocumentPurchaseLine.FindSet(); + // Line 1: 10 x 400, VAT 25%, StandardItemIdentification overrides SellersItemIdentification + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'Line 1 quantity does not match.'); + Assert.AreEqual(4000, EDocumentPurchaseLine."Sub Total", 'Line 1 sub total does not match.'); + Assert.AreEqual('item name', EDocumentPurchaseLine.Description, 'Line 1 description does not match.'); + Assert.AreEqual('7300010000001', EDocumentPurchaseLine."Product Code", 'Line 1 product code should be StandardItemIdentification.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Line 1 VAT rate does not match.'); + Assert.AreEqual(400, EDocumentPurchaseLine."Unit Price", 'Line 1 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Line 2: 10 x 200, VAT 15% (different rate) + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'Line 2 quantity does not match.'); + Assert.AreEqual(2000, EDocumentPurchaseLine."Sub Total", 'Line 2 sub total does not match.'); + Assert.AreEqual(15, EDocumentPurchaseLine."VAT Rate", 'Line 2 VAT rate should be 15% (different from line 1).'); + Assert.AreEqual(200, EDocumentPurchaseLine."Unit Price", 'Line 2 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Line 3: 10 x 90, VAT 25%, StandardItemIdentification with different schemeID (0160) + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'Line 3 quantity does not match.'); + Assert.AreEqual(900, EDocumentPurchaseLine."Sub Total", 'Line 3 sub total does not match.'); + Assert.AreEqual('873649827489', EDocumentPurchaseLine."Product Code", 'Line 3 product code should be StandardItemIdentification with schemeID=0160.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Line 3 VAT rate does not match.'); + Assert.AreEqual(90, EDocumentPurchaseLine."Unit Price", 'Line 3 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Charge line: Cleaning, 200 EUR, VAT 25% + Assert.AreEqual(1, EDocumentPurchaseLine.Quantity, 'Charge line quantity should be 1.'); + Assert.AreEqual(200, EDocumentPurchaseLine."Unit Price", 'Charge line unit price does not match.'); + Assert.AreEqual('Cleaning', EDocumentPurchaseLine.Description, 'Charge line description does not match.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Charge line VAT rate does not match.'); + end; + + internal procedure AssertPEPPOLVatCategoryZExtracted(EDocumentEntryNo: Integer) + var + GLSetup: Record "General Ledger Setup"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + GLSetup.Get(); + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('Vat-Z', EDocumentPurchaseHeader."Sales Invoice No.", 'The invoice ID does not match.'); + Assert.AreEqual(DMY2Date(30, 08, 2018), EDocumentPurchaseHeader."Document Date", 'The document date does not match.'); + Assert.AreEqual(0D, EDocumentPurchaseHeader."Due Date", 'Due Date should be blank when not present in the XML.'); + Assert.AreEqual(ExpectedCurrencyCode('GBP', GLSetup."LCY Code"), EDocumentPurchaseHeader."Currency Code", 'The currency code does not match.'); + Assert.AreEqual('The Sellercompany Incorporated', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match.'); + Assert.AreEqual('GB928741974', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not match.'); + Assert.AreEqual('7300010000001', EDocumentPurchaseHeader."Vendor GLN", 'The vendor GLN does not match for schemeID=0088.'); + Assert.AreEqual('The Buyercompany', EDocumentPurchaseHeader."Customer Company Name", 'The customer name does not match.'); + Assert.AreEqual('', EDocumentPurchaseHeader."Customer GLN", 'Customer GLN should be empty for schemeID=0184.'); + Assert.AreEqual('0184:DK12345678', EDocumentPurchaseHeader."Customer Company Id", 'Customer Company Id should be schemeID:value.'); + Assert.AreEqual(1200, EDocumentPurchaseHeader.Total, 'The total does not match.'); + Assert.AreEqual(1200, EDocumentPurchaseHeader."Sub Total", 'The sub total does not match.'); + Assert.AreEqual(0, EDocumentPurchaseHeader."Total VAT", 'The total VAT should be 0 for zero-rated goods.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + Assert.AreEqual(1, EDocumentPurchaseLine.Count(), 'Expected 1 invoice line.'); + + EDocumentPurchaseLine.FindFirst(); + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'Line quantity does not match.'); + Assert.AreEqual('EA', EDocumentPurchaseLine."Unit of Measure", 'Line unit of measure does not match.'); + Assert.AreEqual(1200, EDocumentPurchaseLine."Sub Total", 'Line sub total does not match.'); + Assert.AreEqual('Test item, category Z', EDocumentPurchaseLine.Description, 'Line description does not match.'); + Assert.AreEqual('192387129837129873', EDocumentPurchaseLine."Product Code", 'Line product code does not match.'); + Assert.AreEqual(0, EDocumentPurchaseLine."VAT Rate", 'Line VAT rate should be 0 for category Z.'); + Assert.AreEqual(120, EDocumentPurchaseLine."Unit Price", 'Line unit price does not match.'); + end; + + internal procedure AssertPEPPOLAllowanceExampleExtracted(EDocumentEntryNo: Integer) + var + GLSetup: Record "General Ledger Setup"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + GLSetup.Get(); + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('Snippet1', EDocumentPurchaseHeader."Sales Invoice No.", 'The invoice ID does not match.'); + Assert.AreEqual(DMY2Date(13, 11, 2017), EDocumentPurchaseHeader."Document Date", 'The document date does not match.'); + Assert.AreEqual(DMY2Date(01, 12, 2017), EDocumentPurchaseHeader."Due Date", 'The due date does not match.'); + Assert.AreEqual(ExpectedCurrencyCode('EUR', GLSetup."LCY Code"), EDocumentPurchaseHeader."Currency Code", 'The currency code does not match.'); + Assert.AreEqual('SupplierTradingName Ltd.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match.'); + Assert.AreEqual('7300010000001', EDocumentPurchaseHeader."Vendor GLN", 'The vendor GLN does not match for schemeID=0088.'); + Assert.AreEqual(6125, EDocumentPurchaseHeader.Total, 'The total (PayableAmount) does not match.'); + Assert.AreEqual(5900, EDocumentPurchaseHeader."Sub Total", 'The sub total (TaxExclusiveAmount) does not match.'); + Assert.AreEqual(200, EDocumentPurchaseHeader."Total Discount", 'The total discount (AllowanceTotalAmount) does not match.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + // 3 invoice lines + 1 charge line (Cleaning 200 EUR); the allowance (Discount 200 EUR) should NOT create a line + Assert.AreEqual(4, EDocumentPurchaseLine.Count(), 'Expected 3 invoice lines + 1 charge line (allowance should NOT create a line).'); + + EDocumentPurchaseLine.FindSet(); + // Line 1: 10 x 410, only SellersItemIdentification (no StandardItemIdentification) + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'Line 1 quantity does not match.'); + Assert.AreEqual(4000, EDocumentPurchaseLine."Sub Total", 'Line 1 sub total does not match.'); + Assert.AreEqual('item name', EDocumentPurchaseLine.Description, 'Line 1 description does not match (Name takes priority).'); + Assert.AreEqual('97iugug876', EDocumentPurchaseLine."Product Code", 'Line 1 product code should be SellersItemIdentification when no StandardItemIdentification exists.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Line 1 VAT rate does not match.'); + Assert.AreEqual(410, EDocumentPurchaseLine."Unit Price", 'Line 1 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Line 2: 10 x 200, VAT E (0%), SellersItemIdentification only + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'Line 2 quantity does not match.'); + Assert.AreEqual(1000, EDocumentPurchaseLine."Sub Total", 'Line 2 sub total does not match.'); + Assert.AreEqual(0, EDocumentPurchaseLine."VAT Rate", 'Line 2 VAT rate should be 0% for category E.'); + Assert.AreEqual(200, EDocumentPurchaseLine."Unit Price", 'Line 2 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Line 3: 10 x 100, VAT 25%, SellersItemIdentification only + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'Line 3 quantity does not match.'); + Assert.AreEqual(900, EDocumentPurchaseLine."Sub Total", 'Line 3 sub total does not match.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Line 3 VAT rate does not match.'); + Assert.AreEqual(100, EDocumentPurchaseLine."Unit Price", 'Line 3 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Charge line: Cleaning, 200 EUR, VAT 25% + Assert.AreEqual(1, EDocumentPurchaseLine.Quantity, 'Charge line quantity should be 1.'); + Assert.AreEqual(200, EDocumentPurchaseLine."Unit Price", 'Charge line unit price does not match.'); + Assert.AreEqual(200, EDocumentPurchaseLine."Sub Total", 'Charge line sub total does not match.'); + Assert.AreEqual('Cleaning', EDocumentPurchaseLine.Description, 'Charge line description does not match.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Charge line VAT rate does not match.'); + end; + + internal procedure AssertPEPPOLCreditNoteCorrectionExtracted(EDocumentEntryNo: Integer) + var + GLSetup: Record "General Ledger Setup"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + GLSetup.Get(); + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('Snippet1', EDocumentPurchaseHeader."Sales Invoice No.", 'The credit note ID does not match.'); + Assert.AreEqual(DMY2Date(13, 11, 2017), EDocumentPurchaseHeader."Document Date", 'The document date does not match.'); + // CreditNote without PaymentMeans/PaymentDueDate: DueDate should be blank + Assert.AreEqual(0D, EDocumentPurchaseHeader."Due Date", 'Due Date should be blank when CreditNote has no PaymentMeans/PaymentDueDate.'); + Assert.AreEqual(ExpectedCurrencyCode('EUR', GLSetup."LCY Code"), EDocumentPurchaseHeader."Currency Code", 'The currency code does not match.'); + Assert.AreEqual('Snippet1', EDocumentPurchaseHeader."Vendor Invoice No.", 'The BillingReference (Vendor Invoice No.) does not match.'); + Assert.AreEqual('SupplierTradingName Ltd.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match.'); + Assert.AreEqual('GB1232434', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not match.'); + Assert.AreEqual(1656.25, EDocumentPurchaseHeader.Total, 'The total does not match.'); + Assert.AreEqual(1325, EDocumentPurchaseHeader."Sub Total", 'The sub total does not match.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + Assert.AreEqual(3, EDocumentPurchaseLine.Count(), 'Expected 2 credit note lines + 1 charge line.'); + + EDocumentPurchaseLine.FindSet(); + // CreditNoteLine 1: 7 x 400 + Assert.AreEqual(7, EDocumentPurchaseLine.Quantity, 'Line 1 quantity does not match.'); + Assert.AreEqual('DAY', EDocumentPurchaseLine."Unit of Measure", 'Line 1 unit of measure does not match.'); + Assert.AreEqual(2800, EDocumentPurchaseLine."Sub Total", 'Line 1 sub total does not match.'); + Assert.AreEqual('item name', EDocumentPurchaseLine.Description, 'Line 1 description does not match.'); + Assert.AreEqual(400, EDocumentPurchaseLine."Unit Price", 'Line 1 unit price does not match.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Line 1 VAT rate does not match.'); + + EDocumentPurchaseLine.Next(); + // CreditNoteLine 2: -3 x 500 (negative quantity) + Assert.AreEqual(-3, EDocumentPurchaseLine.Quantity, 'Line 2 quantity does not match (should be negative).'); + Assert.AreEqual(-1500, EDocumentPurchaseLine."Sub Total", 'Line 2 sub total does not match.'); + Assert.AreEqual('item name 2', EDocumentPurchaseLine.Description, 'Line 2 description does not match.'); + Assert.AreEqual(500, EDocumentPurchaseLine."Unit Price", 'Line 2 unit price does not match.'); + + EDocumentPurchaseLine.Next(); + // Charge line: Insurance, 25 EUR, VAT 25% + Assert.AreEqual(1, EDocumentPurchaseLine.Quantity, 'Charge line quantity should be 1.'); + Assert.AreEqual(25, EDocumentPurchaseLine."Unit Price", 'Charge line unit price does not match.'); + Assert.AreEqual('Insurance', EDocumentPurchaseLine.Description, 'Charge line description does not match.'); + Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'Charge line VAT rate does not match.'); + end; + + internal procedure AssertPEPPOLAttachmentHeaderExtracted(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('INV-ATT-001', EDocumentPurchaseHeader."Sales Invoice No.", 'The invoice ID does not match.'); + Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not match.'); + Assert.AreEqual('Attachment Supplier Ltd.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match.'); + Assert.AreEqual(625, EDocumentPurchaseHeader.Total, 'The total does not match.'); + end; + + internal procedure AssertPEPPOLDescriptionFallbackExtracted(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('INV-DESC-001', EDocumentPurchaseHeader."Sales Invoice No.", 'The invoice ID does not match.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + Assert.AreEqual(3, EDocumentPurchaseLine.Count(), 'Expected 3 invoice lines.'); + + EDocumentPurchaseLine.FindSet(); + // Line 1: Name only - should use Name + Assert.AreEqual('Widget Alpha', EDocumentPurchaseLine.Description, 'Line 1: Name should be used as description.'); + + EDocumentPurchaseLine.Next(); + // Line 2: Description only, no Name - should fall back to Description + Assert.AreEqual('Detailed description of Widget Beta for testing fallback', EDocumentPurchaseLine.Description, 'Line 2: Description should be used as fallback when Name is absent.'); + + EDocumentPurchaseLine.Next(); + // Line 3: Both Name and Description - Name takes priority + Assert.AreEqual('Widget Gamma', EDocumentPurchaseLine.Description, 'Line 3: Name should take priority over Description.'); + end; + + internal procedure AssertPEPPOLPayeePartyOverrideExtracted(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + Assert.AreEqual('INV-PAYEE-001', EDocumentPurchaseHeader."Sales Invoice No.", 'The invoice ID does not match.'); + // PayeeParty overrides AccountingSupplierParty + Assert.AreEqual('Factoring Company GmbH', EDocumentPurchaseHeader."Vendor Company Name", 'Vendor name should be overridden by PayeeParty/PartyName.'); + Assert.AreEqual('DE999888777', EDocumentPurchaseHeader."Vendor VAT Id", 'Vendor VAT Id should be overridden by PayeeParty/PartyLegalEntity/CompanyID.'); + // Address comes from AccountingSupplierParty (PayeeParty has no address) + Assert.AreEqual('Supplier Street 1', EDocumentPurchaseHeader."Vendor Address", 'Vendor address should still come from AccountingSupplierParty.'); + // GLN comes from AccountingSupplierParty endpoint + Assert.AreEqual('1234567890128', EDocumentPurchaseHeader."Vendor GLN", 'Vendor GLN should still come from AccountingSupplierParty endpoint.'); + Assert.AreEqual(250, EDocumentPurchaseHeader.Total, 'The total does not match.'); + end; + #endregion + + #region MLLM + internal procedure AssertFullMLLMDocumentExtracted(EDocumentEntryNo: Integer) + var + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + EDocumentPurchaseLine: Record "E-Document Purchase Line"; + begin + EDocumentPurchaseHeader.Get(EDocumentEntryNo); + + Assert.AreEqual('MLLM-INV-001', EDocumentPurchaseHeader."Sales Invoice No.", 'The sales invoice number does not match the mock data.'); + Assert.AreEqual(DMY2Date(15, 3, 2024), EDocumentPurchaseHeader."Document Date", 'The document date does not match the mock data.'); + Assert.AreEqual(DMY2Date(15, 4, 2024), EDocumentPurchaseHeader."Due Date", 'The due date does not match the mock data.'); + Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not match the mock data.'); + Assert.AreEqual('PO-5678', EDocumentPurchaseHeader."Purchase Order No.", 'The purchase order number does not match the mock data.'); + Assert.AreEqual('Net 30', EDocumentPurchaseHeader."Payment Terms", 'The payment terms do not match the mock data.'); + Assert.AreEqual('Contoso Supplies Ltd.', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not match the mock data.'); + Assert.AreEqual('123 Bill Ave, Seattle 98101, US', EDocumentPurchaseHeader."Vendor Address", 'The vendor address does not match the mock data.'); + Assert.AreEqual('US-VAT-12345', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not match the mock data.'); + Assert.AreEqual('John Doe', EDocumentPurchaseHeader."Vendor Contact Name", 'The vendor contact name does not match the mock data.'); + Assert.AreEqual('Microsoft Corporation', EDocumentPurchaseHeader."Customer Company Name", 'The customer name does not match the mock data.'); + Assert.AreEqual('456 Main St, Redmond 98052, US', EDocumentPurchaseHeader."Customer Address", 'The customer address does not match the mock data.'); + Assert.AreEqual('US-VAT-67890', EDocumentPurchaseHeader."Customer VAT Id", 'The customer VAT id does not match the mock data.'); + Assert.AreEqual('789 Ship Rd, Bellevue 98004, US', EDocumentPurchaseHeader."Shipping Address", 'The shipping address does not match the mock data.'); + Assert.AreEqual('Warehouse Team', EDocumentPurchaseHeader."Shipping Address Recipient", 'The shipping address recipient does not match the mock data.'); + Assert.AreEqual('Contoso Billing Dept', EDocumentPurchaseHeader."Remittance Address Recipient", 'The remittance address recipient does not match the mock data.'); + Assert.AreEqual(37.5, EDocumentPurchaseHeader."Total VAT", 'The total VAT does not match the mock data.'); + Assert.AreEqual(250, EDocumentPurchaseHeader."Sub Total", 'The sub total does not match the mock data.'); + Assert.AreEqual(5, EDocumentPurchaseHeader."Total Discount", 'The total discount does not match the mock data.'); + Assert.AreEqual(287.5, EDocumentPurchaseHeader.Total, 'The total does not match the mock data.'); + Assert.AreEqual(287.5, EDocumentPurchaseHeader."Amount Due", 'The amount due does not match the mock data.'); + + EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); + EDocumentPurchaseLine.FindSet(); + Assert.AreEqual('Consulting Services', EDocumentPurchaseLine.Description, 'The description in line 1 does not match the mock data.'); + Assert.AreEqual('SVC-001', EDocumentPurchaseLine."Product Code", 'The product code in line 1 does not match the mock data.'); + Assert.AreEqual(5, EDocumentPurchaseLine.Quantity, 'The quantity in line 1 does not match the mock data.'); + Assert.AreEqual('HRS', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in line 1 does not match the mock data.'); + Assert.AreEqual(40, EDocumentPurchaseLine."Unit Price", 'The unit price in line 1 does not match the mock data.'); + Assert.AreEqual(200, EDocumentPurchaseLine."Sub Total", 'The sub total in line 1 does not match the mock data.'); + Assert.AreEqual(15, EDocumentPurchaseLine."VAT Rate", 'The VAT rate in line 1 does not match the mock data.'); + Assert.AreEqual(5, EDocumentPurchaseLine."Total Discount", 'The total discount in line 1 does not match the mock data.'); + + EDocumentPurchaseLine.Next(); + Assert.AreEqual('Office Supplies', EDocumentPurchaseLine.Description, 'The description in line 2 does not match the mock data.'); + Assert.AreEqual('MAT-002', EDocumentPurchaseLine."Product Code", 'The product code in line 2 does not match the mock data.'); + Assert.AreEqual(10, EDocumentPurchaseLine.Quantity, 'The quantity in line 2 does not match the mock data.'); + Assert.AreEqual('PCS', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in line 2 does not match the mock data.'); + Assert.AreEqual(3, EDocumentPurchaseLine."Unit Price", 'The unit price in line 2 does not match the mock data.'); + Assert.AreEqual(30, EDocumentPurchaseLine."Sub Total", 'The sub total in line 2 does not match the mock data.'); + Assert.AreEqual(10, EDocumentPurchaseLine."VAT Rate", 'The VAT rate in line 2 does not match the mock data.'); + Assert.AreEqual(0, EDocumentPurchaseLine."Total Discount", 'The total discount in line 2 does not match the mock data.'); + + EDocumentPurchaseLine.Next(); + Assert.AreEqual('Express Delivery', EDocumentPurchaseLine.Description, 'The description in line 3 does not match the mock data.'); + Assert.AreEqual('DLV-003', EDocumentPurchaseLine."Product Code", 'The product code in line 3 does not match the mock data.'); + Assert.AreEqual(1, EDocumentPurchaseLine.Quantity, 'The quantity in line 3 does not match the mock data.'); + Assert.AreEqual('EA', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in line 3 does not match the mock data.'); + Assert.AreEqual(20, EDocumentPurchaseLine."Unit Price", 'The unit price in line 3 does not match the mock data.'); + Assert.AreEqual(20, EDocumentPurchaseLine."Sub Total", 'The sub total in line 3 does not match the mock data.'); + Assert.AreEqual(15, EDocumentPurchaseLine."VAT Rate", 'The VAT rate in line 3 does not match the mock data.'); + Assert.AreEqual(0, EDocumentPurchaseLine."Total Discount", 'The total discount in line 3 does not match the mock data.'); + end; + #endregion + + local procedure ExpectedCurrencyCode(DocumentCurrency: Code[10]; LCYCode: Code[10]): Code[10] + begin + if DocumentCurrency = LCYCode then + exit(''); + exit(DocumentCurrency); + end; + +} diff --git a/src/Apps/W1/EDocument/Test/src/Processing/EDocumentStructuredTests.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/EDocumentStructuredTests.Codeunit.al index 98e4cd174b..26d9ba14dc 100644 --- a/src/Apps/W1/EDocument/Test/src/Processing/EDocumentStructuredTests.Codeunit.al +++ b/src/Apps/W1/EDocument/Test/src/Processing/EDocumentStructuredTests.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.eServices.EDocument.Test; using Microsoft.eServices.EDocument; +using Microsoft.eServices.EDocument.Format; using Microsoft.eServices.EDocument.Integration; using Microsoft.eServices.EDocument.Processing.Import; using Microsoft.eServices.EDocument.Processing.Import.Purchase; @@ -13,6 +14,7 @@ using Microsoft.Foundation.Attachment; using Microsoft.Purchases.Vendor; using Microsoft.Sales.Customer; using System.IO; +using System.TestLibraries.Config; using System.TestLibraries.Utilities; codeunit 139891 "E-Document Structured Tests" @@ -22,15 +24,14 @@ codeunit 139891 "E-Document Structured Tests" var Customer: Record Customer; - Vendor: Record Vendor; EDocumentService: Record "E-Document Service"; + Vendor: Record Vendor; Assert: Codeunit Assert; - LibraryVariableStorage: Codeunit "Library - Variable Storage"; - LibraryEDoc: Codeunit "Library - E-Document"; EDocImplState: Codeunit "E-Doc. Impl. State"; + StructuredValidations: Codeunit "EDoc Structured Validations"; + LibraryEDoc: Codeunit "Library - E-Document"; LibraryLowerPermission: Codeunit "Library - Lower Permissions"; - CAPIStructuredValidations: Codeunit "CAPI Structured Validations"; - PEPPOLStructuredValidations: Codeunit "PEPPOL Structured Validations"; + LibraryVariableStorage: Codeunit "Library - Variable Storage"; IsInitialized: Boolean; EDocumentStatusNotUpdatedErr: Label 'The status of the EDocument was not updated to the expected status after the step was executed.'; @@ -44,7 +45,7 @@ codeunit 139891 "E-Document Structured Tests" SetupCAPIEDocumentService(); CreateInboundEDocumentFromJSON(EDocument, 'capi/capi-invoice-valid-0.json'); if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then - CAPIStructuredValidations.AssertFullEDocumentContentExtracted(EDocument."Entry No") + StructuredValidations.AssertFullCAPIDocumentExtracted(EDocument."Entry No") else Assert.Fail(EDocumentStatusNotUpdatedErr); end; @@ -59,7 +60,7 @@ codeunit 139891 "E-Document Structured Tests" SetupCAPIEDocumentService(); CreateInboundEDocumentFromJSON(EDocument, 'capi/capi-invoice-unexpected-values-0.json'); if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin - CAPIStructuredValidations.AssertMinimalEDocumentContentParsed(EDocument."Entry No"); + StructuredValidations.AssertMinimalCAPIDocumentParsed(EDocument."Entry No"); EDocumentPurchaseHeader.Get(EDocument."Entry No"); // "value_text": null Assert.AreEqual('', EDocumentPurchaseHeader."Shipping Address", 'Text field should be empty when JSON value is null'); @@ -93,22 +94,315 @@ codeunit 139891 "E-Document Structured Tests" SetupPEPPOLEDocumentService(); CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-0.xml'); if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then - PEPPOLStructuredValidations.AssertFullEDocumentContentExtracted(EDocument."Entry No") + StructuredValidations.AssertFullPEPPOLDocumentExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + [Test] + procedure TestPEPPOLCreditNote_ValidDocument() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] A valid PEPPOL CreditNote XML is parsed into the staging tables with correct header, lines, and BillingReference + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-creditnote-0.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin + StructuredValidations.AssertFullPEPPOLCreditNoteExtracted(EDocument."Entry No"); + EDocument.Get(EDocument."Entry No"); + Assert.AreEqual(Enum::"E-Doc. Process Draft"::"Purchase Credit Memo", EDocument."Process Draft Impl.", 'The process draft implementation should be set to Purchase Credit Memo for credit notes.'); + end + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_ReturnsInvoiceProcessDraftImpl() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] After parsing a PEPPOL Invoice, the Process Draft Impl. is set to "Purchase Invoice" (not the obsoleted "Purchase Document") + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-0.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin + EDocument.Get(EDocument."Entry No"); + Assert.AreEqual(Enum::"E-Doc. Process Draft"::"Purchase Invoice", EDocument."Process Draft Impl.", 'The process draft implementation should be set to Purchase Invoice for invoices.'); + end + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_BaseExample() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] A basic PEPPOL invoice with 2 lines and a document-level charge is parsed correctly. + // Covers: vendor GLN (schemeID=0088), customer non-0088 endpoint (no GLN), charge line creation. + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-basic.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertPEPPOLBaseExampleExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_WithCharges() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] Document-level charges (ChargeIndicator=true) create purchase lines; allowances (ChargeIndicator=false) do not. + // Covers: completeness item "Document-level AllowanceCharge lines not created" + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-charges.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertPEPPOLInvoiceWithChargesExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_VatCategoryS() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] Invoice with multiple VAT rates (25% and 15%), StandardItemIdentification priority over SellersItemIdentification. + // Covers: completeness item "SellersItemIdentification vs StandardItemIdentification merged" + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-vat-category-s.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertPEPPOLVatCategorySExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_VatCategoryZ() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] Invoice with zero-rated VAT (category Z), no DueDate, GBP currency. + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-vat-category-z.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertPEPPOLVatCategoryZExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_AllowanceExample() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] Full PEPPOL example with both charge and allowance at document level, SellersItemIdentification only, 3 invoice lines. + // Covers: allowance does NOT create line, charge DOES, SellersItemIdentification as product code fallback. + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-allowance.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertPEPPOLAllowanceExampleExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLCreditNote_CorrectionNoDueDate() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] CreditNote without PaymentMeans/PaymentDueDate results in blank Due Date. + // Covers: completeness item "CreditNote DueDate uses wrong XPath" (negative case - no DueDate at all) + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-creditnote-no-duedate.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin + StructuredValidations.AssertPEPPOLCreditNoteCorrectionExtracted(EDocument."Entry No"); + EDocument.Get(EDocument."Entry No"); + Assert.AreEqual(Enum::"E-Doc. Process Draft"::"Purchase Credit Memo", EDocument."Process Draft Impl.", 'The process draft implementation should be set to Purchase Credit Memo for credit notes.'); + end + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_EmbeddedAttachments() + var + EDocument: Record "E-Document"; + DocumentAttachment: Record "Document Attachment"; + begin + // [SCENARIO] Embedded base64 attachments are extracted; external URI and bare references are skipped. + // Test XML: 1 valid PDF, 1 valid PNG, 1 external URI (no embedded content), 1 bare reference (no attachment node). + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-attachment.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin + StructuredValidations.AssertPEPPOLAttachmentHeaderExtracted(EDocument."Entry No"); + // Verify exactly 2 attachments were created (embedded PDF + embedded PNG); external URI and bare ref skipped + DocumentAttachment.SetRange("E-Document Entry No.", EDocument."Entry No"); + Assert.AreEqual(2, DocumentAttachment.Count(), 'Expected 2 embedded attachments (PDF + PNG). External URI and bare references should be skipped.'); + end + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_DescriptionFallback() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] When Item/Name is absent, Description is used as fallback. When both exist, Name takes priority. + // Covers: completeness item "Description cascade vs separate fields" + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-description-fallback.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertPEPPOLDescriptionFallbackExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_PayeePartyOverride() + var + EDocument: Record "E-Document"; + begin + // [SCENARIO] When PayeeParty is present, it overrides vendor company name and VAT ID from AccountingSupplierParty. + // Covers: completeness item "PayeeParty/PartyIdentification fallback missing" + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-payee-party.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertPEPPOLPayeePartyOverrideExtracted(EDocument."Entry No") else Assert.Fail(EDocumentStatusNotUpdatedErr); end; #endregion - local procedure Initialize(Integration: Enum "Service Integration") + #region MLLM JSON + [Test] + procedure TestMLLMInvoice_ValidDocument() + var + EDocument: Record "E-Document"; + begin + Initialize(Enum::"Service Integration"::"Mock"); + SetupMLLMEDocumentService(); + CreateInboundEDocumentFromJSON(EDocument, 'mllm/mllm-invoice-valid-0.json'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertFullMLLMDocumentExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + #endregion + + #region Experiment Configuration + [Test] + procedure TestExperiment_ControlAllocation_PreferredImplIsADI() + var + EDocPDFFileFormat: Codeunit "E-Doc. PDF File Format"; + FeatureConfigTestLib: Codeunit "Feature Config Test Lib."; + begin + // [SCENARIO] With control allocation, the PDF file format returns ADI as the preferred implementation + LibraryLowerPermission.SetOutsideO365Scope(); + + FeatureConfigTestLib.UseControlAllocation(); + + Assert.AreEqual( + "Structure Received E-Doc."::ADI, + EDocPDFFileFormat.PreferredStructureDataImplementation(), + 'Control allocation should prefer ADI for PDF processing'); + end; + + // Todo: Reenable once #624677 is fixed + // [Test] + // procedure TestExperiment_TreatmentAllocation_PreferredImplIsMLLM() + // var + // EDocPDFFileFormat: Codeunit "E-Doc. PDF File Format"; + // FeatureConfigTestLib: Codeunit "Feature Config Test Lib."; + // begin + // // [SCENARIO] With treatment allocation, the PDF file format returns MLLM as the preferred implementation + // LibraryLowerPermission.SetOutsideO365Scope(); + + // FeatureConfigTestLib.UseTreatmentAllocation(); + + // Assert.AreEqual( + // "Structure Received E-Doc."::MLLM, + // EDocPDFFileFormat.PreferredStructureDataImplementation(), + // 'Treatment allocation should prefer MLLM for PDF processing'); + // end; + + // Todo: Reenable once #624677 is fixed + // [Test] + // procedure TestExperiment_TreatmentAllocation_MLLMProcessesValidDocument() + // var + // EDocument: Record "E-Document"; + // FeatureConfigTestLib: Codeunit "Feature Config Test Lib."; + // begin + // // [SCENARIO] With treatment allocation active, MLLM is used to process a valid UBL invoice E2E + // Initialize(Enum::"Service Integration"::"Mock"); + // SetupMLLMEDocumentService(); + + // FeatureConfigTestLib.UseTreatmentAllocation(); + + // CreateInboundEDocumentFromJSON(EDocument, 'mllm/mllm-invoice-valid-0.json'); + // if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + // StructuredValidations.AssertFullMLLMDocumentExtracted(EDocument."Entry No") + // else + // Assert.Fail(EDocumentStatusNotUpdatedErr); + // end; + #endregion + + #region Fallback + [Test] + procedure TestMLLM_InvalidJson_ProducesEmptyDraft() var - TransformationRule: Record "Transformation Rule"; EDocument: Record "E-Document"; + EDocumentPurchaseHeader: Record "E-Document Purchase Header"; + begin + // [SCENARIO] When MLLM produces invalid/empty JSON, ReadIntoDraft creates a minimal draft without error + Initialize(Enum::"Service Integration"::"Mock"); + SetupMLLMEDocumentService(); + CreateInboundEDocumentFromJSON(EDocument, 'mllm/mllm-invoice-empty.json'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then begin + EDocumentPurchaseHeader.Get(EDocument."Entry No"); + Assert.AreEqual('', EDocumentPurchaseHeader."Sales Invoice No.", 'Empty JSON should produce empty header fields'); + Assert.AreEqual(0, EDocumentPurchaseHeader.Total, 'Empty JSON should produce zero totals'); + end + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + + [Test] + procedure TestPEPPOLInvoice_NamespacePrefixedRootElement() + var + EDocument: Record "E-Document"; + begin + Initialize(Enum::"Service Integration"::"Mock"); + SetupPEPPOLEDocumentService(); + CreateInboundEDocumentFromXML(EDocument, 'peppol/peppol-invoice-prefixed-ns.xml'); + if ProcessEDocumentToStep(EDocument, "Import E-Document Steps"::"Read into Draft") then + StructuredValidations.AssertFullPEPPOLDocumentExtracted(EDocument."Entry No") + else + Assert.Fail(EDocumentStatusNotUpdatedErr); + end; + #endregion + + local procedure Initialize(Integration: Enum "Service Integration") + var + Currency: Record Currency; + DocumentAttachment: Record "Document Attachment"; EDocDataStorage: Record "E-Doc. Data Storage"; - EDocumentServiceStatus: Record "E-Document Service Status"; + EDocument: Record "E-Document"; EDocumentPurchaseHeader: Record "E-Document Purchase Header"; EDocumentPurchaseLine: Record "E-Document Purchase Line"; - DocumentAttachment: Record "Document Attachment"; - Currency: Record Currency; + EDocumentServiceStatus: Record "E-Document Service Status"; + TransformationRule: Record "Transformation Rule"; begin LibraryLowerPermission.SetOutsideO365Scope(); LibraryVariableStorage.Clear(); @@ -156,6 +450,12 @@ codeunit 139891 "E-Document Structured Tests" EDocumentService.Modify(); end; + local procedure SetupMLLMEDocumentService() + begin + EDocumentService."Read into Draft Impl." := "E-Doc. Read into Draft"::MLLM; + EDocumentService.Modify(); + end; + local procedure CreateInboundEDocumentFromJSON(var EDocument: Record "E-Document"; FilePath: Text) var EDocLogRecord: Record "E-Document Log"; @@ -188,13 +488,13 @@ codeunit 139891 "E-Document Structured Tests" local procedure ProcessEDocumentToStep(var EDocument: Record "E-Document"; ProcessingStep: Enum "Import E-Document Steps"): Boolean var - EDocImportParameters: Record "E-Doc. Import Parameters"; + TempEDocImportParameters: Record "E-Doc. Import Parameters"; EDocImport: Codeunit "E-Doc. Import"; EDocumentProcessing: Codeunit "E-Document Processing"; begin EDocumentProcessing.ModifyEDocumentProcessingStatus(EDocument, "Import E-Doc. Proc. Status"::Readable); - EDocImportParameters."Step to Run" := ProcessingStep; - EDocImport.ProcessIncomingEDocument(EDocument, EDocImportParameters); + TempEDocImportParameters."Step to Run" := ProcessingStep; + EDocImport.ProcessIncomingEDocument(EDocument, TempEDocImportParameters); EDocument.CalcFields("Import Processing Status"); exit(EDocument."Import Processing Status" = Enum::"Import E-Doc. Proc. Status"::"Ready for draft"); end; diff --git a/src/Apps/W1/EDocument/Test/src/Processing/PEPPOLStructuredValidations.Codeunit.al b/src/Apps/W1/EDocument/Test/src/Processing/PEPPOLStructuredValidations.Codeunit.al deleted file mode 100644 index 7510acaea3..0000000000 --- a/src/Apps/W1/EDocument/Test/src/Processing/PEPPOLStructuredValidations.Codeunit.al +++ /dev/null @@ -1,62 +0,0 @@ -// ------------------------------------------------------------------------------------------------ -// Copyright (c) Microsoft Corporation. All rights reserved. -// Licensed under the MIT License. See License.txt in the project root for license information. -// ------------------------------------------------------------------------------------------------ -namespace Microsoft.eServices.EDocument.Test; - -using Microsoft.eServices.EDocument.Processing.Import.Purchase; -using Microsoft.Finance.GeneralLedger.Setup; - -codeunit 139896 "PEPPOL Structured Validations" -{ - var - Assert: Codeunit Assert; - - internal procedure AssertFullEDocumentContentExtracted(EDocumentEntryNo: Integer) - var - GLSetup: Record "General Ledger Setup"; - EDocumentPurchaseHeader: Record "E-Document Purchase Header"; - EDocumentPurchaseLine: Record "E-Document Purchase Line"; - begin - GLSetup.Get(); - EDocumentPurchaseHeader.Get(EDocumentEntryNo); - Assert.AreEqual('103033', EDocumentPurchaseHeader."Sales Invoice No.", 'The sales invoice number does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(22, 01, 2026), EDocumentPurchaseHeader."Document Date", 'The invoice date does not allign with the mock data.'); - Assert.AreEqual(DMY2Date(22, 02, 2026), EDocumentPurchaseHeader."Due Date", 'The due date does not allign with the mock data.'); - Assert.AreEqual('XYZ', EDocumentPurchaseHeader."Currency Code", 'The currency code does not allign with the mock data.'); - Assert.AreEqual('2', EDocumentPurchaseHeader."Purchase Order No.", 'The purchase order number does not allign with the mock data.'); - // Assert.AreEqual('', EDocumentPurchaseHeader."Vendor GLN", 'The endpoint schema is not provided to populate the GLN.'); - Assert.AreEqual('CRONUS International', EDocumentPurchaseHeader."Vendor Company Name", 'The vendor name does not allign with the mock data.'); - Assert.AreEqual('Main Street, 14', EDocumentPurchaseHeader."Vendor Address", 'The vendor street does not allign with the mock data.'); - Assert.AreEqual('GB123456789', EDocumentPurchaseHeader."Vendor VAT Id", 'The vendor VAT id does not allign with the mock data.'); - Assert.AreEqual('Jim Olive', EDocumentPurchaseHeader."Vendor Contact Name", 'The vendor contact name does not allign with the mock data.'); - Assert.AreEqual('The Cannon Group PLC', EDocumentPurchaseHeader."Customer Company Name", 'The customer name does not allign with the mock data.'); - Assert.AreEqual('GB789456278', EDocumentPurchaseHeader."Customer VAT Id", 'The customer VAT id does not allign with the mock data.'); - Assert.AreEqual('192 Market Square', EDocumentPurchaseHeader."Customer Address", 'The customer address does not allign with the mock data.'); - - EDocumentPurchaseLine.SetRange("E-Document Entry No.", EDocumentEntryNo); - EDocumentPurchaseLine.FindSet(); - Assert.AreEqual(1, EDocumentPurchaseLine."Quantity", 'The quantity in the purchase line does not allign with the mock data.'); - Assert.AreEqual('PCS', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the purchase line does not allign with the mock data.'); - Assert.AreEqual(4000, EDocumentPurchaseLine."Sub Total", 'The total amount before taxes in the purchase line does not allign with the mock data.'); - Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the purchase line does not allign with the mock data.'); - Assert.AreEqual(0, EDocumentPurchaseLine."Total Discount", 'The total discount in the purchase line does not allign with the mock data.'); - Assert.AreEqual('Bicycle', EDocumentPurchaseLine.Description, 'The product description in the purchase line does not allign with the mock data.'); - Assert.AreEqual('1000', EDocumentPurchaseLine."Product Code", 'The product code in the purchase line does not allign with the mock data.'); - Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'The VAT rate in the purchase line does not allign with the mock data.'); - Assert.AreEqual(4000, EDocumentPurchaseLine."Unit Price", 'The unit price in the purchase line does not allign with the mock data.'); - - EDocumentPurchaseLine.Next(); - Assert.AreEqual(2, EDocumentPurchaseLine."Quantity", 'The quantity in the purchase line does not allign with the mock data.'); - Assert.AreEqual('PCS', EDocumentPurchaseLine."Unit of Measure", 'The unit of measure in the purchase line does not allign with the mock data.'); - Assert.AreEqual(10000, EDocumentPurchaseLine."Sub Total", 'The total amount before taxes in the purchase line does not allign with the mock data.'); - Assert.AreEqual('XYZ', EDocumentPurchaseLine."Currency Code", 'The currency code in the purchase line does not allign with the mock data.'); - Assert.AreEqual(0, EDocumentPurchaseLine."Total Discount", 'The total discount in the purchase line does not allign with the mock data.'); - Assert.AreEqual('Bicycle v2', EDocumentPurchaseLine.Description, 'The product description in the purchase line does not allign with the mock data.'); - Assert.AreEqual('2000', EDocumentPurchaseLine."Product Code", 'The product code in the purchase line does not allign with the mock data.'); - Assert.AreEqual(25, EDocumentPurchaseLine."VAT Rate", 'The VAT rate in the purchase line does not allign with the mock data.'); - Assert.AreEqual(5000, EDocumentPurchaseLine."Unit Price", 'The unit price in the purchase line does not allign with the mock data.'); - - end; - -} diff --git a/src/Apps/W1/EDocumentConnectors/Continia/App/src/API Requests/ContiniaApiUrl.Codeunit.al b/src/Apps/W1/EDocumentConnectors/Continia/App/src/API Requests/ContiniaApiUrl.Codeunit.al index ceeadad542..e6b4f1f34b 100644 --- a/src/Apps/W1/EDocumentConnectors/Continia/App/src/API Requests/ContiniaApiUrl.Codeunit.al +++ b/src/Apps/W1/EDocumentConnectors/Continia/App/src/API Requests/ContiniaApiUrl.Codeunit.al @@ -172,13 +172,16 @@ codeunit 6392 "Continia Api Url" local procedure COBaseUrl() Url: Text var + EnvironmentInformation: Codeunit "Environment Information"; Handled: Boolean; begin OnGetCOBaseUrl(Url, Handled); if Handled then exit(Url); - exit('https://auth.continiaonline.com/api/v1'); + if not EnvironmentInformation.IsSandbox() then + exit('https://auth.continiaonline.com/api/v1'); + exit('https://demoauth.continiaonline.com/api/v1'); end; @@ -192,6 +195,9 @@ codeunit 6392 "Continia Api Url" if Handled then exit(Url); + if EnvironmentInformation.IsSandbox() then + exit('https://democdnapi.continiaonline.com/api/v1.0'); + LocalizedBaseUrl := GetBaseUrlForLocalization(EnvironmentInformation.GetApplicationFamily()); if LocalizedBaseUrl <> '' then exit(LocalizedBaseUrl) diff --git a/src/Apps/W1/ErrorMessagesWithRecommendations/Test/ErrorMessageExtensibilityTests.Codeunit.al b/src/Apps/W1/ErrorMessagesWithRecommendations/Test/ErrorMessageExtensibilityTests.Codeunit.al index 535cdd0f9b..bd3cc878be 100644 --- a/src/Apps/W1/ErrorMessagesWithRecommendations/Test/ErrorMessageExtensibilityTests.Codeunit.al +++ b/src/Apps/W1/ErrorMessagesWithRecommendations/Test/ErrorMessageExtensibilityTests.Codeunit.al @@ -407,12 +407,12 @@ codeunit 139621 ErrorMessageExtensibilityTests local procedure MockFullBatchCheck(TemplateName: Code[10]; BatchName: Code[10]; var TempErrorMessage: Record "Error Message" temporary) var - ErrorHandlingParameters: Record "Error Handling Parameters"; + TempErrorHandlingParameters: Record "Error Handling Parameters"; CheckGenJnlLineBackgr: Codeunit "Check Gen. Jnl. Line. Backgr."; Params: Dictionary of [Text, Text]; begin - SetErrorHandlingParameters(ErrorHandlingParameters, TemplateName, BatchName, '', 0D, '', 0D, true, false); - ErrorHandlingParameters.ToArgs(Params); + SetErrorHandlingParameters(TempErrorHandlingParameters, TemplateName, BatchName, '', 0D, '', 0D, true, false); + TempErrorHandlingParameters.ToArgs(Params); Commit(); CheckGenJnlLineBackgr.RunCheck(Params, TempErrorMessage); end; diff --git a/src/Apps/W1/ExcelReports/App/ReportLayouts/Excel/Purchase/AgedAccountsPayableExcel.xlsx b/src/Apps/W1/ExcelReports/App/ReportLayouts/Excel/Purchase/AgedAccountsPayableExcel.xlsx index aca24385a0..7619e4ffd6 100644 Binary files a/src/Apps/W1/ExcelReports/App/ReportLayouts/Excel/Purchase/AgedAccountsPayableExcel.xlsx and b/src/Apps/W1/ExcelReports/App/ReportLayouts/Excel/Purchase/AgedAccountsPayableExcel.xlsx differ diff --git a/src/Apps/W1/ExcelReports/App/ReportLayouts/Excel/Sales/AgedAccountsReceivableExcel.xlsx b/src/Apps/W1/ExcelReports/App/ReportLayouts/Excel/Sales/AgedAccountsReceivableExcel.xlsx index 3ae5ad1c77..d68dce1ed4 100644 Binary files a/src/Apps/W1/ExcelReports/App/ReportLayouts/Excel/Sales/AgedAccountsReceivableExcel.xlsx and b/src/Apps/W1/ExcelReports/App/ReportLayouts/Excel/Sales/AgedAccountsReceivableExcel.xlsx differ diff --git a/src/Apps/W1/ExcelReports/App/src/Customer/EXRCustomerTopList.Report.al b/src/Apps/W1/ExcelReports/App/src/Customer/EXRCustomerTopList.Report.al index 9a7970efd5..2dccb6d438 100644 --- a/src/Apps/W1/ExcelReports/App/src/Customer/EXRCustomerTopList.Report.al +++ b/src/Apps/W1/ExcelReports/App/src/Customer/EXRCustomerTopList.Report.al @@ -130,7 +130,9 @@ report 4409 "EXR Customer Top List" DateFilter: Text; protected var +#pragma warning disable AA0073 GlobalExtTopCustomerReportBuffer: Record "EXR Top Customer Report Buffer"; +#pragma warning restore AA0073 EXTTopCustomerCaptionHandler: Codeunit "EXT Top Cust. Caption Handler"; NoOfRecordsToPrint: Integer; diff --git a/src/Apps/W1/ExcelReports/App/src/Customer/ExtTopCustCaptionHandler.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Customer/ExtTopCustCaptionHandler.Codeunit.al index 1b99263a59..55512c755e 100644 --- a/src/Apps/W1/ExcelReports/App/src/Customer/ExtTopCustCaptionHandler.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Customer/ExtTopCustCaptionHandler.Codeunit.al @@ -13,7 +13,7 @@ codeunit 4405 "EXT Top Cust. Caption Handler" [EventSubscriber(ObjectType::Table, Database::"EXR Top Customer Report Buffer", 'OnGetAmount1Caption', '', false, false)] local procedure GetAmount1Caption(var NewCaption: Text; var Handled: Boolean) begin - if (EXTTopReportBuffer."Ranking Based On" = EXTTopReportBuffer."Ranking Based On"::"Balance (LCY)") then begin + if (TempEXTTopReportBuffer."Ranking Based On" = TempEXTTopReportBuffer."Ranking Based On"::"Balance (LCY)") then begin NewCaption := BalanceLCYTok; Handled := true; exit; @@ -26,7 +26,7 @@ codeunit 4405 "EXT Top Cust. Caption Handler" [EventSubscriber(ObjectType::Table, Database::"EXR Top Customer Report Buffer", 'OnGetAmount2Caption', '', false, false)] local procedure GetAmount2Caption(var NewCaption: Text; var Handled: Boolean) begin - if EXTTopReportBuffer."Ranking Based On" <> EXTTopReportBuffer."Ranking Based On"::"Balance (LCY)" then begin + if TempEXTTopReportBuffer."Ranking Based On" <> TempEXTTopReportBuffer."Ranking Based On"::"Balance (LCY)" then begin NewCaption := BalanceLCYTok; Handled := true; exit; @@ -38,11 +38,11 @@ codeunit 4405 "EXT Top Cust. Caption Handler" internal procedure SetRankingBasedOn(NewRankingBasedOn: Option) begin - EXTTopReportBuffer."Ranking Based On" := NewRankingBasedOn; + TempEXTTopReportBuffer."Ranking Based On" := NewRankingBasedOn; end; var - EXTTopReportBuffer: Record "EXR Top Customer Report Buffer"; + TempEXTTopReportBuffer: Record "EXR Top Customer Report Buffer"; BalanceLCYTok: Label 'Balance (LCY)'; SalesLCYTok: Label 'Sales (LCY)'; diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgedAccPayableExcel.Report.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgedAccPayableExcel.Report.al index e4de74d342..a09b3759b3 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgedAccPayableExcel.Report.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgedAccPayableExcel.Report.al @@ -43,6 +43,14 @@ report 4403 "EXR Aged Acc Payable Excel" DataItemTableView = sorting("Vendor Source No."); DataItemLink = "Vendor Source No." = field("No."); + column(DocumentType; "Document Type") + { + IncludeCaption = true; + } + column(DocumentNo; "Document No.") + { + IncludeCaption = true; + } column(PeriodStart; "Period Start Date") { @@ -355,6 +363,7 @@ report 4403 "EXR Aged Acc Payable Excel" AgingData."Vendor Source No." := VendorLedgerEntry."Vendor No."; AgingData."Source Name" := VendorLedgerEntry."Vendor Name"; AgingData."Document No." := VendorLedgerEntry."Document No."; + AgingData."Document Type" := VendorLedgerEntry."Document Type"; AgingData."Dimension 1 Code" := VendorLedgerEntry."Global Dimension 1 Code"; AgingData."Dimension 2 Code" := VendorLedgerEntry."Global Dimension 2 Code"; AgingData."Currency Code" := VendorLedgerEntry."Currency Code"; diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgedAccountsRecExcel.Report.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgedAccountsRecExcel.Report.al index 1aad3b6cd9..b16e1ca692 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgedAccountsRecExcel.Report.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgedAccountsRecExcel.Report.al @@ -42,7 +42,14 @@ report 4402 "EXR Aged Accounts Rec Excel" { DataItemTableView = sorting("Vendor Source No."); DataItemLink = "Vendor Source No." = field("No."); - + column(DocumentType; "Document Type") + { + IncludeCaption = true; + } + column(DocumentNo; "Document No.") + { + IncludeCaption = true; + } column(PeriodStart; "Period Start Date") { @@ -355,6 +362,7 @@ report 4402 "EXR Aged Accounts Rec Excel" AgingData."Vendor Source No." := CustLedgerEntry."Customer No."; AgingData."Source Name" := CustLedgerEntry."Customer Name"; AgingData."Document No." := CustLedgerEntry."Document No."; + AgingData."Document Type" := CustLedgerEntry."Document Type"; AgingData."Dimension 1 Code" := CustLedgerEntry."Global Dimension 1 Code"; AgingData."Dimension 2 Code" := CustLedgerEntry."Global Dimension 2 Code"; AgingData."Currency Code" := CustLedgerEntry."Currency Code"; diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgingReportBuffer.Table.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgingReportBuffer.Table.al index c28309749e..2a5bd69c80 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgingReportBuffer.Table.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRAgingReportBuffer.Table.al @@ -5,6 +5,8 @@ namespace Microsoft.Finance.ExcelReports; +using Microsoft.Finance.GeneralLedger.Journal; + table 4401 "EXR Aging Report Buffer" { AllowInCustomizations = Never; @@ -35,6 +37,10 @@ table 4401 "EXR Aging Report Buffer" { Caption = 'Document No.'; } + field(9; "Document Type"; Enum "Gen. Journal Document Type") + { + Caption = 'Document Type'; + } field(11; "Posting Date"; Date) { Caption = 'Posting Date'; diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalPrevYearExcel.Report.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalPrevYearExcel.Report.al index 1445b49945..358569e694 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalPrevYearExcel.Report.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalPrevYearExcel.Report.al @@ -205,31 +205,57 @@ report 4407 "EXR Trial Bal. Prev Year Excel" local procedure AddGLToDataset(var GLAccount: Record "G/L Account"; Dimension1Code: Code[20]; Dimension2Code: Code[20]) var LocalGLAccount: Record "G/L Account"; + LocalGLAccountBalance: Record "G/L Account"; LocalGLAccountLastPeriod: Record "G/L Account"; + LocalGLAccountLastPeriodBalance: Record "G/L Account"; begin LocalGLAccount.Copy(GLAccount); LocalGLAccount.SetRange("Global Dimension 1 Filter", Dimension1Code); LocalGLAccount.SetRange("Global Dimension 2 Filter", Dimension2Code); - LocalGLAccount.CalcFields("Net Change", "Balance at Date", "Additional-Currency Net Change", "Add.-Currency Balance at Date"); + LocalGLAccount.CalcFields("Net Change", "Balance at Date", "Additional-Currency Net Change", "Add.-Currency Balance at Date", "Debit Amount", "Credit Amount", "Add.-Currency Debit Amount", "Add.-Currency Credit Amount"); + // Cumulative debit/credit for balance requires date filter ..EndDate (Debit Amount uses the full range, not upperlimit like Balance at Date) + LocalGLAccountBalance.Copy(LocalGLAccount); + LocalGLAccountBalance.SetFilter("Date Filter", '..%1', LocalGLAccount.GetRangeMax("Date Filter")); + LocalGLAccountBalance.CalcFields("Debit Amount", "Credit Amount", "Add.-Currency Debit Amount", "Add.-Currency Credit Amount"); + LocalGLAccountLastPeriod.Copy(LocalGLAccount); LocalGLAccountLastPeriod.SetRange("Date Filter", PriorFromDate, PriorToDate); - LocalGLAccountLastPeriod.CalcFields("Net Change", "Balance at Date", "Additional-Currency Net Change", "Add.-Currency Balance at Date"); + LocalGLAccountLastPeriod.CalcFields("Net Change", "Balance at Date", "Additional-Currency Net Change", "Add.-Currency Balance at Date", "Debit Amount", "Credit Amount", "Add.-Currency Debit Amount", "Add.-Currency Credit Amount"); + LocalGLAccountLastPeriodBalance.Copy(LocalGLAccountLastPeriod); + LocalGLAccountLastPeriodBalance.SetFilter("Date Filter", '..%1', PriorToDate); + LocalGLAccountLastPeriodBalance.CalcFields("Debit Amount", "Credit Amount", "Add.-Currency Debit Amount", "Add.-Currency Credit Amount"); Clear(EXRTrialBalanceBuffer); EXRTrialBalanceBuffer."G/L Account No." := LocalGLAccount."No."; EXRTrialBalanceBuffer."Dimension 1 Code" := Dimension1Code; EXRTrialBalanceBuffer."Dimension 2 Code" := Dimension2Code; - EXRTrialBalanceBuffer.Validate("Net Change", LocalGLAccount."Net Change"); - EXRTrialBalanceBuffer.Validate("Balance", LocalGLAccount."Balance at Date"); - EXRTrialBalanceBuffer.Validate("Last Period Net", LocalGLAccountLastPeriod."Net Change"); - EXRTrialBalanceBuffer.Validate("Last Period Bal.", LocalGLAccountLastPeriod."Balance at Date"); + EXRTrialBalanceBuffer."Net Change" := LocalGLAccount."Net Change"; + EXRTrialBalanceBuffer."Net Change (Debit)" := LocalGLAccount."Debit Amount"; + EXRTrialBalanceBuffer."Net Change (Credit)" := LocalGLAccount."Credit Amount"; + EXRTrialBalanceBuffer.Balance := LocalGLAccount."Balance at Date"; + EXRTrialBalanceBuffer."Balance (Debit)" := LocalGLAccountBalance."Debit Amount"; + EXRTrialBalanceBuffer."Balance (Credit)" := LocalGLAccountBalance."Credit Amount"; + EXRTrialBalanceBuffer."Last Period Net" := LocalGLAccountLastPeriod."Net Change"; + EXRTrialBalanceBuffer."Last Period Net (Debit)" := LocalGLAccountLastPeriod."Debit Amount"; + EXRTrialBalanceBuffer."Last Period Net (Credit)" := LocalGLAccountLastPeriod."Credit Amount"; + EXRTrialBalanceBuffer."Last Period Bal." := LocalGLAccountLastPeriod."Balance at Date"; + EXRTrialBalanceBuffer."Last Period Bal. (Debit)" := LocalGLAccountLastPeriodBalance."Debit Amount"; + EXRTrialBalanceBuffer."Last Period Bal. (Credit)" := LocalGLAccountLastPeriodBalance."Credit Amount"; - EXRTrialBalanceBuffer.Validate("Net Change (ACY)", LocalGLAccount."Additional-Currency Net Change"); - EXRTrialBalanceBuffer.Validate("Balance (ACY)", LocalGLAccount."Add.-Currency Balance at Date"); - EXRTrialBalanceBuffer.Validate("Last Period Net (ACY)", LocalGLAccountLastPeriod."Additional-Currency Net Change"); - EXRTrialBalanceBuffer.Validate("Last Period Bal. (ACY)", LocalGLAccountLastPeriod."Add.-Currency Balance at Date"); + EXRTrialBalanceBuffer."Net Change (ACY)" := LocalGLAccount."Additional-Currency Net Change"; + EXRTrialBalanceBuffer."Net Change (Debit) (ACY)" := LocalGLAccount."Add.-Currency Debit Amount"; + EXRTrialBalanceBuffer."Net Change (Credit) (ACY)" := LocalGLAccount."Add.-Currency Credit Amount"; + EXRTrialBalanceBuffer."Balance (ACY)" := LocalGLAccount."Add.-Currency Balance at Date"; + EXRTrialBalanceBuffer."Balance (Debit) (ACY)" := LocalGLAccountBalance."Add.-Currency Debit Amount"; + EXRTrialBalanceBuffer."Balance (Credit) (ACY)" := LocalGLAccountBalance."Add.-Currency Credit Amount"; + EXRTrialBalanceBuffer."Last Period Net (ACY)" := LocalGLAccountLastPeriod."Additional-Currency Net Change"; + EXRTrialBalanceBuffer."Last Period Net (Debit) (ACY)" := LocalGLAccountLastPeriod."Add.-Currency Debit Amount"; + EXRTrialBalanceBuffer."Last Period Net (Credit) (ACY)" := LocalGLAccountLastPeriod."Add.-Currency Credit Amount"; + EXRTrialBalanceBuffer."Last Period Bal. (ACY)" := LocalGLAccountLastPeriod."Add.-Currency Balance at Date"; + EXRTrialBalanceBuffer."Last Period Bal. (Debit) (ACY)" := LocalGLAccountLastPeriodBalance."Add.-Currency Debit Amount"; + EXRTrialBalanceBuffer."Last Period Bal. (Cred.) (ACY)" := LocalGLAccountLastPeriodBalance."Add.-Currency Credit Amount"; EXRTrialBalanceBuffer.CalculateVariances(); EXRTrialBalanceBuffer.Insert(true); end; diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalance.Query.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalance.Query.al index 85382bd331..4df78c7f1f 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalance.Query.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalance.Query.al @@ -32,10 +32,26 @@ query 4405 "EXR Trial Balance" { Method = sum; } + column(DebitAmount; "Debit Amount") + { + Method = sum; + } + column(CreditAmount; "Credit Amount") + { + Method = sum; + } column(ACYAmount; "Additional-Currency Amount") { Method = sum; } + column(ACYDebitAmount; "Add.-Currency Debit Amount") + { + Method = sum; + } + column(ACYCreditAmount; "Add.-Currency Credit Amount") + { + Method = sum; + } column(DimensionValue1Code; "Global Dimension 1 Code") { } diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceBU.Query.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceBU.Query.al index bc8421f650..51f12aeb31 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceBU.Query.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceBU.Query.al @@ -32,10 +32,26 @@ query 4407 "EXR Trial Balance BU" { Method = sum; } + column(DebitAmount; "Debit Amount") + { + Method = sum; + } + column(CreditAmount; "Credit Amount") + { + Method = sum; + } column(ACYAmount; "Additional-Currency Amount") { Method = sum; } + column(ACYDebitAmount; "Add.-Currency Debit Amount") + { + Method = sum; + } + column(ACYCreditAmount; "Add.-Currency Credit Amount") + { + Method = sum; + } column(BusinessUnitCode; "Business Unit Code") { } diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceBuffer.Table.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceBuffer.Table.al index c2a347c1e3..53eac3b8fd 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceBuffer.Table.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceBuffer.Table.al @@ -45,18 +45,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Net Change'; AutoFormatType = 1; AutoFormatExpression = ''; - - trigger OnValidate() - begin - if ("Net Change" > 0) then begin - Validate("Net Change (Debit)", "Net Change"); - Validate("Net Change (Credit)", 0); - end - else begin - Validate("Net Change (Credit)", -"Net Change"); - Validate("Net Change (Debit)", 0); - end; - end; } field(11; "Net Change (Debit)"; Decimal) { @@ -75,18 +63,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Balance'; AutoFormatType = 1; AutoFormatExpression = ''; - - trigger OnValidate() - begin - if ("Balance" > 0) then begin - Validate("Balance (Debit)", "Balance"); - Validate("Balance (Credit)", 0); - end - else begin - Validate("Balance (Credit)", -"Balance"); - Validate("Balance (Debit)", 0); - end; - end; } field(14; "Balance (Debit)"; Decimal) { @@ -105,18 +81,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Starting Balance'; AutoFormatType = 1; AutoFormatExpression = ''; - - trigger OnValidate() - begin - if ("Starting Balance" > 0) then begin - Validate("Starting Balance (Debit)", "Starting Balance"); - Validate("Starting Balance (Credit)", 0); - end - else begin - Validate("Starting Balance (Credit)", -"Starting Balance"); - Validate("Starting Balance (Debit)", 0); - end; - end; } field(17; "Starting Balance (Debit)"; Decimal) { @@ -157,18 +121,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Last Period Net'; AutoFormatType = 1; AutoFormatExpression = ''; - - trigger OnValidate() - begin - if ("Last Period Net" > 0) then begin - Validate("Last Period Net (Debit)", "Last Period Net"); - Validate("Last Period Net (Credit)", 0); - end - else begin - Validate("Last Period Net (Credit)", -"Last Period Net"); - Validate("Last Period Net (Debit)", 0); - end; - end; } field(51; "Last Period Net (Debit)"; Decimal) { @@ -187,18 +139,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Last Period Bal.'; AutoFormatType = 1; AutoFormatExpression = ''; - - trigger OnValidate() - begin - if ("Last Period Bal." > 0) then begin - Validate("Last Period Bal. (Debit)", "Last Period Bal."); - Validate("Last Period Bal. (Credit)", 0); - end - else begin - Validate("Last Period Bal. (Credit)", -"Last Period Bal."); - Validate("Last Period Bal. (Debit)", 0); - end; - end; } field(61; "Last Period Bal. (Debit)"; Decimal) { @@ -239,18 +179,6 @@ table 4402 "EXR Trial Balance Buffer" AutoFormatType = 1; AutoFormatExpression = GetAdditionalReportingCurrencyCode(); Caption = 'Net Change'; - - trigger OnValidate() - begin - if ("Net Change (ACY)" > 0) then begin - Validate("Net Change (Debit) (ACY)", "Net Change (ACY)"); - Validate("Net Change (Credit) (ACY)", 0); - end - else begin - Validate("Net Change (Credit) (ACY)", -"Net Change (ACY)"); - Validate("Net Change (Debit) (ACY)", 0); - end; - end; } field(111; "Net Change (Debit) (ACY)"; Decimal) { @@ -269,18 +197,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Balance'; AutoFormatType = 1; AutoFormatExpression = GetAdditionalReportingCurrencyCode(); - - trigger OnValidate() - begin - if ("Balance (ACY)" > 0) then begin - Validate("Balance (Debit) (ACY)", "Balance (ACY)"); - Validate("Balance (Credit) (ACY)", 0); - end - else begin - Validate("Balance (Credit) (ACY)", -"Balance (ACY)"); - Validate("Balance (Debit) (ACY)", 0); - end; - end; } field(114; "Balance (Debit) (ACY)"; Decimal) { @@ -299,18 +215,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Starting Balance'; AutoFormatType = 1; AutoFormatExpression = GetAdditionalReportingCurrencyCode(); - - trigger OnValidate() - begin - if ("Starting Balance (ACY)" > 0) then begin - Validate("Starting Balance (Debit) (ACY)", "Starting Balance (ACY)"); - Validate("Starting Balance (Credit)(ACY)", 0); - end - else begin - Validate("Starting Balance (Credit)(ACY)", -"Starting Balance (ACY)"); - Validate("Starting Balance (Debit) (ACY)", 0); - end; - end; } field(117; "Starting Balance (Debit) (ACY)"; Decimal) { @@ -329,18 +233,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Last Period Net'; AutoFormatType = 1; AutoFormatExpression = GetAdditionalReportingCurrencyCode(); - - trigger OnValidate() - begin - if ("Last Period Net (ACY)" > 0) then begin - Validate("Last Period Net (Debit) (ACY)", "Last Period Net (ACY)"); - Validate("Last Period Net (Credit) (ACY)", 0); - end - else begin - Validate("Last Period Net (Credit) (ACY)", -"Last Period Net (ACY)"); - Validate("Last Period Net (Debit) (ACY)", 0); - end; - end; } field(151; "Last Period Net (Debit) (ACY)"; Decimal) { @@ -359,18 +251,6 @@ table 4402 "EXR Trial Balance Buffer" Caption = 'Last Period Bal.'; AutoFormatType = 1; AutoFormatExpression = GetAdditionalReportingCurrencyCode(); - - trigger OnValidate() - begin - if ("Last Period Bal. (ACY)" > 0) then begin - Validate("Last Period Bal. (Debit) (ACY)", "Last Period Bal. (ACY)"); - Validate("Last Period Bal. (Cred.) (ACY)", 0); - end - else begin - Validate("Last Period Bal. (Cred.) (ACY)", -"Last Period Bal. (ACY)"); - Validate("Last Period Bal. (Debit) (ACY)", 0); - end; - end; } field(161; "Last Period Bal. (Debit) (ACY)"; Decimal) { diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceExcel.Report.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceExcel.Report.al index da0aae9e3c..37bc299986 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceExcel.Report.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalanceExcel.Report.al @@ -38,6 +38,13 @@ report 4405 "EXR Trial Balance Excel" trigger OnAfterGetRecord() begin + if HideAccountsWithNoActivity then begin + TrialBalanceData.SetRange("G/L Account No.", GLAccounts."No."); + if TrialBalanceData.IsEmpty() then + CurrReport.Skip(); + TrialBalanceData.SetRange("G/L Account No."); + end; + IndentedAccountName := PadStr('', GLAccounts.Indentation * 2, ' ') + GLAccounts.Name; end; } @@ -96,6 +103,12 @@ report 4405 "EXR Trial Balance Excel" { Caption = 'Options'; + field(HideAccountsWithNoActivityField; HideAccountsWithNoActivity) + { + ApplicationArea = All; + Caption = 'Hide Accounts with No Activity'; + ToolTip = 'Specifies whether to exclude G/L accounts that have no activity for the selected period. When enabled, only accounts with at least one non-zero value across opening balance, turnover, or closing balance are shown.'; + } // Used to set the date filter on the report header across multiple languages field(RequestDateFilter; DateFilter) { @@ -156,6 +169,7 @@ report 4405 "EXR Trial Balance Excel" var ExcelReportsTelemetry: Codeunit "Excel Reports Telemetry"; DateFilter: Text; + HideAccountsWithNoActivity: Boolean; protected var CompanyInformation: Record "Company Information"; diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalbyPeriodExcel.Report.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalbyPeriodExcel.Report.al index c39bd2c4ec..c749d81dc8 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalbyPeriodExcel.Report.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXRTrialBalbyPeriodExcel.Report.al @@ -206,20 +206,30 @@ report 4408 "EXR Trial Bal by Period Excel" local procedure AddGLToDataset(var GLAccount: Record "G/L Account"; PeriodStartDate: Date; PeriodEndDate: Date; Dimension1Code: Code[20]; Dimension2Code: Code[20]) var LocalGLAccount: Record "G/L Account"; + LocalGLAccountBalance: Record "G/L Account"; begin LocalGLAccount.Copy(GLAccount); LocalGLAccount.SetFilter("Global Dimension 1 Filter", Dimension1Code); LocalGLAccount.SetFilter("Global Dimension 2 Filter", Dimension2Code); - LocalGLAccount.CalcFields("Net Change", "Balance at Date"); + LocalGLAccount.CalcFields("Net Change", "Balance at Date", "Debit Amount", "Credit Amount"); + // Cumulative debit/credit for balance requires date filter ..EndDate + LocalGLAccountBalance.Copy(LocalGLAccount); + LocalGLAccountBalance.SetFilter("Date Filter", '..%1', PeriodEndDate); + LocalGLAccountBalance.CalcFields("Debit Amount", "Credit Amount"); + Clear(EXRTrialBalanceBuffer); EXRTrialBalanceBuffer."G/L Account No." := LocalGLAccount."No."; EXRTrialBalanceBuffer."Period Start" := PeriodStartDate; EXRTrialBalanceBuffer."Period End" := PeriodEndDate; EXRTrialBalanceBuffer."Dimension 1 Code" := Dimension1Code; EXRTrialBalanceBuffer."Dimension 2 Code" := Dimension2Code; - EXRTrialBalanceBuffer.Validate("Net Change", LocalGLAccount."Net Change"); - EXRTrialBalanceBuffer.Validate("Balance", LocalGLAccount."Balance at Date"); + EXRTrialBalanceBuffer."Net Change" := LocalGLAccount."Net Change"; + EXRTrialBalanceBuffer."Net Change (Debit)" := LocalGLAccount."Debit Amount"; + EXRTrialBalanceBuffer."Net Change (Credit)" := LocalGLAccount."Credit Amount"; + EXRTrialBalanceBuffer.Balance := LocalGLAccount."Balance at Date"; + EXRTrialBalanceBuffer."Balance (Debit)" := LocalGLAccountBalance."Debit Amount"; + EXRTrialBalanceBuffer."Balance (Credit)" := LocalGLAccountBalance."Credit Amount"; EXRTrialBalanceBuffer.Insert(true); end; } diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/EXTAgedAccCaptionHandler.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Financials/EXTAgedAccCaptionHandler.Codeunit.al index 2ad3835c04..dc51474a4d 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/EXTAgedAccCaptionHandler.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/EXTAgedAccCaptionHandler.Codeunit.al @@ -13,14 +13,14 @@ codeunit 4406 "EXT Aged Acc. Caption Handler" [EventSubscriber(ObjectType::Table, Database::"EXR Aging Report Buffer", 'OnOverrideAgedBy', '', false, false)] local procedure HandleOverrideAgedBy(var EXRAgingReportBuffer: Record "EXR Aging Report Buffer" temporary) begin - EXRAgingReportBuffer."Aged By" := GlobalEXRAgingReportBuffer."Aged By"; + EXRAgingReportBuffer."Aged By" := TempGlobalEXRAgingReportBuffer."Aged By"; end; internal procedure SetGlobalEXRAgingReportBuffer(var EXRAgingReportBuffer: Record "EXR Aging Report Buffer" temporary) begin - GlobalEXRAgingReportBuffer.Copy(EXRAgingReportBuffer); + TempGlobalEXRAgingReportBuffer.Copy(EXRAgingReportBuffer); end; var - GlobalEXRAgingReportBuffer: Record "EXR Aging Report Buffer"; + TempGlobalEXRAgingReportBuffer: Record "EXR Aging Report Buffer"; } \ No newline at end of file diff --git a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al index 9ae62f192c..eafe2f3269 100644 --- a/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Financials/TrialBalance.Codeunit.al @@ -10,7 +10,6 @@ using Microsoft.Finance.Consolidation; #endif using Microsoft.Finance.Dimension; using Microsoft.Finance.GeneralLedger.Account; -using Microsoft.Finance.GeneralLedger.Ledger; #if not CLEAN27 using System.Environment.Configuration; #endif @@ -147,28 +146,36 @@ codeunit 4410 "Trial Balance" local procedure InsertTrialBalanceDataForGLAccountWithFilters(var GLAccount: Record "G/L Account"; Dimension1ValueCode: Code[20]; Dimension2ValueCode: Code[20]; BusinessUnitCode: Code[20]; var TrialBalanceData: Record "EXR Trial Balance Buffer"; var Dimension1Values: Record "Dimension Value" temporary; var Dimension2Values: Record "Dimension Value" temporary) var GLAccount2: Record "G/L Account"; - GLEntry: Record "G/L Entry"; begin Clear(TrialBalanceData); if GLAccount.GetFilter("Date Filter") <> '' then begin - GLEntry.SetFilter("Posting Date", GLAccount.GetFilter("Date Filter")); - if GLEntry.FindFirst() then begin - GLAccount2.Copy(GLAccount); - GLAccount2.SetFilter("Date Filter", '..%1', GLEntry."Posting Date" - 1); - GLAccount2.CalcFields("Balance at Date", "Add.-Currency Balance at Date"); - TrialBalanceData.Validate("Starting Balance", GLAccount2."Balance at Date"); - TrialBalanceData.Validate("Starting Balance (ACY)", GLAccount2."Add.-Currency Balance at Date"); - end; + GLAccount2.Copy(GLAccount); + GLAccount2.SetFilter("Date Filter", '..%1', ClosingDate(GLAccount2.GetRangeMin("Date Filter") - 1)); + GLAccount2.CalcFields("Balance at Date", "Add.-Currency Balance at Date", "Debit Amount", "Credit Amount", "Add.-Currency Debit Amount", "Add.-Currency Credit Amount"); + TrialBalanceData."Starting Balance" := GLAccount2."Balance at Date"; + TrialBalanceData."Starting Balance (Debit)" := GLAccount2."Debit Amount"; + TrialBalanceData."Starting Balance (Credit)" := GLAccount2."Credit Amount"; + TrialBalanceData."Starting Balance (ACY)" := GLAccount2."Add.-Currency Balance at Date"; + TrialBalanceData."Starting Balance (Debit) (ACY)" := GLAccount2."Add.-Currency Debit Amount"; + TrialBalanceData."Starting Balance (Credit)(ACY)" := GLAccount2."Add.-Currency Credit Amount"; end; - GlAccount.CalcFields("Net Change", "Balance at Date", "Additional-Currency Net Change", "Add.-Currency Balance at Date", "Budgeted Amount", "Budget at Date"); + GlAccount.CalcFields("Net Change", "Balance at Date", "Additional-Currency Net Change", "Add.-Currency Balance at Date", "Budgeted Amount", "Budget at Date", "Debit Amount", "Credit Amount", "Add.-Currency Debit Amount", "Add.-Currency Credit Amount"); TrialBalanceData."G/L Account No." := GlAccount."No."; TrialBalanceData."Dimension 1 Code" := Dimension1ValueCode; TrialBalanceData."Dimension 2 Code" := Dimension2ValueCode; TrialBalanceData."Business Unit Code" := BusinessUnitCode; - TrialBalanceData.Validate("Net Change", GLAccount."Net Change"); - TrialBalanceData.Validate(Balance, GLAccount."Balance at Date"); - TrialBalanceData.Validate("Net Change (ACY)", GLAccount."Additional-Currency Net Change"); - TrialBalanceData.Validate("Balance (ACY)", GLAccount."Add.-Currency Balance at Date"); + TrialBalanceData."Net Change" := GLAccount."Net Change"; + TrialBalanceData."Net Change (Debit)" := GLAccount."Debit Amount"; + TrialBalanceData."Net Change (Credit)" := GLAccount."Credit Amount"; + TrialBalanceData.Balance := GLAccount."Balance at Date"; + TrialBalanceData."Balance (Debit)" := TrialBalanceData."Starting Balance (Debit)" + TrialBalanceData."Net Change (Debit)"; + TrialBalanceData."Balance (Credit)" := TrialBalanceData."Starting Balance (Credit)" + TrialBalanceData."Net Change (Credit)"; + TrialBalanceData."Net Change (ACY)" := GLAccount."Additional-Currency Net Change"; + TrialBalanceData."Net Change (Debit) (ACY)" := GLAccount."Add.-Currency Debit Amount"; + TrialBalanceData."Net Change (Credit) (ACY)" := GLAccount."Add.-Currency Credit Amount"; + TrialBalanceData."Balance (ACY)" := GLAccount."Add.-Currency Balance at Date"; + TrialBalanceData."Balance (Debit) (ACY)" := TrialBalanceData."Starting Balance (Debit) (ACY)" + TrialBalanceData."Net Change (Debit) (ACY)"; + TrialBalanceData."Balance (Credit) (ACY)" := TrialBalanceData."Starting Balance (Credit)(ACY)" + TrialBalanceData."Net Change (Credit) (ACY)"; TrialBalanceData.Validate("Budget (Net)", GLAccount."Budgeted Amount"); TrialBalanceData.Validate("Budget (Bal. at Date)", GLAccount."Budget at Date"); TrialBalanceData.CalculateBudgetComparisons(); @@ -229,11 +236,20 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Dimension 1 Code" := EXRTrialBalanceQuery.DimensionValue1Code; TrialBalanceData."Dimension 2 Code" := EXRTrialBalanceQuery.DimensionValue2Code; // The balances at the ending date are filled in from the values returned in this query - TrialBalanceData.Validate(Balance, EXRTrialBalanceQuery.Amount); - TrialBalanceData.Validate("Balance (ACY)", EXRTrialBalanceQuery.ACYAmount); - // And also in Net Change (which will have later the value at the starting date subtracted) - TrialBalanceData.Validate("Net Change", EXRTrialBalanceQuery.Amount); - TrialBalanceData.Validate("Net Change (ACY)", EXRTrialBalanceQuery.ACYAmount); + TrialBalanceData.Balance := EXRTrialBalanceQuery.Amount; + TrialBalanceData."Balance (Debit)" := EXRTrialBalanceQuery.DebitAmount; + TrialBalanceData."Balance (Credit)" := EXRTrialBalanceQuery.CreditAmount; + TrialBalanceData."Balance (ACY)" := EXRTrialBalanceQuery.ACYAmount; + TrialBalanceData."Balance (Debit) (ACY)" := EXRTrialBalanceQuery.ACYDebitAmount; + TrialBalanceData."Balance (Credit) (ACY)" := EXRTrialBalanceQuery.ACYCreditAmount; + // Net Change fields temporarily hold cumulative values up to the ending date, + // the starting date values will be subtracted in the second query + TrialBalanceData."Net Change" := EXRTrialBalanceQuery.Amount; + TrialBalanceData."Net Change (Debit)" := EXRTrialBalanceQuery.DebitAmount; + TrialBalanceData."Net Change (Credit)" := EXRTrialBalanceQuery.CreditAmount; + TrialBalanceData."Net Change (ACY)" := EXRTrialBalanceQuery.ACYAmount; + TrialBalanceData."Net Change (Debit) (ACY)" := EXRTrialBalanceQuery.ACYDebitAmount; + TrialBalanceData."Net Change (Credit) (ACY)" := EXRTrialBalanceQuery.ACYCreditAmount; TrialBalanceData.CheckAllZero(); if not TrialBalanceData."All Zero" then begin TrialBalanceData.Insert(true); @@ -244,7 +260,7 @@ codeunit 4410 "Trial Balance" EXRTrialBalanceQuery.Close(); // And now we get the balances at the starting date and modify the ones we have already inserted - EXRTrialBalanceQuery.SetFilter(EXRTrialBalanceQuery.PostingDate, '..%1', StartDate - 1); + EXRTrialBalanceQuery.SetFilter(EXRTrialBalanceQuery.PostingDate, '..%1', ClosingDate(StartDate - 1)); EXRTrialBalanceQuery.Open(); while EXRTrialBalanceQuery.Read() do begin TrialBalanceData.SetRange("G/L Account No.", EXRTrialBalanceQuery.AccountNumber); @@ -257,11 +273,19 @@ codeunit 4410 "Trial Balance" TrialBalanceData.Insert(true); end; // The balances at starting date are filled in from the values returned in this query - TrialBalanceData.Validate("Starting Balance", EXRTrialBalanceQuery.Amount); - TrialBalanceData.Validate("Starting Balance (ACY)", EXRTrialBalanceQuery.ACYAmount); - // The "Net Change" will be modified from what it had (balance at ending date) to the subtraction with the starting balance - TrialBalanceData.Validate("Net Change", TrialBalanceData."Net Change" - EXRTrialBalanceQuery.Amount); - TrialBalanceData.Validate("Net Change (ACY)", TrialBalanceData."Net Change (ACY)" - EXRTrialBalanceQuery.ACYAmount); + TrialBalanceData."Starting Balance" := EXRTrialBalanceQuery.Amount; + TrialBalanceData."Starting Balance (Debit)" := EXRTrialBalanceQuery.DebitAmount; + TrialBalanceData."Starting Balance (Credit)" := EXRTrialBalanceQuery.CreditAmount; + TrialBalanceData."Starting Balance (ACY)" := EXRTrialBalanceQuery.ACYAmount; + TrialBalanceData."Starting Balance (Debit) (ACY)" := EXRTrialBalanceQuery.ACYDebitAmount; + TrialBalanceData."Starting Balance (Credit)(ACY)" := EXRTrialBalanceQuery.ACYCreditAmount; + // Subtract cumulative values at the starting date to get the period net change (gross debit and credit) + TrialBalanceData."Net Change" := TrialBalanceData."Net Change" - EXRTrialBalanceQuery.Amount; + TrialBalanceData."Net Change (Debit)" := TrialBalanceData."Net Change (Debit)" - EXRTrialBalanceQuery.DebitAmount; + TrialBalanceData."Net Change (Credit)" := TrialBalanceData."Net Change (Credit)" - EXRTrialBalanceQuery.CreditAmount; + TrialBalanceData."Net Change (ACY)" := TrialBalanceData."Net Change (ACY)" - EXRTrialBalanceQuery.ACYAmount; + TrialBalanceData."Net Change (Debit) (ACY)" := TrialBalanceData."Net Change (Debit) (ACY)" - EXRTrialBalanceQuery.ACYDebitAmount; + TrialBalanceData."Net Change (Credit) (ACY)" := TrialBalanceData."Net Change (Credit) (ACY)" - EXRTrialBalanceQuery.ACYCreditAmount; TrialBalanceData.Modify(); InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); @@ -282,10 +306,18 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Dimension 1 Code" := EXRTrialBalanceBUQuery.DimensionValue1Code; TrialBalanceData."Dimension 2 Code" := EXRTrialBalanceBUQuery.DimensionValue2Code; TrialBalanceData."Business Unit Code" := EXRTrialBalanceBUQuery.BusinessUnitCode; - TrialBalanceData.Validate(Balance, EXRTrialBalanceBUQuery.Amount); - TrialBalanceData.Validate("Balance (ACY)", EXRTrialBalanceBUQuery.ACYAmount); - TrialBalanceData.Validate("Net Change", EXRTrialBalanceBUQuery.Amount); - TrialBalanceData.Validate("Net Change (ACY)", EXRTrialBalanceBUQuery.ACYAmount); + TrialBalanceData.Balance := EXRTrialBalanceBUQuery.Amount; + TrialBalanceData."Balance (Debit)" := EXRTrialBalanceBUQuery.DebitAmount; + TrialBalanceData."Balance (Credit)" := EXRTrialBalanceBUQuery.CreditAmount; + TrialBalanceData."Balance (ACY)" := EXRTrialBalanceBUQuery.ACYAmount; + TrialBalanceData."Balance (Debit) (ACY)" := EXRTrialBalanceBUQuery.ACYDebitAmount; + TrialBalanceData."Balance (Credit) (ACY)" := EXRTrialBalanceBUQuery.ACYCreditAmount; + TrialBalanceData."Net Change" := EXRTrialBalanceBUQuery.Amount; + TrialBalanceData."Net Change (Debit)" := EXRTrialBalanceBUQuery.DebitAmount; + TrialBalanceData."Net Change (Credit)" := EXRTrialBalanceBUQuery.CreditAmount; + TrialBalanceData."Net Change (ACY)" := EXRTrialBalanceBUQuery.ACYAmount; + TrialBalanceData."Net Change (Debit) (ACY)" := EXRTrialBalanceBUQuery.ACYDebitAmount; + TrialBalanceData."Net Change (Credit) (ACY)" := EXRTrialBalanceBUQuery.ACYCreditAmount; TrialBalanceData.CheckAllZero(); if not TrialBalanceData."All Zero" then begin TrialBalanceData.Insert(true); @@ -296,7 +328,7 @@ codeunit 4410 "Trial Balance" EXRTrialBalanceBUQuery.Close(); // And now we get the balances at the starting date and modify the ones we have already inserted - EXRTrialBalanceBUQuery.SetFilter(EXRTrialBalanceBUQuery.PostingDate, '..%1', StartDate - 1); + EXRTrialBalanceBUQuery.SetFilter(EXRTrialBalanceBUQuery.PostingDate, '..%1', ClosingDate(StartDate - 1)); EXRTrialBalanceBUQuery.Open(); while EXRTrialBalanceBUQuery.Read() do begin TrialBalanceData.SetRange("G/L Account No.", EXRTrialBalanceBUQuery.AccountNumber); @@ -310,10 +342,18 @@ codeunit 4410 "Trial Balance" TrialBalanceData."Business Unit Code" := EXRTrialBalanceBUQuery.BusinessUnitCode; TrialBalanceData.Insert(true); end; - TrialBalanceData.Validate("Starting Balance", EXRTrialBalanceBUQuery.Amount); - TrialBalanceData.Validate("Starting Balance (ACY)", EXRTrialBalanceBUQuery.ACYAmount); - TrialBalanceData.Validate("Net Change", TrialBalanceData."Net Change" - EXRTrialBalanceBUQuery.Amount); - TrialBalanceData.Validate("Net Change (ACY)", TrialBalanceData."Net Change (ACY)" - EXRTrialBalanceBUQuery.ACYAmount); + TrialBalanceData."Starting Balance" := EXRTrialBalanceBUQuery.Amount; + TrialBalanceData."Starting Balance (Debit)" := EXRTrialBalanceBUQuery.DebitAmount; + TrialBalanceData."Starting Balance (Credit)" := EXRTrialBalanceBUQuery.CreditAmount; + TrialBalanceData."Starting Balance (ACY)" := EXRTrialBalanceBUQuery.ACYAmount; + TrialBalanceData."Starting Balance (Debit) (ACY)" := EXRTrialBalanceBUQuery.ACYDebitAmount; + TrialBalanceData."Starting Balance (Credit)(ACY)" := EXRTrialBalanceBUQuery.ACYCreditAmount; + TrialBalanceData."Net Change" := TrialBalanceData."Net Change" - EXRTrialBalanceBUQuery.Amount; + TrialBalanceData."Net Change (Debit)" := TrialBalanceData."Net Change (Debit)" - EXRTrialBalanceBUQuery.DebitAmount; + TrialBalanceData."Net Change (Credit)" := TrialBalanceData."Net Change (Credit)" - EXRTrialBalanceBUQuery.CreditAmount; + TrialBalanceData."Net Change (ACY)" := TrialBalanceData."Net Change (ACY)" - EXRTrialBalanceBUQuery.ACYAmount; + TrialBalanceData."Net Change (Debit) (ACY)" := TrialBalanceData."Net Change (Debit) (ACY)" - EXRTrialBalanceBUQuery.ACYDebitAmount; + TrialBalanceData."Net Change (Credit) (ACY)" := TrialBalanceData."Net Change (Credit) (ACY)" - EXRTrialBalanceBUQuery.ACYCreditAmount; TrialBalanceData.Modify(); InsertUsedDimensionValue(1, TrialBalanceData."Dimension 1 Code", Dimension1Values); InsertUsedDimensionValue(2, TrialBalanceData."Dimension 2 Code", Dimension2Values); @@ -441,16 +481,11 @@ codeunit 4410 "Trial Balance" local procedure GetRangeDatesForGLAccountFilter(GLAccountDateFilter: Text; var StartDate: Date; var EndDate: Date) var - GLEntry: Record "G/L Entry"; + GLAccount: Record "G/L Account"; begin - GLEntry.SetFilter("Posting Date", GLAccountDateFilter); - GLEntry.SetLoadFields("Posting Date"); - GLEntry.SetCurrentKey("Posting Date"); - GLEntry.SetAscending("Posting Date", true); - if GLEntry.FindFirst() then - StartDate := GLEntry."Posting Date"; - if GLEntry.FindLast() then - EndDate := GLEntry."Posting Date"; + GLAccount.SetFilter("Date Filter", GLAccountDateFilter); + StartDate := GLAccount.GetRangeMin("Date Filter"); + EndDate := GLAccount.GetRangeMax("Date Filter"); if StartDate = 0D then StartDate := WorkDate(); if EndDate = 0D then diff --git a/src/Apps/W1/ExcelReports/App/src/Vendor/EXRVendorTopList.Report.al b/src/Apps/W1/ExcelReports/App/src/Vendor/EXRVendorTopList.Report.al index acb5730752..2f8599d6cb 100644 --- a/src/Apps/W1/ExcelReports/App/src/Vendor/EXRVendorTopList.Report.al +++ b/src/Apps/W1/ExcelReports/App/src/Vendor/EXRVendorTopList.Report.al @@ -131,7 +131,9 @@ report 4404 "EXR Vendor Top List" DateFilter: Text; protected var +#pragma warning disable AA0073 GlobalExtTopVendorReportBuffer: Record "EXR Top Vendor Report Buffer"; +#pragma warning restore AA0073 EXTTopVendorCaptionHandler: Codeunit "EXT Top Vendor Caption Handler"; NoOfRecordsToPrint: Integer; diff --git a/src/Apps/W1/ExcelReports/App/src/Vendor/ExtTopVendorCaptionHandler.Codeunit.al b/src/Apps/W1/ExcelReports/App/src/Vendor/ExtTopVendorCaptionHandler.Codeunit.al index 94e3bb1142..bf92ebc70b 100644 --- a/src/Apps/W1/ExcelReports/App/src/Vendor/ExtTopVendorCaptionHandler.Codeunit.al +++ b/src/Apps/W1/ExcelReports/App/src/Vendor/ExtTopVendorCaptionHandler.Codeunit.al @@ -13,7 +13,7 @@ codeunit 4404 "EXT Top Vendor Caption Handler" [EventSubscriber(ObjectType::Table, Database::"EXR Top Vendor Report Buffer", 'OnGetAmount1Caption', '', false, false)] local procedure GetAmount1Caption(var NewCaption: Text; var Handled: Boolean) begin - if (EXTTopReportBuffer."Ranking Based On" = EXTTopReportBuffer."Ranking Based On"::"Balance (LCY)") then begin + if (TempEXTTopReportBuffer."Ranking Based On" = TempEXTTopReportBuffer."Ranking Based On"::"Balance (LCY)") then begin NewCaption := BalanceLCYTok; Handled := true; exit; @@ -26,7 +26,7 @@ codeunit 4404 "EXT Top Vendor Caption Handler" [EventSubscriber(ObjectType::Table, Database::"EXR Top Vendor Report Buffer", 'OnGetAmount2Caption', '', false, false)] local procedure GetAmount2Caption(var NewCaption: Text; var Handled: Boolean) begin - if EXTTopReportBuffer."Ranking Based On" <> EXTTopReportBuffer."Ranking Based On"::"Balance (LCY)" then begin + if TempEXTTopReportBuffer."Ranking Based On" <> TempEXTTopReportBuffer."Ranking Based On"::"Balance (LCY)" then begin NewCaption := BalanceLCYTok; Handled := true; exit; @@ -38,11 +38,11 @@ codeunit 4404 "EXT Top Vendor Caption Handler" internal procedure SetRankingBasedOn(NewRankingBasedOn: Option) begin - EXTTopReportBuffer."Ranking Based On" := NewRankingBasedOn; + TempEXTTopReportBuffer."Ranking Based On" := NewRankingBasedOn; end; var - EXTTopReportBuffer: Record "EXR Top Vendor Report Buffer"; + TempEXTTopReportBuffer: Record "EXR Top Vendor Report Buffer"; BalanceLCYTok: Label 'Balance (LCY)'; PurchasesLCYTok: Label 'Purchases (LCY)'; diff --git a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al index e30dfedd30..799760754a 100644 --- a/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al +++ b/src/Apps/W1/ExcelReports/Test/src/TrialBalanceExcelReports.Codeunit.al @@ -10,7 +10,12 @@ using Microsoft.Finance.Dimension; using Microsoft.Finance.ExcelReports; using Microsoft.Finance.GeneralLedger.Account; using Microsoft.Finance.GeneralLedger.Budget; +using Microsoft.Finance.GeneralLedger.Journal; using Microsoft.Finance.GeneralLedger.Ledger; +using Microsoft.Purchases.Payables; +using Microsoft.Purchases.Vendor; +using Microsoft.Sales.Customer; +using Microsoft.Sales.Receivables; codeunit 139544 "Trial Balance Excel Reports" { @@ -21,8 +26,11 @@ codeunit 139544 "Trial Balance Excel Reports" var LibraryERM: Codeunit "Library - ERM"; + LibraryRandom: Codeunit "Library - Random"; LibraryReportDataset: Codeunit "Library - Report Dataset"; Assert: Codeunit Assert; + DocumentTypeShouldBeInvoiceErr: Label 'Document Type should be Invoice'; + DocumentNoShouldMatchErr: Label 'Document No should match the ledger entry'; [Test] [HandlerFunctions('EXRTrialBalanceExcelHandler')] @@ -46,6 +54,33 @@ codeunit 139544 "Trial Balance Excel Reports" Assert.AreEqual(5, LibraryReportDataset.RowCount(), 'The exported items should be GLAccounts'); end; + [Test] + [HandlerFunctions('EXRTrialBalanceHideNoActivityHandler')] + procedure TrialBalanceHidesZeroActivityAccounts() + var + GLAccount: Record "G/L Account"; + Variant: Variant; + RequestPageXml: Text; + ActiveAccountNo: Code[20]; + begin + // [SCENARIO] With Hide Accounts with No Activity enabled, only accounts with activity are exported + // [GIVEN] 5 G/L Accounts, only 1 with activity + Initialize(); + CreateSampleGLAccounts(5, GLAccount); + ActiveAccountNo := GLAccount."No."; + CreateGLEntryWithAmount(ActiveAccountNo, '', '', '', WorkDate(), 100); + Commit(); + // [WHEN] Running the report with Hide Accounts with No Activity enabled + RequestPageXml := Report.RunRequestPage(Report::"EXR Trial Balance Excel", RequestPageXml); + LibraryReportDataset.RunReportAndLoad(Report::"EXR Trial Balance Excel", Variant, RequestPageXml); + // [THEN] Only the active account should be exported + LibraryReportDataset.SetXmlNodeList('DataItem[@name="GLAccounts"]'); + Assert.AreEqual(1, LibraryReportDataset.RowCount(), 'Only the account with activity should be exported'); + LibraryReportDataset.GetNextRow(); + LibraryReportDataset.FindCurrentRowValue('AccountNumber', Variant); + Assert.AreEqual(ActiveAccountNo, Format(Variant), 'The exported account should be the one with activity'); + end; + [Test] [HandlerFunctions('EXRTrialBalanceBudgetExcelHandler')] procedure TrialBalanceBudgetExportsAsManyItemsAsGLAccounts() @@ -306,74 +341,12 @@ codeunit 139544 "Trial Balance Excel Reports" asserterror LibraryReportDataset.RunReportAndLoad(Report::"EXR Consolidated Trial Balance", Variant, RequestPageXml); end; - [Test] - procedure TrialBalanceBufferNetChangeSplitsIntoDebitAndCreditWhenCalledSeveralTimes() - var - EXRTrialBalanceBuffer: Record "EXR Trial Balance Buffer"; - ValuesToSplitInCreditAndDebit: array[3] of Decimal; - begin - // [SCENARIO 547558] Trial Balance Buffer data split into Debit and Credit correctly, even if called multiple times. - // [GIVEN] Trial Balance Buffer filled with positive Balance/Net Change - ValuesToSplitInCreditAndDebit[1] := 837; - // [GIVEN] Trial Balance Buffer filled with negative Balance/Net Change - ValuesToSplitInCreditAndDebit[2] := -110; - // [GIVEN] Trial Balance Buffer filled with positive Balance/Net Change - ValuesToSplitInCreditAndDebit[3] := 998; - // [WHEN] Trial Balance Buffer entries are inserted - EXRTrialBalanceBuffer."G/L Account No." := 'A'; - EXRTrialBalanceBuffer.Validate("Starting Balance", ValuesToSplitInCreditAndDebit[1]); - EXRTrialBalanceBuffer.Validate("Net Change", ValuesToSplitInCreditAndDebit[1]); - EXRTrialBalanceBuffer.Validate(Balance, ValuesToSplitInCreditAndDebit[1]); - EXRTrialBalanceBuffer.Validate("Starting Balance (ACY)", ValuesToSplitInCreditAndDebit[1]); - EXRTrialBalanceBuffer.Validate("Net Change (ACY)", ValuesToSplitInCreditAndDebit[1]); - EXRTrialBalanceBuffer.Validate("Balance (ACY)", ValuesToSplitInCreditAndDebit[1]); - EXRTrialBalanceBuffer.Insert(); - EXRTrialBalanceBuffer."G/L Account No." := 'B'; - EXRTrialBalanceBuffer.Validate("Starting Balance", ValuesToSplitInCreditAndDebit[2]); - EXRTrialBalanceBuffer.Validate("Net Change", ValuesToSplitInCreditAndDebit[2]); - EXRTrialBalanceBuffer.Validate(Balance, ValuesToSplitInCreditAndDebit[2]); - EXRTrialBalanceBuffer.Validate("Starting Balance (ACY)", ValuesToSplitInCreditAndDebit[2]); - EXRTrialBalanceBuffer.Validate("Net Change (ACY)", ValuesToSplitInCreditAndDebit[2]); - EXRTrialBalanceBuffer.Validate("Balance (ACY)", ValuesToSplitInCreditAndDebit[2]); - EXRTrialBalanceBuffer.Insert(); - EXRTrialBalanceBuffer."G/L Account No." := 'C'; - EXRTrialBalanceBuffer.Validate("Starting Balance", ValuesToSplitInCreditAndDebit[3]); - EXRTrialBalanceBuffer.Validate("Net Change", ValuesToSplitInCreditAndDebit[3]); - EXRTrialBalanceBuffer.Validate(Balance, ValuesToSplitInCreditAndDebit[3]); - EXRTrialBalanceBuffer.Validate("Starting Balance (ACY)", ValuesToSplitInCreditAndDebit[3]); - EXRTrialBalanceBuffer.Validate("Net Change (ACY)", ValuesToSplitInCreditAndDebit[3]); - EXRTrialBalanceBuffer.Validate("Balance (ACY)", ValuesToSplitInCreditAndDebit[3]); - EXRTrialBalanceBuffer.Insert(); - // [THEN] All Entries have the right split in Credit and Debit - EXRTrialBalanceBuffer.FindSet(); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[1], Abs(EXRTrialBalanceBuffer."Starting Balance (Debit)" + EXRTrialBalanceBuffer."Starting Balance (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[1], Abs(EXRTrialBalanceBuffer."Net Change (Debit)" + EXRTrialBalanceBuffer."Net Change (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[1], Abs(EXRTrialBalanceBuffer."Balance (Debit)" + EXRTrialBalanceBuffer."Balance (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[1], Abs(EXRTrialBalanceBuffer."Starting Balance (Debit) (ACY)" + EXRTrialBalanceBuffer."Starting Balance (Credit)(ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[1], Abs(EXRTrialBalanceBuffer."Net Change (Debit) (ACY)" + EXRTrialBalanceBuffer."Net Change (Credit) (ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[1], Abs(EXRTrialBalanceBuffer."Balance (Debit) (ACY)" + EXRTrialBalanceBuffer."Balance (Credit) (ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - EXRTrialBalanceBuffer.Next(); - Assert.AreEqual(-ValuesToSplitInCreditAndDebit[2], Abs(EXRTrialBalanceBuffer."Starting Balance (Debit)" + EXRTrialBalanceBuffer."Starting Balance (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(-ValuesToSplitInCreditAndDebit[2], Abs(EXRTrialBalanceBuffer."Net Change (Debit)" + EXRTrialBalanceBuffer."Net Change (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(-ValuesToSplitInCreditAndDebit[2], Abs(EXRTrialBalanceBuffer."Balance (Debit)" + EXRTrialBalanceBuffer."Balance (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(-ValuesToSplitInCreditAndDebit[2], Abs(EXRTrialBalanceBuffer."Starting Balance (Debit) (ACY)" + EXRTrialBalanceBuffer."Starting Balance (Credit)(ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(-ValuesToSplitInCreditAndDebit[2], Abs(EXRTrialBalanceBuffer."Net Change (Debit) (ACY)" + EXRTrialBalanceBuffer."Net Change (Credit) (ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(-ValuesToSplitInCreditAndDebit[2], Abs(EXRTrialBalanceBuffer."Balance (Debit) (ACY)" + EXRTrialBalanceBuffer."Balance (Credit) (ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - EXRTrialBalanceBuffer.Next(); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[3], Abs(EXRTrialBalanceBuffer."Starting Balance (Debit)" + EXRTrialBalanceBuffer."Starting Balance (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[3], Abs(EXRTrialBalanceBuffer."Net Change (Debit)" + EXRTrialBalanceBuffer."Net Change (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[3], Abs(EXRTrialBalanceBuffer."Balance (Debit)" + EXRTrialBalanceBuffer."Balance (Credit)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[3], Abs(EXRTrialBalanceBuffer."Starting Balance (Debit) (ACY)" + EXRTrialBalanceBuffer."Starting Balance (Credit)(ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[3], Abs(EXRTrialBalanceBuffer."Net Change (Debit) (ACY)" + EXRTrialBalanceBuffer."Net Change (Credit) (ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - Assert.AreEqual(ValuesToSplitInCreditAndDebit[3], Abs(EXRTrialBalanceBuffer."Balance (Debit) (ACY)" + EXRTrialBalanceBuffer."Balance (Credit) (ACY)"), 'Split in line in credit and debit should be the same as the inserted value.'); - end; - [Test] procedure QueryPathProducesCorrectAmounts() var GLAccount: Record "G/L Account"; TempDimensionValue: Record "Dimension Value" temporary; - TrialBalanceData: Record "EXR Trial Balance Buffer"; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; PostingAccount: Code[20]; BeforePeriodAmount: Decimal; @@ -393,14 +366,49 @@ codeunit 139544 "Trial Balance Excel Reports" GLAccount.SetRange("No.", PostingAccount); GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); TrialBalance.ConfigureTrialBalance(false, false); - TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimensionValue, TempDimensionValue, TrialBalanceData); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimensionValue, TempDimensionValue, TempTrialBalanceData); // [THEN] The buffer has correct amounts - TrialBalanceData.SetRange("G/L Account No.", PostingAccount); - Assert.IsTrue(TrialBalanceData.FindFirst(), 'Buffer record should exist for the posting account'); - Assert.AreEqual(BeforePeriodAmount, TrialBalanceData."Starting Balance", 'Starting Balance should equal the entry before the period'); - Assert.AreEqual(InPeriodAmount, TrialBalanceData."Net Change", 'Net Change should equal the entry within the period'); - Assert.AreEqual(BeforePeriodAmount + InPeriodAmount, TrialBalanceData.Balance, 'Balance should equal Starting Balance + Net Change'); + TempTrialBalanceData.SetRange("G/L Account No.", PostingAccount); + Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist for the posting account'); + Assert.AreEqual(BeforePeriodAmount, TempTrialBalanceData."Starting Balance", 'Starting Balance should equal the entry before the period'); + Assert.AreEqual(InPeriodAmount, TempTrialBalanceData."Net Change", 'Net Change should equal the entry within the period'); + Assert.AreEqual(BeforePeriodAmount + InPeriodAmount, TempTrialBalanceData.Balance, 'Balance should equal Starting Balance + Net Change'); + end; + + [Test] + procedure GrossDebitAndCreditTurnoverReportedForEachAccount() + var + GLAccount: Record "G/L Account"; + TempDimensionValue: Record "Dimension Value" temporary; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; + TrialBalance: Codeunit "Trial Balance"; + PostingAccount: Code[20]; + DebitAmount: Decimal; + CreditAmount: Decimal; + begin + // [SCENARIO] The query path produces gross debit and credit turnover, not netted amounts. + // [GIVEN] A posting account with both debit and credit entries in the same period + Initialize(); + CreateGLAccount(GLAccount); + PostingAccount := GLAccount."No."; + DebitAmount := 5000; + CreditAmount := -8000; + CreateGLEntryWithAmount(PostingAccount, '', '', '', DMY2Date(1, 3, Date2DMY(WorkDate(), 3)), DebitAmount); + CreateGLEntryWithAmount(PostingAccount, '', '', '', DMY2Date(15, 3, Date2DMY(WorkDate(), 3)), CreditAmount); + + // [WHEN] Running the query-based trial balance for the current year + GLAccount.SetRange("No.", PostingAccount); + GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); + TrialBalance.ConfigureTrialBalance(false, false); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimensionValue, TempDimensionValue, TempTrialBalanceData); + + // [THEN] The buffer has gross debit and credit amounts, not netted + TempTrialBalanceData.SetRange("G/L Account No.", PostingAccount); + Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist for the posting account'); + Assert.AreEqual(DebitAmount + CreditAmount, TempTrialBalanceData."Net Change", 'Net Change should be the algebraic sum'); + Assert.AreEqual(DebitAmount, TempTrialBalanceData."Net Change (Debit)", 'Net Change (Debit) should be the gross debit amount'); + Assert.AreEqual(-CreditAmount, TempTrialBalanceData."Net Change (Credit)", 'Net Change (Credit) should be the gross credit amount'); end; [Test] @@ -410,7 +418,7 @@ codeunit 139544 "Trial Balance Excel Reports" Dimension: Record Dimension; DimensionValue1, DimensionValue2 : Record "Dimension Value"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; - TrialBalanceData: Record "EXR Trial Balance Buffer"; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; Amount1Dim1, Amount2Dim1, Amount1Dim2 : Decimal; begin @@ -444,29 +452,29 @@ codeunit 139544 "Trial Balance Excel Reports" // [WHEN] Running the trial balance for the current year GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); TrialBalance.ConfigureTrialBalance(false, false); - TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TrialBalanceData); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); // [THEN] End-Total has per-dimension rows with correct sums - TrialBalanceData.Reset(); - TrialBalanceData.SetRange("G/L Account No.", EndTotalAccount."No."); - Assert.AreEqual(2, TrialBalanceData.Count(), 'End-Total should have 2 rows (one per Dim2 value)'); + TempTrialBalanceData.Reset(); + TempTrialBalanceData.SetRange("G/L Account No.", EndTotalAccount."No."); + Assert.AreEqual(2, TempTrialBalanceData.Count(), 'End-Total should have 2 rows (one per Dim2 value)'); - TrialBalanceData.SetRange("Dimension 2 Code", DimensionValue1.Code); - TrialBalanceData.FindFirst(); - Assert.AreEqual(Amount1Dim1 + Amount2Dim1, TrialBalanceData.Balance, 'End-Total Dim2=Value1 should sum both posting accounts'); + TempTrialBalanceData.SetRange("Dimension 2 Code", DimensionValue1.Code); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(Amount1Dim1 + Amount2Dim1, TempTrialBalanceData.Balance, 'End-Total Dim2=Value1 should sum both posting accounts'); - TrialBalanceData.SetRange("Dimension 2 Code", DimensionValue2.Code); - TrialBalanceData.FindFirst(); - Assert.AreEqual(Amount1Dim2, TrialBalanceData.Balance, 'End-Total Dim2=Value2 should have only Account1 amount'); + TempTrialBalanceData.SetRange("Dimension 2 Code", DimensionValue2.Code); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(Amount1Dim2, TempTrialBalanceData.Balance, 'End-Total Dim2=Value2 should have only Account1 amount'); // [THEN] Total account has identical per-dimension rows - TrialBalanceData.Reset(); - TrialBalanceData.SetRange("G/L Account No.", TotalAccount."No."); - Assert.AreEqual(2, TrialBalanceData.Count(), 'Total should have 2 rows (one per Dim2 value)'); + TempTrialBalanceData.Reset(); + TempTrialBalanceData.SetRange("G/L Account No.", TotalAccount."No."); + Assert.AreEqual(2, TempTrialBalanceData.Count(), 'Total should have 2 rows (one per Dim2 value)'); - TrialBalanceData.SetRange("Dimension 2 Code", DimensionValue1.Code); - TrialBalanceData.FindFirst(); - Assert.AreEqual(Amount1Dim1 + Amount2Dim1, TrialBalanceData.Balance, 'Total Dim2=Value1 should sum both posting accounts'); + TempTrialBalanceData.SetRange("Dimension 2 Code", DimensionValue1.Code); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(Amount1Dim1 + Amount2Dim1, TempTrialBalanceData.Balance, 'Total Dim2=Value1 should sum both posting accounts'); end; [Test] @@ -476,7 +484,7 @@ codeunit 139544 "Trial Balance Excel Reports" GLBudgetName: Record "G/L Budget Name"; GLBudgetEntry: Record "G/L Budget Entry"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; - TrialBalanceData: Record "EXR Trial Balance Buffer"; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; PostingAccount: Code[20]; EntryAmount, BudgetInPeriod, BudgetBeforePeriod : Decimal; @@ -507,13 +515,13 @@ codeunit 139544 "Trial Balance Excel Reports" GLAccount.SetRange("No.", PostingAccount); GLAccount.SetRange("Date Filter", PeriodStart, PeriodEnd); TrialBalance.ConfigureTrialBalance(false, true); - TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TrialBalanceData); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); // [THEN] Budget fields are populated - TrialBalanceData.SetRange("G/L Account No.", PostingAccount); - Assert.IsTrue(TrialBalanceData.FindFirst(), 'Buffer record should exist'); - Assert.AreEqual(BudgetInPeriod, TrialBalanceData."Budget (Net)", 'Budget (Net) should be the budget entry within the period'); - Assert.AreEqual(BudgetBeforePeriod + BudgetInPeriod, TrialBalanceData."Budget (Bal. at Date)", 'Budget (Bal. at Date) should be cumulative up to period end'); + TempTrialBalanceData.SetRange("G/L Account No.", PostingAccount); + Assert.IsTrue(TempTrialBalanceData.FindFirst(), 'Buffer record should exist'); + Assert.AreEqual(BudgetInPeriod, TempTrialBalanceData."Budget (Net)", 'Budget (Net) should be the budget entry within the period'); + Assert.AreEqual(BudgetBeforePeriod + BudgetInPeriod, TempTrialBalanceData."Budget (Bal. at Date)", 'Budget (Bal. at Date) should be cumulative up to period end'); end; [Test] @@ -522,7 +530,7 @@ codeunit 139544 "Trial Balance Excel Reports" GLAccount: Record "G/L Account"; BusinessUnit1, BusinessUnit2 : Record "Business Unit"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; - TrialBalanceData: Record "EXR Trial Balance Buffer"; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; PostingAccount: Code[20]; AmountBU1, AmountBU2 : Decimal; @@ -544,19 +552,19 @@ codeunit 139544 "Trial Balance Excel Reports" GLAccount.SetRange("No.", PostingAccount); GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); TrialBalance.ConfigureTrialBalance(true, false); - TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TrialBalanceData); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); // [THEN] Two buffer records exist, one per BU, with correct amounts - TrialBalanceData.SetRange("G/L Account No.", PostingAccount); - Assert.AreEqual(2, TrialBalanceData.Count(), 'Should have one row per Business Unit'); + TempTrialBalanceData.SetRange("G/L Account No.", PostingAccount); + Assert.AreEqual(2, TempTrialBalanceData.Count(), 'Should have one row per Business Unit'); - TrialBalanceData.SetRange("Business Unit Code", BusinessUnit1.Code); - TrialBalanceData.FindFirst(); - Assert.AreEqual(AmountBU1, TrialBalanceData.Balance, 'BU1 balance should match its entries'); + TempTrialBalanceData.SetRange("Business Unit Code", BusinessUnit1.Code); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(AmountBU1, TempTrialBalanceData.Balance, 'BU1 balance should match its entries'); - TrialBalanceData.SetRange("Business Unit Code", BusinessUnit2.Code); - TrialBalanceData.FindFirst(); - Assert.AreEqual(AmountBU2, TrialBalanceData.Balance, 'BU2 balance should match its entries'); + TempTrialBalanceData.SetRange("Business Unit Code", BusinessUnit2.Code); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(AmountBU2, TempTrialBalanceData.Balance, 'BU2 balance should match its entries'); end; [Test] @@ -564,7 +572,7 @@ codeunit 139544 "Trial Balance Excel Reports" var GLAccount1, GLAccount2, GLAccount3, GLAccount : Record "G/L Account"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; - TrialBalanceData: Record "EXR Trial Balance Buffer"; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; begin // [SCENARIO] The query path only returns data for accounts matching the No. filter. @@ -581,13 +589,13 @@ codeunit 139544 "Trial Balance Excel Reports" GLAccount.SetRange("No.", GLAccount2."No."); GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); TrialBalance.ConfigureTrialBalance(false, false); - TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TrialBalanceData); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); // [THEN] Only the filtered account appears in the buffer - Assert.AreEqual(1, TrialBalanceData.Count(), 'Only one account should be in the buffer'); - TrialBalanceData.FindFirst(); - Assert.AreEqual(GLAccount2."No.", TrialBalanceData."G/L Account No.", 'The filtered account should be the one returned'); - Assert.AreEqual(200, TrialBalanceData.Balance, 'Amount should match the filtered account entry'); + Assert.AreEqual(1, TempTrialBalanceData.Count(), 'Only one account should be in the buffer'); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(GLAccount2."No.", TempTrialBalanceData."G/L Account No.", 'The filtered account should be the one returned'); + Assert.AreEqual(200, TempTrialBalanceData.Balance, 'Amount should match the filtered account entry'); end; [Test] @@ -595,7 +603,7 @@ codeunit 139544 "Trial Balance Excel Reports" var GLAccount: Record "G/L Account"; TempDimension1Values, TempDimension2Values : Record "Dimension Value" temporary; - TrialBalanceData: Record "EXR Trial Balance Buffer"; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; TrialBalance: Codeunit "Trial Balance"; ZeroAccount, NonZeroAccount : Code[20]; begin @@ -615,12 +623,122 @@ codeunit 139544 "Trial Balance Excel Reports" GLAccount.SetFilter("No.", '%1|%2', ZeroAccount, NonZeroAccount); GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); TrialBalance.ConfigureTrialBalance(false, false); - TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TrialBalanceData); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimension1Values, TempDimension2Values, TempTrialBalanceData); // [THEN] Only the non-zero account appears - Assert.AreEqual(1, TrialBalanceData.Count(), 'Only the non-zero account should be in the buffer'); - TrialBalanceData.FindFirst(); - Assert.AreEqual(NonZeroAccount, TrialBalanceData."G/L Account No.", 'The non-zero account should be the one returned'); + Assert.AreEqual(1, TempTrialBalanceData.Count(), 'Only the non-zero account should be in the buffer'); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(NonZeroAccount, TempTrialBalanceData."G/L Account No.", 'The non-zero account should be the one returned'); + end; + + [Test] + procedure QueryPathStartingBalanceIncludesClosingDateEntries() + var + GLAccount: Record "G/L Account"; + TempDimensionValue: Record "Dimension Value" temporary; + TempTrialBalanceData: Record "EXR Trial Balance Buffer"; + TrialBalance: Codeunit "Trial Balance"; + PostingAccount: Code[20]; + ActivityAmount: Decimal; + PriorYear: Integer; + begin + // [SCENARIO] Starting Balance includes closing date entries from the prior fiscal year, emulating what "Close Income Statement" produces. + // [GIVEN] A posting account with activity during the prior year + Initialize(); + CreateGLAccount(GLAccount); + PostingAccount := GLAccount."No."; + PriorYear := Date2DMY(WorkDate(), 3) - 1; + ActivityAmount := 5000; + CreateGLEntryWithAmount(PostingAccount, '', '', '', DMY2Date(15, 6, PriorYear), ActivityAmount); + // [GIVEN] A closing entry on ClosingDate(31/12) that zeroes out the account (emulates Close Income Statement) + CreateGLEntryWithAmount(PostingAccount, '', '', '', ClosingDate(DMY2Date(31, 12, PriorYear)), -ActivityAmount); + // [GIVEN] An entry on the first day of the current year so the old FindFirst logic derives cutoff ..31/12 (normal date), which misses C31/12 + CreateGLEntryWithAmount(PostingAccount, '', '', '', DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), 100); + + // [WHEN] Running the trial balance for the current year + GLAccount.SetRange("No.", PostingAccount); + GLAccount.SetRange("Date Filter", DMY2Date(1, 1, Date2DMY(WorkDate(), 3)), DMY2Date(31, 12, Date2DMY(WorkDate(), 3))); + TrialBalance.ConfigureTrialBalance(false, false); + TrialBalance.InsertTrialBalanceReportData(GLAccount, TempDimensionValue, TempDimensionValue, TempTrialBalanceData); + + // [THEN] Starting Balance is zero because the closing entry zeroed out the account + TempTrialBalanceData.SetRange("G/L Account No.", PostingAccount); + TempTrialBalanceData.FindFirst(); + Assert.AreEqual(0, TempTrialBalanceData."Starting Balance", 'Starting Balance should be zero after closing entries') + end; + + [Test] + [HandlerFunctions('EXRAgedAccPayableExcelHandler')] + procedure AgedAccountsPayableExportsDocumentTypeAndNo() + var + Vendor: Record Vendor; + VendorLedgerEntry: Record "Vendor Ledger Entry"; + Variant: Variant; + RequestPageXml: Text; + ReportDocumentType: Text; + ReportDocumentNo: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO 622247] Aged Accounts Payable Excel report exports Document Type and Document No fields correctly for Invoice entries + InitializeAgingData(); + + // [GIVEN] Vendor "V" with an open vendor ledger entry of type Invoice + // Create vendor directly to avoid VAT posting setup requirements in some localizations + CreateMinimalVendor(Vendor); + CreateVendorLedgerEntry(VendorLedgerEntry, Vendor."No.", "Gen. Journal Document Type"::Invoice); + Commit(); + + // [WHEN] Running the Aged Accounts Payable Excel report + RequestPageXml := Report.RunRequestPage(Report::"EXR Aged Acc Payable Excel", RequestPageXml); + LibraryReportDataset.RunReportAndLoad(Report::"EXR Aged Acc Payable Excel", Variant, RequestPageXml); + + // [THEN] The exported data contains the Document Type "Invoice" and the correct Document No + LibraryReportDataset.SetXmlNodeList('DataItem[@name="AgingData"]'); + Assert.AreEqual(1, LibraryReportDataset.RowCount(), 'One aging entry should be exported'); + LibraryReportDataset.GetNextRow(); + LibraryReportDataset.FindCurrentRowValue('DocumentType', Variant); + ReportDocumentType := Variant; + Assert.AreEqual(Format("Gen. Journal Document Type"::Invoice), ReportDocumentType, DocumentTypeShouldBeInvoiceErr); + LibraryReportDataset.FindCurrentRowValue('DocumentNo', Variant); + ReportDocumentNo := Variant; + Assert.AreEqual(VendorLedgerEntry."Document No.", ReportDocumentNo, DocumentNoShouldMatchErr); + end; + + [Test] + [HandlerFunctions('EXRAgedAccountsRecExcelHandler')] + procedure AgedAccountsRecExportsDocumentTypeAndNo() + var + Customer: Record Customer; + CustLedgerEntry: Record "Cust. Ledger Entry"; + Variant: Variant; + RequestPageXml: Text; + ReportDocumentType: Text; + ReportDocumentNo: Text; + begin + // [FEATURE] [AI test] + // [SCENARIO 622247] Aged Accounts Receivable Excel report exports Document Type and Document No fields correctly for Invoice entries + InitializeAgingData(); + + // [GIVEN] Customer "C" with an open customer ledger entry of type Invoice + // Create customer directly to avoid VAT posting setup requirements in some localizations + CreateMinimalCustomer(Customer); + CreateCustLedgerEntry(CustLedgerEntry, Customer."No.", "Gen. Journal Document Type"::Invoice); + Commit(); + + // [WHEN] Running the Aged Accounts Receivable Excel report + RequestPageXml := Report.RunRequestPage(Report::"EXR Aged Accounts Rec Excel", RequestPageXml); + LibraryReportDataset.RunReportAndLoad(Report::"EXR Aged Accounts Rec Excel", Variant, RequestPageXml); + + // [THEN] The exported data contains the Document Type "Invoice" and the correct Document No + LibraryReportDataset.SetXmlNodeList('DataItem[@name="AgingData"]'); + Assert.AreEqual(1, LibraryReportDataset.RowCount(), 'One aging entry should be exported'); + LibraryReportDataset.GetNextRow(); + LibraryReportDataset.FindCurrentRowValue('DocumentType', Variant); + ReportDocumentType := Variant; + Assert.AreEqual(Format("Gen. Journal Document Type"::Invoice), ReportDocumentType, DocumentTypeShouldBeInvoiceErr); + LibraryReportDataset.FindCurrentRowValue('DocumentNo', Variant); + ReportDocumentNo := Variant; + Assert.AreEqual(CustLedgerEntry."Document No.", ReportDocumentNo, DocumentNoShouldMatchErr); end; local procedure CreateSampleBusinessUnits(HowMany: Integer) @@ -735,23 +853,139 @@ codeunit 139544 "Trial Balance Excel Reports" GLEntry."Business Unit Code" := BusinessUnitCode; GLEntry.Amount := Amount; GLEntry."Additional-Currency Amount" := Amount; - if Amount > 0 then - GLEntry."Debit Amount" := Amount - else + if Amount > 0 then begin + GLEntry."Debit Amount" := Amount; + GLEntry."Add.-Currency Debit Amount" := Amount; + end else begin GLEntry."Credit Amount" := -Amount; + GLEntry."Add.-Currency Credit Amount" := -Amount; + end; GLEntry."Posting Date" := PostingDate; GLEntry.Insert(); end; + local procedure InitializeAgingData() + var + Vendor: Record Vendor; + Customer: Record Customer; + VendorLedgerEntry: Record "Vendor Ledger Entry"; + CustLedgerEntry: Record "Cust. Ledger Entry"; + DetailedVendorLedgEntry: Record "Detailed Vendor Ledg. Entry"; + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + begin + DetailedVendorLedgEntry.DeleteAll(); + DetailedCustLedgEntry.DeleteAll(); + VendorLedgerEntry.DeleteAll(); + CustLedgerEntry.DeleteAll(); + Vendor.DeleteAll(); + Customer.DeleteAll(); + end; + + local procedure CreateMinimalVendor(var Vendor: Record Vendor) + begin + Vendor.Init(); + Vendor."No." := CopyStr(Format(CreateGuid()), 1, MaxStrLen(Vendor."No.")); + Vendor.Name := Vendor."No."; + Vendor.Insert(); + end; + + local procedure CreateMinimalCustomer(var Customer: Record Customer) + begin + Customer.Init(); + Customer."No." := CopyStr(Format(CreateGuid()), 1, MaxStrLen(Customer."No.")); + Customer.Name := Customer."No."; + Customer.Insert(); + end; + + local procedure CreateVendorLedgerEntry(var VendorLedgerEntry: Record "Vendor Ledger Entry"; VendorNo: Code[20]; DocumentType: Enum "Gen. Journal Document Type") + var + DetailedVendorLedgEntry: Record "Detailed Vendor Ledg. Entry"; + EntryNo: Integer; + Amount: Decimal; + begin + if VendorLedgerEntry.FindLast() then; + EntryNo := VendorLedgerEntry."Entry No." + 1; + + VendorLedgerEntry.Init(); + VendorLedgerEntry."Entry No." := EntryNo; + VendorLedgerEntry."Vendor No." := VendorNo; + VendorLedgerEntry."Vendor Name" := VendorNo; + VendorLedgerEntry."Document Type" := DocumentType; + VendorLedgerEntry."Document No." := 'DOC' + Format(EntryNo); + VendorLedgerEntry."Posting Date" := WorkDate(); + VendorLedgerEntry."Document Date" := WorkDate(); + VendorLedgerEntry."Due Date" := WorkDate() + 30; + VendorLedgerEntry.Open := true; + VendorLedgerEntry.Insert(); + + // Create detailed vendor ledger entry for remaining amount + Amount := -LibraryRandom.RandDec(1000, 2); + if DetailedVendorLedgEntry.FindLast() then; + DetailedVendorLedgEntry.Init(); + DetailedVendorLedgEntry."Entry No." := DetailedVendorLedgEntry."Entry No." + 1; + DetailedVendorLedgEntry."Vendor Ledger Entry No." := VendorLedgerEntry."Entry No."; + DetailedVendorLedgEntry."Vendor No." := VendorNo; + DetailedVendorLedgEntry."Posting Date" := WorkDate(); + DetailedVendorLedgEntry."Entry Type" := DetailedVendorLedgEntry."Entry Type"::"Initial Entry"; + DetailedVendorLedgEntry.Amount := Amount; + DetailedVendorLedgEntry."Amount (LCY)" := Amount; + DetailedVendorLedgEntry.Insert(); + end; + + local procedure CreateCustLedgerEntry(var CustLedgerEntry: Record "Cust. Ledger Entry"; CustomerNo: Code[20]; DocumentType: Enum "Gen. Journal Document Type") + var + DetailedCustLedgEntry: Record "Detailed Cust. Ledg. Entry"; + EntryNo: Integer; + Amount: Decimal; + begin + if CustLedgerEntry.FindLast() then; + EntryNo := CustLedgerEntry."Entry No." + 1; + + CustLedgerEntry.Init(); + CustLedgerEntry."Entry No." := EntryNo; + CustLedgerEntry."Customer No." := CustomerNo; + CustLedgerEntry."Customer Name" := CustomerNo; + CustLedgerEntry."Document Type" := DocumentType; + CustLedgerEntry."Document No." := 'DOC' + Format(EntryNo); + CustLedgerEntry."Posting Date" := WorkDate(); + CustLedgerEntry."Document Date" := WorkDate(); + CustLedgerEntry."Due Date" := WorkDate() + 30; + CustLedgerEntry.Open := true; + CustLedgerEntry.Insert(); + + // Create detailed customer ledger entry for remaining amount + Amount := LibraryRandom.RandDec(1000, 2); + if DetailedCustLedgEntry.FindLast() then; + DetailedCustLedgEntry.Init(); + DetailedCustLedgEntry."Entry No." := DetailedCustLedgEntry."Entry No." + 1; + DetailedCustLedgEntry."Cust. Ledger Entry No." := CustLedgerEntry."Entry No."; + DetailedCustLedgEntry."Customer No." := CustomerNo; + DetailedCustLedgEntry."Posting Date" := WorkDate(); + DetailedCustLedgEntry."Entry Type" := DetailedCustLedgEntry."Entry Type"::"Initial Entry"; + DetailedCustLedgEntry.Amount := Amount; + DetailedCustLedgEntry."Amount (LCY)" := Amount; + DetailedCustLedgEntry.Insert(); + end; + [RequestPageHandler] procedure EXRTrialBalanceExcelHandler(var EXRTrialBalanceExcel: TestRequestPage "EXR Trial Balance Excel") begin + EXRTrialBalanceExcel.GLAccounts.SetFilter("Date Filter", Format(DMY2Date(1, 1, Date2DMY(WorkDate(), 3))) + '..' + Format(DMY2Date(31, 12, Date2DMY(WorkDate(), 3)))); + EXRTrialBalanceExcel.OK().Invoke(); + end; + + [RequestPageHandler] + procedure EXRTrialBalanceHideNoActivityHandler(var EXRTrialBalanceExcel: TestRequestPage "EXR Trial Balance Excel") + begin + EXRTrialBalanceExcel.GLAccounts.SetFilter("Date Filter", Format(DMY2Date(1, 1, Date2DMY(WorkDate(), 3))) + '..' + Format(DMY2Date(31, 12, Date2DMY(WorkDate(), 3)))); + EXRTrialBalanceExcel.HideAccountsWithNoActivityField.SetValue(true); EXRTrialBalanceExcel.OK().Invoke(); end; [RequestPageHandler] procedure EXRTrialBalanceBudgetExcelHandler(var EXRTrialBalanceBudgetExcel: TestRequestPage "EXR Trial BalanceBudgetExcel") begin + EXRTrialBalanceBudgetExcel.GLAccounts.SetFilter("Date Filter", Format(DMY2Date(1, 1, Date2DMY(WorkDate(), 3))) + '..' + Format(DMY2Date(31, 12, Date2DMY(WorkDate(), 3)))); EXRTrialBalanceBudgetExcel.OK().Invoke(); end; @@ -762,6 +996,20 @@ codeunit 139544 "Trial Balance Excel Reports" EXRConsolidatedTrialBalance.OK().Invoke(); end; + [RequestPageHandler] + procedure EXRAgedAccPayableExcelHandler(var EXRAgedAccPayableExcel: TestRequestPage "EXR Aged Acc Payable Excel") + begin + EXRAgedAccPayableExcel.AgedAsOfOption.SetValue(WorkDate()); + EXRAgedAccPayableExcel.OK().Invoke(); + end; + + [RequestPageHandler] + procedure EXRAgedAccountsRecExcelHandler(var EXRAgedAccountsRecExcel: TestRequestPage "EXR Aged Accounts Rec Excel") + begin + EXRAgedAccountsRecExcel.AgedAsOfOption.SetValue(WorkDate()); + EXRAgedAccountsRecExcel.OK().Invoke(); + end; + #if not CLEAN27 #pragma warning disable AL0432 [EventSubscriber(ObjectType::Codeunit, Codeunit::"Trial Balance", OnIsPerformantTrialBalanceFeatureActive, '', false, false)] diff --git a/src/Apps/W1/External File Storage - Azure Blob Service Connector/App/src/ExtBlobStoConnectorImpl.Codeunit.al b/src/Apps/W1/External File Storage - Azure Blob Service Connector/App/src/ExtBlobStoConnectorImpl.Codeunit.al index a40ca00c6e..e98b514e04 100644 --- a/src/Apps/W1/External File Storage - Azure Blob Service Connector/App/src/ExtBlobStoConnectorImpl.Codeunit.al +++ b/src/Apps/W1/External File Storage - Azure Blob Service Connector/App/src/ExtBlobStoConnectorImpl.Codeunit.al @@ -31,7 +31,7 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage /// A list with all files stored in the path. procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var - ABSContainerContent: Record "ABS Container Content"; + TempABSContainerContent: Record "ABS Container Content"; ABSBlobClient: Codeunit "ABS Blob Client"; ABSOperationResponse: Codeunit "ABS Operation Response"; ABSOptionalParameters: Codeunit "ABS Optional Parameters"; @@ -40,21 +40,21 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage CheckPath(Path); InitOptionalParameters(Path, FilePaginationData, ABSOptionalParameters); ABSOptionalParameters.Delimiter('/'); - ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters); + ABSOperationResponse := ABSBlobClient.ListBlobs(TempABSContainerContent, ABSOptionalParameters); ValidateListingResponse(FilePaginationData, ABSOperationResponse); - ABSContainerContent.SetFilter("Blob Type", '<>%1', ''); - ABSContainerContent.SetFilter(Name, '<>%1', MarkerFileNameTok); - if not ABSContainerContent.FindSet() then + TempABSContainerContent.SetFilter("Blob Type", '<>%1', ''); + TempABSContainerContent.SetFilter(Name, '<>%1', MarkerFileNameTok); + if not TempABSContainerContent.FindSet() then exit; repeat TempFileAccountContent.Init(); - TempFileAccountContent.Name := ABSContainerContent.Name; + TempFileAccountContent.Name := TempABSContainerContent.Name; TempFileAccountContent.Type := TempFileAccountContent.Type::"File"; - TempFileAccountContent."Parent Directory" := ABSContainerContent."Parent Directory"; + TempFileAccountContent."Parent Directory" := TempABSContainerContent."Parent Directory"; TempFileAccountContent.Insert(); - until ABSContainerContent.Next() = 0; + until TempABSContainerContent.Next() = 0; end; /// @@ -144,7 +144,7 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage /// Returns true if the file exists procedure FileExists(AccountId: Guid; Path: Text): Boolean var - ABSContainerContent: Record "ABS Container Content"; + TempABSContainerContent: Record "ABS Container Content"; ABSBlobClient: Codeunit "ABS Blob Client"; ABSOperationResponse: Codeunit "ABS Operation Response"; ABSOptionalParameters: Codeunit "ABS Optional Parameters"; @@ -154,11 +154,11 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage InitBlobClient(AccountId, ABSBlobClient); ABSOptionalParameters.Prefix(Path); - ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters); + ABSOperationResponse := ABSBlobClient.ListBlobs(TempABSContainerContent, ABSOptionalParameters); if not ABSOperationResponse.IsSuccessful() then Error(ABSOperationResponse.GetError()); - exit(not ABSContainerContent.IsEmpty()); + exit(not TempABSContainerContent.IsEmpty()); end; /// @@ -189,7 +189,7 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage /// A list with all directories stored in the path. procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var - ABSContainerContent: Record "ABS Container Content"; + TempABSContainerContent: Record "ABS Container Content"; ABSBlobClient: Codeunit "ABS Blob Client"; ABSOperationResponse: Codeunit "ABS Operation Response"; ABSOptionalParameters: Codeunit "ABS Optional Parameters"; @@ -197,21 +197,21 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage InitBlobClient(AccountId, ABSBlobClient); CheckPath(Path); InitOptionalParameters(Path, FilePaginationData, ABSOptionalParameters); - ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters); + ABSOperationResponse := ABSBlobClient.ListBlobs(TempABSContainerContent, ABSOptionalParameters); ValidateListingResponse(FilePaginationData, ABSOperationResponse); - ABSContainerContent.SetRange("Parent Directory", Path); - ABSContainerContent.SetRange("Resource Type", ABSContainerContent."Resource Type"::Directory); - if not ABSContainerContent.FindSet() then + TempABSContainerContent.SetRange("Parent Directory", Path); + TempABSContainerContent.SetRange("Resource Type", TempABSContainerContent."Resource Type"::Directory); + if not TempABSContainerContent.FindSet() then exit; repeat TempFileAccountContent.Init(); - TempFileAccountContent.Name := ABSContainerContent.Name; + TempFileAccountContent.Name := TempABSContainerContent.Name; TempFileAccountContent.Type := TempFileAccountContent.Type::Directory; - TempFileAccountContent."Parent Directory" := ABSContainerContent."Parent Directory"; + TempFileAccountContent."Parent Directory" := TempABSContainerContent."Parent Directory"; TempFileAccountContent.Insert(); - until ABSContainerContent.Next() = 0; + until TempABSContainerContent.Next() = 0; end; /// @@ -246,7 +246,7 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage /// Returns true if the directory exists procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean var - ABSContainerContent: Record "ABS Container Content"; + TempABSContainerContent: Record "ABS Container Content"; ABSBlobClient: Codeunit "ABS Blob Client"; ABSOperationResponse: Codeunit "ABS Operation Response"; ABSOptionalParameters: Codeunit "ABS Optional Parameters"; @@ -257,11 +257,11 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage InitBlobClient(AccountId, ABSBlobClient); ABSOptionalParameters.Prefix(Path); ABSOptionalParameters.MaxResults(1); - ABSOperationResponse := ABSBlobClient.ListBlobs(ABSContainerContent, ABSOptionalParameters); + ABSOperationResponse := ABSBlobClient.ListBlobs(TempABSContainerContent, ABSOptionalParameters); if not ABSOperationResponse.IsSuccessful() then Error(ABSOperationResponse.GetError()); - exit(not ABSContainerContent.IsEmpty()); + exit(not TempABSContainerContent.IsEmpty()); end; /// @@ -401,7 +401,7 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage internal procedure LookUpContainer(var Account: Record "Ext. Blob Storage Account"; AuthType: Enum "Ext. Blob Storage Auth. Type"; Secret: SecretText; var NewContainerName: Text[2048]) var - ABSContainers: Record "ABS Container"; + TempABSContainers: Record "ABS Container"; ABSContainerClient: Codeunit "ABS Container Client"; StorageServiceAuthorization: Codeunit "Storage Service Authorization"; ABSOperationResponse: Codeunit "ABS Operation Response"; @@ -416,17 +416,17 @@ codeunit 4560 "Ext. Blob Sto. Connector Impl." implements "External File Storage end; ABSContainerClient.Initialize(Account."Storage Account Name", Authorization); - ABSOperationResponse := ABSContainerClient.ListContainers(ABSContainers); + ABSOperationResponse := ABSContainerClient.ListContainers(TempABSContainers); if not ABSOperationResponse.IsSuccessful() then Error(ABSOperationResponse.GetError()); - if not ABSContainers.Get(NewContainerName) then - if ABSContainers.FindFirst() then; + if not TempABSContainers.Get(NewContainerName) then + if TempABSContainers.FindFirst() then; - if (Page.RunModal(Page::"Ext. Blob Sto Container Lookup", ABSContainers) <> Action::LookupOK) then + if (Page.RunModal(Page::"Ext. Blob Sto Container Lookup", TempABSContainers) <> Action::LookupOK) then exit; - NewContainerName := ABSContainers.Name; + NewContainerName := TempABSContainers.Name; end; local procedure InitBlobClient(var AccountId: Guid; var ABSBlobClient: Codeunit "ABS Blob Client") diff --git a/src/Apps/W1/External File Storage - Azure Blob Service Connector/App/src/ExtBlobStorAccountWizard.Page.al b/src/Apps/W1/External File Storage - Azure Blob Service Connector/App/src/ExtBlobStorAccountWizard.Page.al index 22bd90a085..da99a49bbd 100644 --- a/src/Apps/W1/External File Storage - Azure Blob Service Connector/App/src/ExtBlobStorAccountWizard.Page.al +++ b/src/Apps/W1/External File Storage - Azure Blob Service Connector/App/src/ExtBlobStorAccountWizard.Page.al @@ -127,7 +127,7 @@ page 4561 "Ext. Blob Stor. Account Wizard" trigger OnAction() begin - BlobStorageConnectorImpl.CreateAccount(Rec, Secret, BlobStorageAccount); + BlobStorageConnectorImpl.CreateAccount(Rec, Secret, TempBlobStorageAccount); CurrPage.Close(); end; } @@ -135,7 +135,7 @@ page 4561 "Ext. Blob Stor. Account Wizard" } var - BlobStorageAccount: Record "File Account"; + TempBlobStorageAccount: Record "File Account"; MediaResources: Record "Media Resources"; BlobStorageConnectorImpl: Codeunit "Ext. Blob Sto. Connector Impl."; [NonDebuggable] @@ -156,10 +156,10 @@ page 4561 "Ext. Blob Stor. Account Wizard" internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean begin - if IsNullGuid(BlobStorageAccount."Account Id") then + if IsNullGuid(TempBlobStorageAccount."Account Id") then exit(false); - FileAccount := BlobStorageAccount; + FileAccount := TempBlobStorageAccount; exit(true); end; diff --git a/src/Apps/W1/External File Storage - Azure Blob Service Connector/Test/src/ExtAzureBlobServiceTest.Codeunit.al b/src/Apps/W1/External File Storage - Azure Blob Service Connector/Test/src/ExtAzureBlobServiceTest.Codeunit.al index c5a88c0bd8..8d0ce5ae90 100644 --- a/src/Apps/W1/External File Storage - Azure Blob Service Connector/Test/src/ExtAzureBlobServiceTest.Codeunit.al +++ b/src/Apps/W1/External File Storage - Azure Blob Service Connector/Test/src/ExtAzureBlobServiceTest.Codeunit.al @@ -20,7 +20,7 @@ codeunit 144566 "Ext. Azure Blob Service Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestMultipleAccountsCanBeRegistered() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExtFileConnector: Codeunit "Ext. Blob Sto. Connector Impl."; FileAccounts: TestPage "File Accounts"; AccountIds: array[3] of Guid; @@ -34,14 +34,14 @@ codeunit 144566 "Ext. Azure Blob Service Test" for Index := 1 to 3 do begin SetBasicAccount(); - Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); - AccountIds[Index] := FileAccount."Account Id"; + Assert.IsTrue(ExtFileConnector.RegisterAccount(TempFileAccount), 'Failed to register account.'); + AccountIds[Index] := TempFileAccount."Account Id"; AccountName[Index] := FileAccountMock.Name(); // [Then] Accounts are retrieved from the GetAccounts method - FileAccount.DeleteAll(); - ExtFileConnector.GetAccounts(FileAccount); - Assert.RecordCount(FileAccount, Index); + TempFileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(TempFileAccount); + Assert.RecordCount(TempFileAccount, Index); end; FileAccounts.OpenView(); @@ -58,7 +58,7 @@ codeunit 144566 "Ext. Azure Blob Service Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestEnviromentCleanupDisablesAccounts() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExtSharePointAccount: Record "Ext. Blob Storage Account"; ExtFileConnector: Codeunit "Ext. Blob Sto. Connector Impl."; EnvironmentTriggers: Codeunit "Environment Triggers"; @@ -72,13 +72,13 @@ codeunit 144566 "Ext. Azure Blob Service Test" for Index := 1 to 3 do begin SetBasicAccount(); - Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); - AccountIds[Index] := FileAccount."Account Id"; + Assert.IsTrue(ExtFileConnector.RegisterAccount(TempFileAccount), 'Failed to register account.'); + AccountIds[Index] := TempFileAccount."Account Id"; // [Then] Accounts are retrieved from the GetAccounts method - FileAccount.DeleteAll(); - ExtFileConnector.GetAccounts(FileAccount); - Assert.RecordCount(FileAccount, Index); + TempFileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(TempFileAccount); + Assert.RecordCount(TempFileAccount, Index); end; ExtSharePointAccount.SetRange(Disabled, true); @@ -95,7 +95,7 @@ codeunit 144566 "Ext. Azure Blob Service Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestShowAccountInformation() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; FileConnector: Codeunit "Ext. Blob Sto. Connector Impl."; begin // [Scenario] Account Information is displayed in the Account page. @@ -103,10 +103,10 @@ codeunit 144566 "Ext. Azure Blob Service Test" // [Given] An file account Initialize(); SetBasicAccount(); - FileConnector.RegisterAccount(FileAccount); + FileConnector.RegisterAccount(TempFileAccount); // [When] The ShowAccountInformation method is invoked - FileConnector.ShowAccountInformation(FileAccount."Account Id"); + FileConnector.ShowAccountInformation(TempFileAccount."Account Id"); // [Then] The account page opens and displays the information // Verify in AccountModalPageHandler diff --git a/src/Apps/W1/External File Storage - Azure File Service Connector/App/src/ExtFileShareAccountWizard.Page.al b/src/Apps/W1/External File Storage - Azure File Service Connector/App/src/ExtFileShareAccountWizard.Page.al index a1d85ab5d3..c98169818c 100644 --- a/src/Apps/W1/External File Storage - Azure File Service Connector/App/src/ExtFileShareAccountWizard.Page.al +++ b/src/Apps/W1/External File Storage - Azure File Service Connector/App/src/ExtFileShareAccountWizard.Page.al @@ -115,7 +115,7 @@ page 4571 "Ext. File Share Account Wizard" trigger OnAction() begin - FileShareConnectorImpl.CreateAccount(Rec, Secret, FileShareAccount); + FileShareConnectorImpl.CreateAccount(Rec, Secret, TempFileShareAccount); CurrPage.Close(); end; } @@ -123,7 +123,7 @@ page 4571 "Ext. File Share Account Wizard" } var - FileShareAccount: Record "File Account"; + TempFileShareAccount: Record "File Account"; MediaResources: Record "Media Resources"; FileShareConnectorImpl: Codeunit "Ext. File Share Connector Impl"; [NonDebuggable] @@ -144,10 +144,10 @@ page 4571 "Ext. File Share Account Wizard" internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean begin - if IsNullGuid(FileShareAccount."Account Id") then + if IsNullGuid(TempFileShareAccount."Account Id") then exit(false); - FileAccount := FileShareAccount; + FileAccount := TempFileShareAccount; exit(true); end; diff --git a/src/Apps/W1/External File Storage - Azure File Service Connector/App/src/ExtFileShareConnectorImpl.Codeunit.al b/src/Apps/W1/External File Storage - Azure File Service Connector/App/src/ExtFileShareConnectorImpl.Codeunit.al index bfb759c612..c11aa117ad 100644 --- a/src/Apps/W1/External File Storage - Azure File Service Connector/App/src/ExtFileShareConnectorImpl.Codeunit.al +++ b/src/Apps/W1/External File Storage - Azure File Service Connector/App/src/ExtFileShareConnectorImpl.Codeunit.al @@ -32,22 +32,22 @@ codeunit 4570 "Ext. File Share Connector Impl" implements "External File Storage /// A list with all files stored in the path. procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var - AFSDirectoryContent: Record "AFS Directory Content"; + TempAFSDirectoryContent: Record "AFS Directory Content"; begin - GetDirectoryContent(AccountId, Path, FilePaginationData, AFSDirectoryContent); + GetDirectoryContent(AccountId, Path, FilePaginationData, TempAFSDirectoryContent); - AFSDirectoryContent.SetRange("Parent Directory", Path); - AFSDirectoryContent.SetRange("Resource Type", AFSDirectoryContent."Resource Type"::File); - if not AFSDirectoryContent.FindSet() then + TempAFSDirectoryContent.SetRange("Parent Directory", Path); + TempAFSDirectoryContent.SetRange("Resource Type", TempAFSDirectoryContent."Resource Type"::File); + if not TempAFSDirectoryContent.FindSet() then exit; repeat TempFileAccountContent.Init(); - TempFileAccountContent.Name := AFSDirectoryContent.Name; + TempFileAccountContent.Name := TempAFSDirectoryContent.Name; TempFileAccountContent.Type := TempFileAccountContent.Type::"File"; - TempFileAccountContent."Parent Directory" := AFSDirectoryContent."Parent Directory"; + TempFileAccountContent."Parent Directory" := TempAFSDirectoryContent."Parent Directory"; TempFileAccountContent.Insert(); - until AFSDirectoryContent.Next() = 0; + until TempAFSDirectoryContent.Next() = 0; end; /// @@ -180,22 +180,22 @@ codeunit 4570 "Ext. File Share Connector Impl" implements "External File Storage /// A list with all directories stored in the path. procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var - AFSDirectoryContent: Record "AFS Directory Content"; + TempAFSDirectoryContent: Record "AFS Directory Content"; begin - GetDirectoryContent(AccountId, Path, FilePaginationData, AFSDirectoryContent); + GetDirectoryContent(AccountId, Path, FilePaginationData, TempAFSDirectoryContent); - AFSDirectoryContent.SetRange("Parent Directory", Path); - AFSDirectoryContent.SetRange("Resource Type", AFSDirectoryContent."Resource Type"::Directory); - if not AFSDirectoryContent.FindSet() then + TempAFSDirectoryContent.SetRange("Parent Directory", Path); + TempAFSDirectoryContent.SetRange("Resource Type", TempAFSDirectoryContent."Resource Type"::Directory); + if not TempAFSDirectoryContent.FindSet() then exit; repeat TempFileAccountContent.Init(); - TempFileAccountContent.Name := AFSDirectoryContent.Name; + TempFileAccountContent.Name := TempAFSDirectoryContent.Name; TempFileAccountContent.Type := TempFileAccountContent.Type::Directory; - TempFileAccountContent."Parent Directory" := AFSDirectoryContent."Parent Directory"; + TempFileAccountContent."Parent Directory" := TempAFSDirectoryContent."Parent Directory"; TempFileAccountContent.Insert(); - until AFSDirectoryContent.Next() = 0; + until TempAFSDirectoryContent.Next() = 0; end; /// @@ -226,7 +226,7 @@ codeunit 4570 "Ext. File Share Connector Impl" implements "External File Storage /// Returns true if the directory exists procedure DirectoryExists(AccountId: Guid; Path: Text): Boolean var - AFSDirectoryContent: Record "AFS Directory Content"; + TempAFSDirectoryContent: Record "AFS Directory Content"; AFSFileClient: Codeunit "AFS File Client"; AFSOperationResponse: Codeunit "AFS Operation Response"; AFSOptionalParameters: Codeunit "AFS Optional Parameters"; @@ -236,7 +236,7 @@ codeunit 4570 "Ext. File Share Connector Impl" implements "External File Storage InitFileClient(AccountId, AFSFileClient); AFSOptionalParameters.MaxResults(1); - AFSOperationResponse := AFSFileClient.ListDirectory(CopyStr(Path, 1, 2048), AFSDirectoryContent, AFSOptionalParameters); + AFSOperationResponse := AFSFileClient.ListDirectory(CopyStr(Path, 1, 2048), TempAFSDirectoryContent, AFSOptionalParameters); if AFSOperationResponse.IsSuccessful() then exit(true); @@ -416,19 +416,19 @@ codeunit 4570 "Ext. File Share Connector Impl" implements "External File Storage AFSOptionalParameters.Marker(FilePaginationData.GetMarker()); end; - local procedure ValidateListingResponse(var FilePaginationData: Codeunit "File Pagination Data"; var AFSDirectoryContent: Record "AFS Directory Content"; var AFSOperationResponse: Codeunit "AFS Operation Response") + local procedure ValidateListingResponse(var FilePaginationData: Codeunit "File Pagination Data"; var TempAFSDirectoryContent: Record "AFS Directory Content"; var AFSOperationResponse: Codeunit "AFS Operation Response") begin if not AFSOperationResponse.IsSuccessful() then Error(AFSOperationResponse.GetError()); - if not AFSDirectoryContent.FindLast() then + if not TempAFSDirectoryContent.FindLast() then FilePaginationData.SetEndOfListing(true); - FilePaginationData.SetMarker(AFSDirectoryContent."Next Marker"); - FilePaginationData.SetEndOfListing(AFSDirectoryContent."Next Marker" = ''); + FilePaginationData.SetMarker(TempAFSDirectoryContent."Next Marker"); + FilePaginationData.SetEndOfListing(TempAFSDirectoryContent."Next Marker" = ''); end; - local procedure GetDirectoryContent(var AccountId: Guid; var PassedPath: Text; var FilePaginationData: Codeunit "File Pagination Data"; var AFSDirectoryContent: Record "AFS Directory Content") + local procedure GetDirectoryContent(var AccountId: Guid; var PassedPath: Text; var FilePaginationData: Codeunit "File Pagination Data"; var TempAFSDirectoryContent: Record "AFS Directory Content") var AFSFileClient: Codeunit "AFS File Client"; AFSOperationResponse: Codeunit "AFS Operation Response"; @@ -439,9 +439,9 @@ codeunit 4570 "Ext. File Share Connector Impl" implements "External File Storage CheckPath(PassedPath); InitOptionalParameters(FilePaginationData, AFSOptionalParameters); Path := CopyStr(PassedPath, 1, MaxStrLen(Path)); - AFSOperationResponse := AFSFileClient.ListDirectory(Path, AFSDirectoryContent, AFSOptionalParameters); + AFSOperationResponse := AFSFileClient.ListDirectory(Path, TempAFSDirectoryContent, AFSOptionalParameters); PassedPath := Path; - ValidateListingResponse(FilePaginationData, AFSDirectoryContent, AFSOperationResponse); + ValidateListingResponse(FilePaginationData, TempAFSDirectoryContent, AFSOperationResponse); end; local procedure SetReadySAS(var StorageServiceAuthorization: Codeunit "Storage Service Authorization"; Secret: SecretText): Interface System.Azure.Storage."Storage Service Authorization" diff --git a/src/Apps/W1/External File Storage - Azure File Service Connector/Test/src/ExtAzureFileServiceTest.Codeunit.al b/src/Apps/W1/External File Storage - Azure File Service Connector/Test/src/ExtAzureFileServiceTest.Codeunit.al index 54903ac7b3..b2584b578d 100644 --- a/src/Apps/W1/External File Storage - Azure File Service Connector/Test/src/ExtAzureFileServiceTest.Codeunit.al +++ b/src/Apps/W1/External File Storage - Azure File Service Connector/Test/src/ExtAzureFileServiceTest.Codeunit.al @@ -20,7 +20,7 @@ codeunit 144571 "Ext. Azure File Service Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestMultipleAccountsCanBeRegistered() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExtFileConnector: Codeunit "Ext. File Share Connector Impl"; FileAccounts: TestPage "File Accounts"; AccountIds: array[3] of Guid; @@ -34,14 +34,14 @@ codeunit 144571 "Ext. Azure File Service Test" for Index := 1 to 3 do begin SetBasicAccount(); - Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); - AccountIds[Index] := FileAccount."Account Id"; + Assert.IsTrue(ExtFileConnector.RegisterAccount(TempFileAccount), 'Failed to register account.'); + AccountIds[Index] := TempFileAccount."Account Id"; AccountName[Index] := FileAccountMock.Name(); // [Then] Accounts are retrieved from the GetAccounts method - FileAccount.DeleteAll(); - ExtFileConnector.GetAccounts(FileAccount); - Assert.RecordCount(FileAccount, Index); + TempFileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(TempFileAccount); + Assert.RecordCount(TempFileAccount, Index); end; FileAccounts.OpenView(); @@ -58,7 +58,7 @@ codeunit 144571 "Ext. Azure File Service Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestEnviromentCleanupDisablesAccounts() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExtSharePointAccount: Record "Ext. File Share Account"; ExtFileConnector: Codeunit "Ext. File Share Connector Impl"; EnvironmentTriggers: Codeunit "Environment Triggers"; @@ -72,13 +72,13 @@ codeunit 144571 "Ext. Azure File Service Test" for Index := 1 to 3 do begin SetBasicAccount(); - Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); - AccountIds[Index] := FileAccount."Account Id"; + Assert.IsTrue(ExtFileConnector.RegisterAccount(TempFileAccount), 'Failed to register account.'); + AccountIds[Index] := TempFileAccount."Account Id"; // [Then] Accounts are retrieved from the GetAccounts method - FileAccount.DeleteAll(); - ExtFileConnector.GetAccounts(FileAccount); - Assert.RecordCount(FileAccount, Index); + TempFileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(TempFileAccount); + Assert.RecordCount(TempFileAccount, Index); end; ExtSharePointAccount.SetRange(Disabled, true); @@ -95,7 +95,7 @@ codeunit 144571 "Ext. Azure File Service Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestShowAccountInformation() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; FileConnector: Codeunit "Ext. File Share Connector Impl"; begin // [Scenario] Account Information is displayed in the Account page. @@ -103,10 +103,10 @@ codeunit 144571 "Ext. Azure File Service Test" // [Given] An file account Initialize(); SetBasicAccount(); - FileConnector.RegisterAccount(FileAccount); + FileConnector.RegisterAccount(TempFileAccount); // [When] The ShowAccountInformation method is invoked - FileConnector.ShowAccountInformation(FileAccount."Account Id"); + FileConnector.ShowAccountInformation(TempFileAccount."Account Id"); // [Then] The account page opens and displays the information // Verify in AccountModalPageHandler diff --git a/src/Apps/W1/External File Storage - SFTP Connector/App/src/ExtSFTPAccountWizard.Page.al b/src/Apps/W1/External File Storage - SFTP Connector/App/src/ExtSFTPAccountWizard.Page.al index 74696cdb3e..a8da971cb9 100644 --- a/src/Apps/W1/External File Storage - SFTP Connector/App/src/ExtSFTPAccountWizard.Page.al +++ b/src/Apps/W1/External File Storage - SFTP Connector/App/src/ExtSFTPAccountWizard.Page.al @@ -179,7 +179,7 @@ page 4622 "Ext. SFTP Account Wizard" trigger OnAction() begin - ConnectorImpl.CreateAccount(Rec, Password, Certificate, CertificatePassword, Account); + ConnectorImpl.CreateAccount(Rec, Password, Certificate, CertificatePassword, TempAccount); CurrPage.Close(); end; } @@ -187,7 +187,7 @@ page 4622 "Ext. SFTP Account Wizard" } var - Account: Record "File Account"; + TempAccount: Record "File Account"; MediaResources: Record "Media Resources"; ConnectorImpl: Codeunit "Ext. SFTP Connector Impl"; [NonDebuggable] @@ -213,10 +213,10 @@ page 4622 "Ext. SFTP Account Wizard" internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean begin - if IsNullGuid(Account."Account Id") then + if IsNullGuid(TempAccount."Account Id") then exit(false); - FileAccount := Account; + FileAccount := TempAccount; exit(true); end; diff --git a/src/Apps/W1/External File Storage - SFTP Connector/App/src/ExtSFTPConnectorImpl.Codeunit.al b/src/Apps/W1/External File Storage - SFTP Connector/App/src/ExtSFTPConnectorImpl.Codeunit.al index 473feb955b..7667621f7c 100644 --- a/src/Apps/W1/External File Storage - SFTP Connector/App/src/ExtSFTPConnectorImpl.Codeunit.al +++ b/src/Apps/W1/External File Storage - SFTP Connector/App/src/ExtSFTPConnectorImpl.Codeunit.al @@ -31,7 +31,7 @@ codeunit 4621 "Ext. SFTP Connector Impl" implements "External File Storage Conne /// A list with all files stored in the path. procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var - FolderContent: Record "SFTP Folder Content"; + TempFolderContent: Record "SFTP Folder Content"; SFTPClient: Codeunit "SFTP Client"; Response: Codeunit "SFTP Operation Response"; OrginalPath: Text; @@ -39,7 +39,7 @@ codeunit 4621 "Ext. SFTP Connector Impl" implements "External File Storage Conne OrginalPath := Path; InitPath(AccountId, Path); InitSFTPClient(AccountId, SFTPClient); - Response := SFTPClient.ListFiles(Path, FolderContent); + Response := SFTPClient.ListFiles(Path, TempFolderContent); SFTPClient.Disconnect(); if Response.IsError() then @@ -47,17 +47,17 @@ codeunit 4621 "Ext. SFTP Connector Impl" implements "External File Storage Conne FilePaginationData.SetEndOfListing(true); - FolderContent.SetRange("Is Directory", false); - if not FolderContent.FindSet() then + TempFolderContent.SetRange("Is Directory", false); + if not TempFolderContent.FindSet() then exit; repeat TempFileAccountContent.Init(); - TempFileAccountContent.Name := FolderContent.Name; + TempFileAccountContent.Name := TempFolderContent.Name; TempFileAccountContent.Type := TempFileAccountContent.Type::"File"; TempFileAccountContent."Parent Directory" := CopyStr(OrginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); TempFileAccountContent.Insert(); - until FolderContent.Next() = 0; + until TempFolderContent.Next() = 0; end; /// @@ -197,7 +197,7 @@ codeunit 4621 "Ext. SFTP Connector Impl" implements "External File Storage Conne /// A list with all directories stored in the path. procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var - FolderContent: Record "SFTP Folder Content"; + TempFolderContent: Record "SFTP Folder Content"; SFTPClient: Codeunit "SFTP Client"; Response: Codeunit "SFTP Operation Response"; OrginalPath: Text; @@ -205,7 +205,7 @@ codeunit 4621 "Ext. SFTP Connector Impl" implements "External File Storage Conne OrginalPath := Path; InitPath(AccountId, Path); InitSFTPClient(AccountId, SFTPClient); - Response := SFTPClient.ListFiles(Path, FolderContent); + Response := SFTPClient.ListFiles(Path, TempFolderContent); SFTPClient.Disconnect(); if Response.IsError() then @@ -213,18 +213,18 @@ codeunit 4621 "Ext. SFTP Connector Impl" implements "External File Storage Conne FilePaginationData.SetEndOfListing(true); - FolderContent.SetRange("Is Directory", true); - FolderContent.SetFilter(Name, '<>%1&<>%2', '.', '..'); // Exclude . and .. - if not FolderContent.FindSet() then + TempFolderContent.SetRange("Is Directory", true); + TempFolderContent.SetFilter(Name, '<>%1&<>%2', '.', '..'); // Exclude . and .. + if not TempFolderContent.FindSet() then exit; repeat TempFileAccountContent.Init(); - TempFileAccountContent.Name := FolderContent.Name; + TempFileAccountContent.Name := TempFolderContent.Name; TempFileAccountContent.Type := TempFileAccountContent.Type::Directory; TempFileAccountContent."Parent Directory" := CopyStr(OrginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); TempFileAccountContent.Insert(); - until FolderContent.Next() = 0; + until TempFolderContent.Next() = 0; end; /// diff --git a/src/Apps/W1/External File Storage - SFTP Connector/Test/src/ExtSFTPConnectorTest.Codeunit.al b/src/Apps/W1/External File Storage - SFTP Connector/Test/src/ExtSFTPConnectorTest.Codeunit.al index 7241327e0f..0ff3dedcc5 100644 --- a/src/Apps/W1/External File Storage - SFTP Connector/Test/src/ExtSFTPConnectorTest.Codeunit.al +++ b/src/Apps/W1/External File Storage - SFTP Connector/Test/src/ExtSFTPConnectorTest.Codeunit.al @@ -20,7 +20,7 @@ codeunit 144591 "Ext. SFTP Connector Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestMultipleAccountsCanBeRegistered() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExtFileConnector: Codeunit "Ext. SFTP Connector Impl"; FileAccounts: TestPage "File Accounts"; AccountIds: array[3] of Guid; @@ -34,14 +34,14 @@ codeunit 144591 "Ext. SFTP Connector Test" for Index := 1 to 3 do begin SetBasicAccount(); - Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); - AccountIds[Index] := FileAccount."Account Id"; + Assert.IsTrue(ExtFileConnector.RegisterAccount(TempFileAccount), 'Failed to register account.'); + AccountIds[Index] := TempFileAccount."Account Id"; AccountName[Index] := FileAccountMock.Name(); // [Then] Accounts are retrieved from the GetAccounts method - FileAccount.DeleteAll(); - ExtFileConnector.GetAccounts(FileAccount); - Assert.RecordCount(FileAccount, Index); + TempFileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(TempFileAccount); + Assert.RecordCount(TempFileAccount, Index); end; FileAccounts.OpenView(); @@ -57,7 +57,7 @@ codeunit 144591 "Ext. SFTP Connector Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestEnvironmentCleanupDisablesAccounts() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExtSFTPAccount: Record "Ext. SFTP Account"; ExtFileConnector: Codeunit "Ext. SFTP Connector Impl"; EnvironmentTriggers: Codeunit "Environment Triggers"; @@ -71,13 +71,13 @@ codeunit 144591 "Ext. SFTP Connector Test" for Index := 1 to 3 do begin SetBasicAccount(); - Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); - AccountIds[Index] := FileAccount."Account Id"; + Assert.IsTrue(ExtFileConnector.RegisterAccount(TempFileAccount), 'Failed to register account.'); + AccountIds[Index] := TempFileAccount."Account Id"; // [Then] Accounts are retrieved from the GetAccounts method - FileAccount.DeleteAll(); - ExtFileConnector.GetAccounts(FileAccount); - Assert.RecordCount(FileAccount, Index); + TempFileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(TempFileAccount); + Assert.RecordCount(TempFileAccount, Index); end; ExtSFTPAccount.SetRange(Disabled, true); @@ -94,7 +94,7 @@ codeunit 144591 "Ext. SFTP Connector Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestShowAccountInformation() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; FileConnector: Codeunit "Ext. SFTP Connector Impl"; begin // [Scenario] Account Information is displayed in the Account page. @@ -102,10 +102,10 @@ codeunit 144591 "Ext. SFTP Connector Test" // [Given] An file account Initialize(); SetBasicAccount(); - FileConnector.RegisterAccount(FileAccount); + FileConnector.RegisterAccount(TempFileAccount); // [When] The ShowAccountInformation method is invoked - FileConnector.ShowAccountInformation(FileAccount."Account Id"); + FileConnector.ShowAccountInformation(TempFileAccount."Account Id"); // [Then] The account page opens and displays the information // Verify in AccountModalPageHandler diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al index 10fef6fb73..29fea7fe10 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointAccountWizard.Page.al @@ -182,7 +182,7 @@ page 4581 "Ext. SharePoint Account Wizard" SecretToPass := Certificate; end; - SharePointConnectorImpl.CreateAccount(Rec, SecretToPass, CertificatePassword, SharePointAccount); + SharePointConnectorImpl.CreateAccount(Rec, SecretToPass, CertificatePassword, TempSharePointAccount); CurrPage.Close(); end; } @@ -190,7 +190,7 @@ page 4581 "Ext. SharePoint Account Wizard" } var - SharePointAccount: Record "File Account"; + TempSharePointAccount: Record "File Account"; MediaResources: Record "Media Resources"; SharePointConnectorImpl: Codeunit "Ext. SharePoint Connector Impl"; [NonDebuggable] @@ -220,10 +220,10 @@ page 4581 "Ext. SharePoint Account Wizard" internal procedure GetAccount(var FileAccount: Record "File Account"): Boolean begin - if IsNullGuid(SharePointAccount."Account Id") then + if IsNullGuid(TempSharePointAccount."Account Id") then exit(false); - FileAccount := SharePointAccount; + FileAccount := TempSharePointAccount; exit(true); end; diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al index cf8be00aaf..5be0b987b6 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/App/src/ExtSharePointConnectorImpl.Codeunit.al @@ -30,28 +30,28 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// A list with all files stored in the path. procedure ListFiles(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var - SharePointFile: Record "SharePoint File"; + TempSharePointFile: Record "SharePoint File"; SharePointClient: Codeunit "SharePoint Client"; OrginalPath: Text; begin OrginalPath := Path; InitPath(AccountId, Path); InitSharePointClient(AccountId, SharePointClient); - if not SharePointClient.GetFolderFilesByServerRelativeUrl(Path, SharePointFile) then + if not SharePointClient.GetFolderFilesByServerRelativeUrl(Path, TempSharePointFile) then ShowError(SharePointClient); FilePaginationData.SetEndOfListing(true); - if not SharePointFile.FindSet() then + if not TempSharePointFile.FindSet() then exit; repeat TempFileAccountContent.Init(); - TempFileAccountContent.Name := SharePointFile.Name; + TempFileAccountContent.Name := TempSharePointFile.Name; TempFileAccountContent.Type := TempFileAccountContent.Type::"File"; TempFileAccountContent."Parent Directory" := CopyStr(OrginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); TempFileAccountContent.Insert(); - until SharePointFile.Next() = 0; + until TempSharePointFile.Next() = 0; end; /// @@ -85,14 +85,14 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The Stream were the file is read from. procedure CreateFile(AccountId: Guid; Path: Text; Stream: InStream) var - SharePointFile: Record "SharePoint File"; + TempSharePointFile: Record "SharePoint File"; SharePointClient: Codeunit "SharePoint Client"; ParentPath, FileName : Text; begin InitPath(AccountId, Path); InitSharePointClient(AccountId, SharePointClient); SplitPath(Path, ParentPath, FileName); - if SharePointClient.AddFileToFolder(ParentPath, FileName, Stream, SharePointFile, false) then + if SharePointClient.AddFileToFolder(ParentPath, FileName, Stream, TempSharePointFile, false) then exit; ShowError(SharePointClient); @@ -138,16 +138,16 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// Returns true if the file exists procedure FileExists(AccountId: Guid; Path: Text): Boolean var - SharePointFile: Record "SharePoint File"; + TempSharePointFile: Record "SharePoint File"; SharePointClient: Codeunit "SharePoint Client"; begin InitPath(AccountId, Path); InitSharePointClient(AccountId, SharePointClient); - if not SharePointClient.GetFolderFilesByServerRelativeUrl(GetParentPath(Path), SharePointFile) then + if not SharePointClient.GetFolderFilesByServerRelativeUrl(GetParentPath(Path), TempSharePointFile) then ShowError(SharePointClient); - SharePointFile.SetRange(Name, GetFileName(Path)); - exit(not SharePointFile.IsEmpty()); + TempSharePointFile.SetRange(Name, GetFileName(Path)); + exit(not TempSharePointFile.IsEmpty()); end; /// @@ -176,28 +176,28 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// A list with all directories stored in the path. procedure ListDirectories(AccountId: Guid; Path: Text; FilePaginationData: Codeunit "File Pagination Data"; var TempFileAccountContent: Record "File Account Content" temporary) var - SharePointFolder: Record "SharePoint Folder"; + TempSharePointFolder: Record "SharePoint Folder"; SharePointClient: Codeunit "SharePoint Client"; OrginalPath: Text; begin OrginalPath := Path; InitPath(AccountId, Path); InitSharePointClient(AccountId, SharePointClient); - if not SharePointClient.GetSubFoldersByServerRelativeUrl(Path, SharePointFolder) then + if not SharePointClient.GetSubFoldersByServerRelativeUrl(Path, TempSharePointFolder) then ShowError(SharePointClient); FilePaginationData.SetEndOfListing(true); - if not SharePointFolder.FindSet() then + if not TempSharePointFolder.FindSet() then exit; repeat TempFileAccountContent.Init(); - TempFileAccountContent.Name := SharePointFolder.Name; + TempFileAccountContent.Name := TempSharePointFolder.Name; TempFileAccountContent.Type := TempFileAccountContent.Type::Directory; TempFileAccountContent."Parent Directory" := CopyStr(OrginalPath, 1, MaxStrLen(TempFileAccountContent."Parent Directory")); TempFileAccountContent.Insert(); - until SharePointFolder.Next() = 0; + until TempSharePointFolder.Next() = 0; end; /// @@ -207,12 +207,12 @@ codeunit 4580 "Ext. SharePoint Connector Impl" implements "External File Storage /// The directory path inside the file account. procedure CreateDirectory(AccountId: Guid; Path: Text) var - SharePointFolder: Record "SharePoint Folder"; + TempSharePointFolder: Record "SharePoint Folder"; SharePointClient: Codeunit "SharePoint Client"; begin InitPath(AccountId, Path); InitSharePointClient(AccountId, SharePointClient); - if SharePointClient.CreateFolder(Path, SharePointFolder) then + if SharePointClient.CreateFolder(Path, TempSharePointFolder) then exit; ShowError(SharePointClient); diff --git a/src/Apps/W1/External File Storage - SharePoint Connector/Test/src/ExtSharePointConnectorTest.Codeunit.al b/src/Apps/W1/External File Storage - SharePoint Connector/Test/src/ExtSharePointConnectorTest.Codeunit.al index 0e0f715dc4..f487f09782 100644 --- a/src/Apps/W1/External File Storage - SharePoint Connector/Test/src/ExtSharePointConnectorTest.Codeunit.al +++ b/src/Apps/W1/External File Storage - SharePoint Connector/Test/src/ExtSharePointConnectorTest.Codeunit.al @@ -20,7 +20,7 @@ codeunit 144581 "Ext. SharePoint Connector Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestMultipleAccountsCanBeRegistered() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExtFileConnector: Codeunit "Ext. SharePoint Connector Impl"; FileAccounts: TestPage "File Accounts"; AccountIds: array[3] of Guid; @@ -34,14 +34,14 @@ codeunit 144581 "Ext. SharePoint Connector Test" for Index := 1 to 3 do begin SetBasicAccount(); - Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); - AccountIds[Index] := FileAccount."Account Id"; + Assert.IsTrue(ExtFileConnector.RegisterAccount(TempFileAccount), 'Failed to register account.'); + AccountIds[Index] := TempFileAccount."Account Id"; AccountName[Index] := FileAccountMock.Name(); // [Then] Accounts are retrieved from the GetAccounts method - FileAccount.DeleteAll(); - ExtFileConnector.GetAccounts(FileAccount); - Assert.RecordCount(FileAccount, Index); + TempFileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(TempFileAccount); + Assert.RecordCount(TempFileAccount, Index); end; FileAccounts.OpenView(); @@ -57,7 +57,7 @@ codeunit 144581 "Ext. SharePoint Connector Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestEnviromentCleanupDisablesAccounts() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExtSharePointAccount: Record "Ext. SharePoint Account"; ExtFileConnector: Codeunit "Ext. SharePoint Connector Impl"; EnvironmentTriggers: Codeunit "Environment Triggers"; @@ -71,13 +71,13 @@ codeunit 144581 "Ext. SharePoint Connector Test" for Index := 1 to 3 do begin SetBasicAccount(); - Assert.IsTrue(ExtFileConnector.RegisterAccount(FileAccount), 'Failed to register account.'); - AccountIds[Index] := FileAccount."Account Id"; + Assert.IsTrue(ExtFileConnector.RegisterAccount(TempFileAccount), 'Failed to register account.'); + AccountIds[Index] := TempFileAccount."Account Id"; // [Then] Accounts are retrieved from the GetAccounts method - FileAccount.DeleteAll(); - ExtFileConnector.GetAccounts(FileAccount); - Assert.RecordCount(FileAccount, Index); + TempFileAccount.DeleteAll(); + ExtFileConnector.GetAccounts(TempFileAccount); + Assert.RecordCount(TempFileAccount, Index); end; ExtSharePointAccount.SetRange(Disabled, true); @@ -94,7 +94,7 @@ codeunit 144581 "Ext. SharePoint Connector Test" [TransactionModel(TransactionModel::AutoRollback)] procedure TestShowAccountInformation() var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; FileConnector: Codeunit "Ext. SharePoint Connector Impl"; begin // [Scenario] Account Information is displayed in the Account page. @@ -102,10 +102,10 @@ codeunit 144581 "Ext. SharePoint Connector Test" // [Given] An file account Initialize(); SetBasicAccount(); - FileConnector.RegisterAccount(FileAccount); + FileConnector.RegisterAccount(TempFileAccount); // [When] The ShowAccountInformation method is invoked - FileConnector.ShowAccountInformation(FileAccount."Account Id"); + FileConnector.ShowAccountInformation(TempFileAccount."Account Id"); // [Then] The account page opens and displays the information // Verify in AccountModalPageHandler diff --git a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al index e1938182b9..d8ef9a0151 100644 --- a/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al +++ b/src/Apps/W1/External Storage - Document Attachments/app/src/DocumentAttachmentIntegration/DAExternalStorageImpl.Codeunit.al @@ -31,7 +31,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" procedure BeforeAddOrModifyFileScenarioCheck(Scenario: Enum "File Scenario"; Connector: Enum "Ext. File Storage Connector"): Boolean; var ExternalStorageSetup: Record "DA External Storage Setup"; - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; FileScenarioCU: Codeunit "File Scenario"; ConfirmManagement: Codeunit "Confirm Management"; ExternalStorageSetupPage: Page "DA External Storage Setup"; @@ -42,7 +42,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" exit; // Check if scenario is already assigned to a different account - if FileScenarioCU.GetSpecificFileAccount(Scenario, FileAccount) then + if FileScenarioCU.GetSpecificFileAccount(Scenario, TempFileAccount) then // If feature is enabled and has uploaded files, don't allow reassignment if ExternalStorageSetup.Get() then if ExternalStorageSetup.Enabled then begin @@ -133,7 +133,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// True if upload was successful, false otherwise. procedure UploadToExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; DAFeatureTelemetry: Codeunit "DA Feature Telemetry"; @@ -168,7 +168,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" // Search for External Storage assigned File Scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(false); // Create the file with connector using the File Account framework @@ -193,7 +193,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// True if download was successful, false otherwise. procedure DownloadFromExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; DAFeatureTelemetry: Codeunit "DA Feature Telemetry"; @@ -218,7 +218,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" // Search for External Storage assigned File Scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(false); // Get the file with connector using the File Account framework @@ -240,7 +240,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// True if download and import was successful, false otherwise. procedure DownloadFromExternalStorageToInternal(var DocumentAttachment: Record "Document Attachment"): Boolean var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; FileScenario: Enum "File Scenario"; @@ -260,7 +260,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" // Search for External Storage assigned File Scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(false); // Get the file with connector using the File Account framework @@ -284,7 +284,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// True if the download was successful, false otherwise. procedure DownloadFromExternalStorageToStream(ExternalFilePath: Text; var AttachmentOutStream: OutStream): Boolean var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; FileScenario: Enum "File Scenario"; @@ -292,7 +292,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" begin // Search for External Storage assigned File Scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(false); // Get the file from external storage @@ -313,7 +313,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// True if the download was successful, false otherwise. procedure DownloadFromExternalStorageToTempBlob(ExternalFilePath: Text; var TempBlob: Codeunit "Temp Blob"): Boolean var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; FileScenario: Enum "File Scenario"; @@ -322,7 +322,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" begin // Search for External Storage assigned File Scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(false); // Get the file from external storage @@ -343,14 +343,14 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// True if the file exists, false otherwise. procedure CheckIfFileExistInExternalStorage(ExternalFilePath: Text): Boolean var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; FileScenario: Enum "File Scenario"; begin // Search for External Storage assigned File Scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(false); // Get the file from external storage @@ -365,7 +365,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// True if deletion was successful, false otherwise. procedure DeleteFromExternalStorage(var DocumentAttachment: Record "Document Attachment"): Boolean var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; DAFeatureTelemetry: Codeunit "DA Feature Telemetry"; @@ -400,7 +400,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" // Search for External Storage assigned File Scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(false); // Delete the file with connector using the File Account framework @@ -519,7 +519,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// The selected folder path, or empty string if cancelled. procedure SelectRootFolder(): Text var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; SelectFolderPathLbl: Label 'Select Root Folder for Attachments'; @@ -527,7 +527,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" begin // Initialize external file storage with the scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(''); ExternalFileStorage.Initialize(FileScenario); @@ -612,14 +612,14 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" local procedure EnsureFolderExists(CompanyFolderPath: Text) var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; FileScenario: Enum "File Scenario"; begin // Initialize external file storage with the scenario FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit; ExternalFileStorage.Initialize(FileScenario); @@ -674,7 +674,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" /// True if migration was successful, false otherwise. procedure MigrateFileToCurrentEnvironment(var DocumentAttachment: Record "Document Attachment"): Boolean var - FileAccount: Record "File Account"; + TempFileAccount: Record "File Account"; ExternalFileStorage: Codeunit "External File Storage"; FileScenarioCU: Codeunit "File Scenario"; TempBlob: Codeunit "Temp Blob"; @@ -692,7 +692,7 @@ codeunit 8751 "DA External Storage Impl." implements "File Scenario" // Initialize external file storage FileScenario := FileScenario::"Doc. Attach. - External Storage"; - if not FileScenarioCU.GetSpecificFileAccount(FileScenario, FileAccount) then + if not FileScenarioCU.GetSpecificFileAccount(FileScenario, TempFileAccount) then exit(false); ExternalFileStorage.Initialize(FileScenario); diff --git a/src/Apps/W1/PEPPOL/App/src/Install/PEPPOL30Initialize.Codeunit.al b/src/Apps/W1/PEPPOL/App/src/Install/PEPPOL30Initialize.Codeunit.al index fcf7db89d9..99deb25050 100644 --- a/src/Apps/W1/PEPPOL/App/src/Install/PEPPOL30Initialize.Codeunit.al +++ b/src/Apps/W1/PEPPOL/App/src/Install/PEPPOL30Initialize.Codeunit.al @@ -13,6 +13,11 @@ codeunit 37204 "PEPPOL30 Initialize" InherentPermissions = X; Access = Internal; + trigger OnInstallAppPerCompany() + begin + CreateElectronicDocumentFormats(); + end; + internal procedure CreateElectronicDocumentFormats() var ElectronicDocumentFormat: Record "Electronic Document Format"; diff --git a/src/Apps/W1/PEPPOL/extensibility_examples.md b/src/Apps/W1/PEPPOL/extensibility_examples.md index 6d38b681bd..3c97e96014 100644 --- a/src/Apps/W1/PEPPOL/extensibility_examples.md +++ b/src/Apps/W1/PEPPOL/extensibility_examples.md @@ -1,138 +1,226 @@ -# Extensibility examples +# Extending PEPPOL 3.0 -Existing PEPPOL functionality can be extended by partners using provided interfaces. +The PEPPOL app exposes 10 interfaces through the `"PEPPOL 3.0 Format"` enum, allowing partners to override any part of the PEPPOL document generation pipeline. ## Dependency -In order to extend existing PEPPOL export functionality partners first should add dependency on the 1st party app "PEPPOL" in their app.json file: +Add a dependency on the PEPPOL app in your `app.json`: ```json - "dependencies": [ - { - "id": "e1966889-b5fb-4fda-a84c-ea71b590e1a9", - "name": "PEPPOL", - "publisher": "Microsoft", - "version": "27.0.0.0" - } - ] +"dependencies": [ + { + "id": "e1966889-b5fb-4fda-a84c-ea71b590e1a9", + "name": "PEPPOL", + "publisher": "Microsoft", + "version": "29.0.0.0" + } +] ``` -## Electronic Document Formats adjustments +## Architecture + +The `"PEPPOL 3.0 Format"` enum (ID 37200) implements these interfaces: + +| Interface | Responsibility | +|-----------|---------------| +| `"PEPPOL30 Validation"` | Document and line validation | +| `"PEPPOL Document Info Provider"` | IDs, dates, currency, references | +| `"PEPPOL Line Info Provider"` | Line quantities, amounts, items, pricing | +| `"PEPPOL Party Info Provider"` | Supplier and customer party details | +| `"PEPPOL Monetary Info Provider"` | Totals and currency amounts | +| `"PEPPOL Tax Info Provider"` | VAT, tax categories, exemptions | +| `"PEPPOL Payment Info Provider"` | Payment means and terms | +| `"PEPPOL Delivery Info Provider"` | Delivery dates, addresses, GLN | +| `"PEPPOL Attachment Provider"` | Document attachments and PDF generation | +| `"PEPPOL Posted Document Iterator"` | Iterating posted invoice/credit memo records | -When the app is installed the new electronic document formats are created. In order to use new PEPPOL functionality customer would need to adjust existing "Electronic Document Formats" to only include export for the new PEPPOL format. +Most interfaces have a default implementation in the `"PEPPOL30"` codeunit. Two interfaces — `"PEPPOL30 Validation"` and `"PEPPOL Posted Document Iterator"` — require per-value implementations because they vary between sales and service documents. -## Enum extension +## Extending the enum -With the new implementation of PEPPOL processing an enum "E-Document Format" has been created which can be extended as needed in order to implement custom business logic for processing: +Add a new value to `"PEPPOL 3.0 Format"` and specify which interfaces you override. Interfaces you don't list fall back to the default implementation. ```al -enumextension 50100 "E-Document Format" extends "E-Document Format" +enumextension 50100 "My PEPPOL Format" extends "PEPPOL 3.0 Format" { - value(1; "PEPPOL XYZ") + value(50100; "My Custom PEPPOL") { - Caption = 'PEPPOL XYZ'; + Caption = 'My Custom PEPPOL'; + Implementation = "PEPPOL30 Validation" = "My PEPPOL Validation", + "PEPPOL Posted Document Iterator" = "PEPPOL30 Sales Iterator"; } } ``` -The value of the enum can be set on the `Company Information` page +> **Note:** `"PEPPOL30 Validation"` and `"PEPPOL Posted Document Iterator"` have no default implementation on the enum, so you must always specify them. You can reuse the standard codeunits (`"PEPPOL30 Sales Validation"`, `"PEPPOL30 Sales Iterator"`, etc.) or provide your own. -## Interfaces +After installing your extension, select your new format value on the **PEPPOL 3.0 Setup** page. -Existing PEPPOL functionality has been split into multiple interfaces in order to allow partners to execute their business logic in a more granular way. +## Example: Custom validation -Partners should only implement interfaces that they are going to extend. +The `"PEPPOL30 Validation"` interface defines these methods: ```al -enumextension 50100 "E-Document Format" extends "E-Document Format" +interface "PEPPOL30 Validation" { - value(1; "PEPPOL XYZ") - { - Caption = 'PEPPOL XYZ'; - Implementation = "PEPPOL30 Validation" = "XYZ PEPPOL30 Validation"; - } + procedure ValidateDocument(RecordVariant: Variant) + procedure ValidateDocumentLines(RecordVariant: Variant) + procedure ValidateDocumentLine(RecordVariant: Variant) + procedure ValidateLineTypeAndDescription(RecordVariant: Variant): Boolean + procedure ValidatePostedDocument(RecordVariant: Variant) } ``` -If for example partners want to implement their custom business logic in just one procedure it's possible to achieve by writing additional code in procedure while calling standard Microsoft procedures in for the remaining ones in the interface. +All parameters are `Variant` so the same interface works for both sales and service documents. + +### Overriding a single method -In this example we only want to execute custom business logic for procedure `CheckSalesDocument` why keeping the other processing standard +To customize only one method while keeping standard behavior for the rest, delegate to the standard implementation codeunit: ```al -codeunit 50149 "XYZ PEPPOL30 Validation" implements "PEPPOL30 Validation" +codeunit 50100 "My PEPPOL Validation" implements "PEPPOL30 Validation" { var - PEPPOLValidation: Codeunit "PEPPOL30 Validation"; + StandardValidation: Codeunit "PEPPOL30 Sales Validation"; - procedure CheckSalesDocument(SalesHeader: Record "Sales Header") + procedure ValidateDocument(RecordVariant: Variant) begin - SalesHeader.TestField("External Document No."); + // Custom logic: require External Document No. + StandardValidation.ValidateDocument(RecordVariant); end; - procedure CheckSalesDocumentLines(SalesHeader: Record "Sales Header") + procedure ValidateDocumentLines(RecordVariant: Variant) begin - PEPPOLValidation.CheckSalesDocumentLines(SalesHeader); + StandardValidation.ValidateDocumentLines(RecordVariant); end; - procedure CheckSalesDocumentLine(SalesLine: Record "Sales Line") + procedure ValidateDocumentLine(RecordVariant: Variant) begin - PEPPOLValidation.CheckSalesDocumentLine(SalesLine); + StandardValidation.ValidateDocumentLine(RecordVariant); end; - procedure CheckSalesInvoice(SalesInvoiceHeader: Record "Sales Invoice Header") + procedure ValidateLineTypeAndDescription(RecordVariant: Variant): Boolean begin - PEPPOLValidation.CheckSalesInvoice(SalesInvoiceHeader); + exit(StandardValidation.ValidateLineTypeAndDescription(RecordVariant)); end; - procedure CheckSalesCreditMemo(SalesCrMemoHeader: Record "Sales Cr.Memo Header") + procedure ValidatePostedDocument(RecordVariant: Variant) begin - PEPPOLValidation.CheckSalesCreditMemo(SalesCrMemoHeader); + StandardValidation.ValidatePostedDocument(RecordVariant); end; +} +``` - procedure CheckSalesLineTypeAndDescription(SalesLine: Record "Sales Line"): Boolean +### Adding validation after standard checks + +To run additional checks after the standard validation, call the standard method first, then add your logic: + +```al +codeunit 50100 "My PEPPOL Validation" implements "PEPPOL30 Validation" +{ + var + StandardValidation: Codeunit "PEPPOL30 Sales Validation"; + + procedure ValidateDocument(RecordVariant: Variant) + var + SalesHeader: Record "Sales Header"; + begin + StandardValidation.ValidateDocument(RecordVariant); + SalesHeader := RecordVariant; + SalesHeader.TestField("External Document No."); + end; + + procedure ValidateDocumentLines(RecordVariant: Variant) begin - exit(PEPPOLValidation.CheckSalesLineTypeAndDescription(SalesLine)); + StandardValidation.ValidateDocumentLines(RecordVariant); end; + + procedure ValidateDocumentLine(RecordVariant: Variant) + var + SalesLine: Record "Sales Line"; + begin + StandardValidation.ValidateDocumentLine(RecordVariant); + SalesLine := RecordVariant; + SalesLine.TestField("Tax Area Code"); + end; + + procedure ValidateLineTypeAndDescription(RecordVariant: Variant): Boolean + begin + exit(StandardValidation.ValidateLineTypeAndDescription(RecordVariant)); + end; + + procedure ValidatePostedDocument(RecordVariant: Variant) + begin + StandardValidation.ValidatePostedDocument(RecordVariant); + end; +} +``` + +## Example: Custom document info + +To override how document-level fields are populated in the PEPPOL XML, implement `"PEPPOL Document Info Provider"`: + +```al +enumextension 50100 "My PEPPOL Format" extends "PEPPOL 3.0 Format" +{ + value(50100; "My Custom PEPPOL") + { + Caption = 'My Custom PEPPOL'; + Implementation = "PEPPOL30 Validation" = "PEPPOL30 Sales Validation", + "PEPPOL Posted Document Iterator" = "PEPPOL30 Sales Iterator", + "PEPPOL Document Info Provider" = "My PEPPOL Doc Info"; + } } ``` -Another example is that we want to do some additional validations after standard code is finished. For this example we'll update procedure `CheckSalesDocumentLine` +Then implement only the methods you need to change, delegating the rest to `"PEPPOL30"`: ```al -codeunit 50149 "XYZ PEPPOL30 Validation" implements "PEPPOL30 Validation" +codeunit 50101 "My PEPPOL Doc Info" implements "PEPPOL Document Info Provider" { var - PEPPOLValidation: Codeunit "PEPPOL30 Validation"; + StandardProvider: Codeunit "PEPPOL30"; - procedure CheckSalesDocument(SalesHeader: Record "Sales Header") + procedure GetGeneralInfoBIS(SalesHeader: Record "Sales Header"; var ID: Text; var IssueDate: Text; var InvoiceTypeCode: Text; var Note: Text; var TaxPointDate: Text; var DocumentCurrencyCode: Text; var AccountingCost: Text) begin - SalesHeader.TestField("External Document No."); + StandardProvider.GetGeneralInfoBIS(SalesHeader, ID, IssueDate, InvoiceTypeCode, Note, TaxPointDate, DocumentCurrencyCode, AccountingCost); + Note := 'Custom note: ' + Note; end; - procedure CheckSalesDocumentLines(SalesHeader: Record "Sales Header") + // Remaining methods delegate to StandardProvider... + procedure GetGeneralInfo(SalesHeader: Record "Sales Header"; var ID: Text; var IssueDate: Text; var InvoiceTypeCode: Text; var InvoiceTypeCodeListID: Text; var Note: Text; var TaxPointDate: Text; var DocumentCurrencyCode: Text; var DocumentCurrencyCodeListID: Text; var TaxCurrencyCode: Text; var TaxCurrencyCodeListID: Text; var AccountingCost: Text) begin - PEPPOLValidation.CheckSalesDocumentLines(SalesHeader); + StandardProvider.GetGeneralInfo(SalesHeader, ID, IssueDate, InvoiceTypeCode, InvoiceTypeCodeListID, Note, TaxPointDate, DocumentCurrencyCode, DocumentCurrencyCodeListID, TaxCurrencyCode, TaxCurrencyCodeListID, AccountingCost); end; - procedure CheckSalesDocumentLine(SalesLine: Record "Sales Line") + procedure GetInvoicePeriodInfo(var StartDate: Text; var EndDate: Text) begin - PEPPOLValidation.CheckSalesDocumentLine(SalesLine); - SalesLine.TestField("Tax Area Code"); + StandardProvider.GetInvoicePeriodInfo(StartDate, EndDate); + end; + + procedure GetOrderReferenceInfo(SalesHeader: Record "Sales Header"; var OrderReferenceID: Text) + begin + StandardProvider.GetOrderReferenceInfo(SalesHeader, OrderReferenceID); + end; + + procedure GetOrderReferenceInfoBIS(SalesHeader: Record "Sales Header"; var OrderReferenceID: Text) + begin + StandardProvider.GetOrderReferenceInfoBIS(SalesHeader, OrderReferenceID); end; - procedure CheckSalesInvoice(SalesInvoiceHeader: Record "Sales Invoice Header") + procedure GetContractDocRefInfo(SalesHeader: Record "Sales Header"; var ContractDocumentReferenceID: Text; var DocumentTypeCode: Text; var ContractRefDocTypeCodeListID: Text; var DocumentType: Text) begin - PEPPOLValidation.CheckSalesInvoice(SalesInvoiceHeader); + StandardProvider.GetContractDocRefInfo(SalesHeader, ContractDocumentReferenceID, DocumentTypeCode, ContractRefDocTypeCodeListID, DocumentType); end; - procedure CheckSalesCreditMemo(SalesCrMemoHeader: Record "Sales Cr.Memo Header") + procedure GetBuyerReference(SalesHeader: Record "Sales Header") BuyerReference: Text begin - PEPPOLValidation.CheckSalesCreditMemo(SalesCrMemoHeader); + BuyerReference := StandardProvider.GetBuyerReference(SalesHeader); end; - procedure CheckSalesLineTypeAndDescription(SalesLine: Record "Sales Line"): Boolean + procedure GetCrMemoBillingReferenceInfo(SalesCrMemoHeader: Record "Sales Cr.Memo Header"; var InvoiceDocRefID: Text; var InvoiceDocRefIssueDate: Text) begin - exit(PEPPOLValidation.CheckSalesLineTypeAndDescription(SalesLine)); + StandardProvider.GetCrMemoBillingReferenceInfo(SalesCrMemoHeader, InvoiceDocRefID, InvoiceDocRefIssueDate); end; } ``` diff --git a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al index 3ec3418a76..c899463801 100644 --- a/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al +++ b/src/Apps/W1/PaymentPractices/App/src/Pages/PaymentPracticeCard.Page.al @@ -99,7 +99,7 @@ page 687 "Payment Practice Card" ApplicationArea = All; SubPageLink = "Header No." = field("No."); UpdatePropagation = Both; - Visible = Rec."Lines Exist"; + Visible = true; } } } diff --git a/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Period.docx b/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Period.docx index ca412270ca..8bcbadc684 100644 Binary files a/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Period.docx and b/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Period.docx differ diff --git a/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Vendor Size.docx b/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Vendor Size.docx index c6c7db6c03..e57cebc5f9 100644 Binary files a/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Vendor Size.docx and b/src/Apps/W1/PaymentPractices/App/src/Reports/Payment Practice by Vendor Size.docx differ diff --git a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al index 944f710bd1..bd9c6ef507 100644 --- a/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al +++ b/src/Apps/W1/PaymentPractices/Test/src/PaymentPracticesUT.Codeunit.al @@ -571,6 +571,40 @@ codeunit 134197 "Payment Practices UT" PaymentPracticeLine.TestField("Modified Manually"); end; + [Test] + procedure PaymentPracticeCardLinesPartAlwaysVisible() + var + PaymentPracticeHeader: Record "Payment Practice Header"; + PaymentPracticeCard: TestPage "Payment Practice Card"; + VendorNo: Code[20]; + begin + // [FEATURE] [AI test 0.3] + // [SCENARIO 626602] Lines part on Payment Practice Card is visible after clicking Generate + Initialize(); + + // [GIVEN] Vendor "V" with company size and an entry in the period + VendorNo := PaymentPracticesLibrary.CreateVendorNoWithSizeAndExcl(CompanySizeCodes[1], false); + MockVendorInvoice(VendorNo, WorkDate(), WorkDate()); + + // [GIVEN] A payment practice header "PPH" + PaymentPracticesLibrary.CreatePaymentPracticeHeaderSimple(PaymentPracticeHeader); + + // [WHEN] Open the Payment Practice Card for "PPH" + PaymentPracticeCard.OpenEdit(); + PaymentPracticeCard.Filter.SetFilter("No.", Format(PaymentPracticeHeader."No.")); + + // [THEN] Lines part is visible even before generating + Assert.IsTrue(PaymentPracticeCard.Lines.Visible(), 'Lines part should be visible before generating.'); + + // [WHEN] Generate the payment practice lines + PaymentPracticeCard.Generate.Invoke(); + + // [THEN] Lines part is still visible after generating + Assert.IsTrue(PaymentPracticeCard.Lines.Visible(), 'Lines part should be visible after generating.'); + + PaymentPracticeCard.Close(); + end; + local procedure Initialize() begin LibraryTestInitialize.OnTestInitialize(Codeunit::"Payment Practices UT"); diff --git a/src/Apps/W1/PowerBIReports/App/.resources/Finance app.pbix b/src/Apps/W1/PowerBIReports/App/.resources/Finance app.pbix new file mode 100644 index 0000000000..9c77af828f Binary files /dev/null and b/src/Apps/W1/PowerBIReports/App/.resources/Finance app.pbix differ diff --git a/src/Apps/W1/PowerBIReports/App/.resources/Inventory Valuation app.pbix b/src/Apps/W1/PowerBIReports/App/.resources/Inventory Valuation app.pbix new file mode 100644 index 0000000000..97a479c020 Binary files /dev/null and b/src/Apps/W1/PowerBIReports/App/.resources/Inventory Valuation app.pbix differ diff --git a/src/Apps/W1/PowerBIReports/App/.resources/Inventory app.pbix b/src/Apps/W1/PowerBIReports/App/.resources/Inventory app.pbix new file mode 100644 index 0000000000..77fbcb1eb2 Binary files /dev/null and b/src/Apps/W1/PowerBIReports/App/.resources/Inventory app.pbix differ diff --git a/src/Apps/W1/PowerBIReports/App/.resources/Manufacturing app.pbix b/src/Apps/W1/PowerBIReports/App/.resources/Manufacturing app.pbix new file mode 100644 index 0000000000..2e000d54d4 Binary files /dev/null and b/src/Apps/W1/PowerBIReports/App/.resources/Manufacturing app.pbix differ diff --git a/src/Apps/W1/PowerBIReports/App/.resources/Projects app.pbix b/src/Apps/W1/PowerBIReports/App/.resources/Projects app.pbix new file mode 100644 index 0000000000..17253481eb Binary files /dev/null and b/src/Apps/W1/PowerBIReports/App/.resources/Projects app.pbix differ diff --git a/src/Apps/W1/PowerBIReports/App/.resources/Purchase app.pbix b/src/Apps/W1/PowerBIReports/App/.resources/Purchase app.pbix new file mode 100644 index 0000000000..ce4ea5426d Binary files /dev/null and b/src/Apps/W1/PowerBIReports/App/.resources/Purchase app.pbix differ diff --git a/src/Apps/W1/PowerBIReports/App/.resources/Sales app.pbix b/src/Apps/W1/PowerBIReports/App/.resources/Sales app.pbix new file mode 100644 index 0000000000..481c79c7a2 Binary files /dev/null and b/src/Apps/W1/PowerBIReports/App/.resources/Sales app.pbix differ diff --git a/src/Apps/W1/PowerBIReports/App/.resources/Sustainability app.pbix b/src/Apps/W1/PowerBIReports/App/.resources/Sustainability app.pbix new file mode 100644 index 0000000000..a729166325 Binary files /dev/null and b/src/Apps/W1/PowerBIReports/App/.resources/Sustainability app.pbix differ diff --git a/src/Apps/W1/PowerBIReports/App/Core/Codeunits/Initialization.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/Codeunits/Initialization.Codeunit.al index 3c313c3071..46d9510504 100644 --- a/src/Apps/W1/PowerBIReports/App/Core/Codeunits/Initialization.Codeunit.al +++ b/src/Apps/W1/PowerBIReports/App/Core/Codeunits/Initialization.Codeunit.al @@ -25,9 +25,11 @@ codeunit 36951 Initialization DimensionSetEntriesJobQueueDescriptionLbl: Label 'Update Power BI Dimension Set Entries'; procedure SetupDefaultsForPowerBIReportsIfNotInitialized() + var + PBISetup: Record "PowerBI Reports Setup"; begin InsertGuidedExperience(); - InitializePBISetup(); + PBISetup.GetOrCreate(); InitializePBIWorkingDays(); InitializeStartingEndingDates(); InitializeDimensionSetEntryCollectionJobQueueEntry(); @@ -59,16 +61,6 @@ codeunit 36951 Initialization FinanceInstallationHandler.SetupDefaultsForPowerBIReportsIfNotInitialized(); end; - local procedure InitializePBISetup() - var - PBISetup: Record "PowerBI Reports Setup"; - begin - if not PBISetup.Get() then begin - PBISetup.Init(); - PBISetup.Insert(); - end; - end; - local procedure InitializeStartingEndingDates() var AccountingPeriod: Record "Accounting Period"; diff --git a/src/Apps/W1/PowerBIReports/App/Core/Codeunits/PowerBIReportSetup.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/Codeunits/PowerBIReportSetup.Codeunit.al index 241f76627d..62f289c2e0 100644 --- a/src/Apps/W1/PowerBIReports/App/Core/Codeunits/PowerBIReportSetup.Codeunit.al +++ b/src/Apps/W1/PowerBIReports/App/Core/Codeunits/PowerBIReportSetup.Codeunit.al @@ -7,6 +7,7 @@ namespace Microsoft.PowerBIReports; using System.Environment.Configuration; using System.Globalization; using System.Integration.PowerBI; +using System.Utilities; codeunit 36962 "Power BI Report Setup" { @@ -26,6 +27,47 @@ codeunit 36962 "Power BI Report Setup" end; end; + /// + /// Ensures that everything related to the specified Power BI report setup is properly configured, or directs to the appropriate setup pages. It errors if after the different prompts the report is not set up or in the process of being deployed. Typically used as a validation step before opening an embedded Power BI report page. + /// + /// The Power BI report setup to validate. + /// The GUID of the Power BI report as configured in the setup. + procedure OpenPowerBIEmbeddedReportPageValidation(PBIReportSetup: Enum "PBI Report Setup"): Guid + var + PowerBIAssistedSetup: Page "PowerBI Assisted Setup"; + DeploySelectionPage: Page "PBI Report Deploy. Selection"; + ConfiguredReportId: Guid; + ReportNotSetupErr: Label 'Your report has not been setup in PowerBI Reports Setup. You need to set up this report in order to view it.', Comment = '%1 = report name'; + begin + EnsureUserAcceptedPowerBITerms(); + ConfiguredReportId := GetConfiguredReportId(PBIReportSetup); + if not IsNullGuid(ConfiguredReportId) then + exit(ConfiguredReportId); + PromptOpeningReportDeploymentsWhenInProgress(PBIReportSetup); + if PowerBIAssistedSetup.RunModal() = Action::OK then + if PowerBIAssistedSetup.IsDeployOOBReportsSelected() then + DeploySelectionPage.RunModal(); + ConfiguredReportId := GetConfiguredReportId(PBIReportSetup); + if not IsNullGuid(ConfiguredReportId) then + exit(ConfiguredReportId); + PromptOpeningReportDeploymentsWhenInProgress(PBIReportSetup); + Error(ReportNotSetupErr) + end; + + local procedure GetConfiguredReportId(PBIReportSetup: Enum "PBI Report Setup"): Guid + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + RecRef: RecordRef; + ReportSetup: Interface "PBI Report Setup"; + ConfiguredReportId: Guid; + begin + PowerBIReportsSetup.GetOrCreate(); + RecRef.GetTable(PowerBIReportsSetup); + ReportSetup := PBIReportSetup; + ConfiguredReportId := RecRef.Field(ReportSetup.GetSetupReportIdFieldNo()).Value(); + exit(ConfiguredReportId) + end; + procedure GetReportIdAndEnsureSetup(ReportName: Text; FieldId: Integer) ReportId: Guid var AssistedSetup: Page "PowerBI Assisted Setup"; @@ -40,6 +82,47 @@ codeunit 36962 "Power BI Report Setup" end; end; + procedure FindReportSetup(DeployableReportType: Enum "Power BI Deployable Report"; var ReportSetup: Interface "PBI Report Setup"): Boolean + var + Ordinal: Integer; + begin + foreach Ordinal in Enum::"PBI Report Setup".Ordinals() do begin + ReportSetup := Enum::"PBI Report Setup".FromInteger(Ordinal); + if ReportSetup.GetDeployableReportType() = DeployableReportType then + exit(true); + end; + Clear(ReportSetup); + exit(false); + end; + + local procedure IsReportBeingDeployed(ReportSetup: Interface "PBI Report Setup"): Boolean + var + PowerBIDeployment: Record "Power BI Deployment"; + begin + if not PowerBIDeployment.Get(ReportSetup.GetDeployableReportType()) then + exit(false); + PowerBIDeployment.GetUploadStatus(); + exit(not (PowerBIDeployment.GetUploadStatus() in [ + Enum::"Power BI Upload Status"::Completed, + Enum::"Power BI Upload Status"::Failed, + Enum::"Power BI Upload Status"::Skipped, + Enum::"Power BI Upload Status"::PendingDeletion])); + end; + + local procedure PromptOpeningReportDeploymentsWhenInProgress(ReportSetup: Interface "PBI Report Setup"): Boolean + var + ConfirmMgt: Codeunit "Confirm Management"; + ReportDeployingQst: Label 'Your %1 report is being deployed to Power BI. Would you like to open the Power BI Report Deployments page to track the status?', Comment = '%1 = report name'; + DeployableReport: Interface "Power BI Deployable Report"; + begin + DeployableReport := ReportSetup.GetDeployableReportType(); + if IsReportBeingDeployed(ReportSetup) then begin + if ConfirmMgt.GetResponse(StrSubstNo(ReportDeployingQst, DeployableReport.GetReportName())) then + Page.Run(Page::"Power BI Report Deployments"); + Error(''); + end; + end; + local procedure GetReportId(FieldId: Integer): Guid var PowerBiReportsSetup: Record "PowerBI Reports Setup"; diff --git a/src/Apps/W1/PowerBIReports/App/Core/PBIReportDeploymentsExt.PageExt.al b/src/Apps/W1/PowerBIReports/App/Core/PBIReportDeploymentsExt.PageExt.al new file mode 100644 index 0000000000..9d96d90ffe --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/PBIReportDeploymentsExt.PageExt.al @@ -0,0 +1,68 @@ +namespace Microsoft.PowerBIReports; + +using System.Integration.PowerBI; + +pageextension 36965 "PBI Report Deployments Ext." extends "Power BI Report Deployments" +{ + layout + { + addafter(ReportName) + { + field(SetupConfigured; IsSetupConfigured) + { + ApplicationArea = All; + Caption = 'Linked in Setup'; + ToolTip = 'Specifies whether a Power BI report has been configured for this app in the Power BI Reports Setup.'; + Editable = false; + } + } + } + + actions + { + addlast(NavigateActions) + { + action(OpenPowerBIReportsSetup) + { + ApplicationArea = All; + Caption = 'Power BI Reports Setup'; + Image = Setup; + RunObject = page "PowerBI Reports Setup"; + ToolTip = 'Opens the Power BI Reports Setup page.'; + } + } + addlast(Category_Category2) + { + actionref(PowerBIReportsSetup_Promoted; OpenPowerBIReportsSetup) + { + } + } + } + + trigger OnAfterGetRecord() + begin + IsSetupConfigured := GetIsSetupConfigured(); + end; + + var + IsSetupConfigured: Boolean; + + local procedure GetIsSetupConfigured(): Boolean + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + SetupHelper: Codeunit "Power BI Report Setup"; + RecRef: RecordRef; + FldRef: FieldRef; + ReportSetup: Interface "PBI Report Setup"; + begin + if not PowerBIReportsSetup.Get() then + exit(false); + + if not SetupHelper.FindReportSetup(Rec."Report Id", ReportSetup) then + exit(false); + + RecRef.GetTable(PowerBIReportsSetup); + FldRef := RecRef.Field(ReportSetup.GetSetupReportIdFieldNo()); + exit(not IsNullGuid(FldRef.Value())); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBIAssistedSetup.Page.al b/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBIAssistedSetup.Page.al index f41ff8997f..d0a0711a69 100644 --- a/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBIAssistedSetup.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBIAssistedSetup.Page.al @@ -208,6 +208,22 @@ page 36950 "PowerBI Assisted Setup" } } + group(StepReportChoice) + { + Visible = CurrentStep = Steps::ReportChoice; + group(ReportChoiceIntro) + { + Caption = 'Report Setup'; + InstructionalText = 'You can manually configure which Power BI reports to use for each area, or deploy our out-of-the-box reports directly to your Power BI workspace.'; + } + field(ReportSetupChoice; ReportSetupChoice) + { + ApplicationArea = All; + Caption = 'I would like to'; + ToolTip = 'Specifies whether to manually configure report settings or deploy pre-built reports.'; + OptionCaption = 'Deploy out-of-the-box reports,Configure report settings'; + } + } group(Step5) { Visible = CurrentStep = Steps::Setting; @@ -760,6 +776,11 @@ page 36950 "PowerBI Assisted Setup" trigger OnAction() begin + if (CurrentStep = Steps::ReportChoice) and (ReportSetupChoice = ReportSetupChoice::"Deploy out-of-the-box reports") then begin + GuidedExperience.CompleteAssistedSetup(ObjectType::Page, Page::"PowerBI Assisted Setup"); + CurrPage.Close(); + exit; + end; TakeStep(1); end; } @@ -789,7 +810,7 @@ page 36950 "PowerBI Assisted Setup" TimeZoneSelection: Codeunit "Time Zone Selection"; EnvironmentInformation: Codeunit "Environment Information"; SetupHelper: Codeunit "Power BI Report Setup"; - Steps: Option Intro,DateTableConfig,UTCOffset,WorkingDays,Setting,Finish; + Steps: Option Intro,DateTableConfig,UTCOffset,WorkingDays,ReportChoice,Setting,Finish; PrevStep: Option; CurrentStep: Option; BackEnabled: Boolean; @@ -815,10 +836,13 @@ page 36950 "PowerBI Assisted Setup" ShowLessTxt: Label 'Show Less'; AdminPermissionRequiredErr: Label 'Setting up Power BI requires the ''%1'' permission set (or equivalent) that your account doesn''t have. Ask your administrator to assign the permission set to you.', Comment = '%1 = permission set name'; PermisionSetNameTok: Label 'Power BI Core Admin', Locked = true; + IsEvalCompany: Boolean; + ReportSetupChoice: Option "Deploy out-of-the-box reports","Configure report settings"; trigger OnOpenPage() var UserSetup: Record "User Setup"; + CurrentCompany: Record Company; PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin if not PowerBIReportsSetup.WritePermission() then @@ -830,6 +854,9 @@ page 36950 "PowerBI Assisted Setup" if NavApp.GetCurrentModuleInfo(AppInfo) then AssistedSetupComplete := GuidedExperience.IsAssistedSetupComplete(ObjectType::Page, Page::"PowerBI Assisted Setup"); + CurrentCompany.Get(CompanyName()); + IsEvalCompany := CurrentCompany."Evaluation Company"; + if UserSetup.Get(UserId()) then TestEmailAddress := UserSetup."E-Mail"; @@ -855,6 +882,11 @@ page 36950 "PowerBI Assisted Setup" PrevStep := CurrentStep; CurrentStep := CurrentStep + Step; + + // Skip ReportChoice for non-evaluation companies + if (CurrentStep = Steps::ReportChoice) and (not IsEvalCompany) then + CurrentStep := CurrentStep + Step; + NextEnabled := false; BackEnabled := true; FinishEnabled := false; @@ -869,13 +901,12 @@ page 36950 "PowerBI Assisted Setup" if CalendarType > 0 then NextEnabled := true; Steps::UTCOffset: - NextEnabled := true; Steps::WorkingDays: - + NextEnabled := true; + Steps::ReportChoice: NextEnabled := true; Steps::Setting: - NextEnabled := true; Steps::Finish: begin @@ -929,4 +960,9 @@ page 36950 "PowerBI Assisted Setup" end; end; end; + + procedure IsDeployOOBReportsSelected(): Boolean + begin + exit(IsEvalCompany and (ReportSetupChoice = ReportSetupChoice::"Deploy out-of-the-box reports")); + end; } diff --git a/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBIReportsSetup.Page.al b/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBIReportsSetup.Page.al index 1cef0db07c..fa59af78aa 100644 --- a/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBIReportsSetup.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBIReportsSetup.Page.al @@ -5,6 +5,7 @@ namespace Microsoft.PowerBIReports; using Microsoft.Finance.PowerBIReports; +using Microsoft.Inventory.Analysis; using System.DateTime; using System.Environment; @@ -580,6 +581,14 @@ page 36951 "PowerBI Reports Setup" Image = CodesList; RunObject = page "PBI Close Income Stmt. SC."; } + action(ABCAnalysisSetup) + { + ApplicationArea = All; + Caption = 'ABC Analysis Setup'; + ToolTip = 'Set up your ABC analysis thresholds in the Power BI Inventory reports.'; + Image = Percentage; + RunObject = page "ABC Analysis Setup"; + } } area(Promoted) @@ -598,6 +607,9 @@ page 36951 "PowerBI Reports Setup" actionref(CloseIncomeStatementSourceCodes_Promoted; CloseIncomeStatementSourceCodes) { } + actionref(ABCAnalysisSetup_Promoted; ABCAnalysisSetup) + { + } } } } diff --git a/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBISelectionLookup.Page.al b/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBISelectionLookup.Page.al index 27ca8a2f75..bbce68a4ae 100644 --- a/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBISelectionLookup.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Core/Pages/PowerBISelectionLookup.Page.al @@ -8,6 +8,7 @@ using System.Integration.PowerBI; page 36963 "Power BI Selection Lookup" { + ApplicationArea = All; Caption = 'Select Power BI Element'; DeleteAllowed = false; InsertAllowed = false; @@ -52,10 +53,10 @@ page 36963 "Power BI Selection Lookup" trigger OnOpenPage() var - PowerBISelectionElement: Record "Power BI Selection Element"; + TempPowerBISelectionElement: Record "Power BI Selection Element"; SelectTxt: Label 'Select Power BI %1', Comment = '%1 = Type'; begin - if Evaluate(PowerBISelectionElement.Type, Rec.GetFilter(Type)) then - CurrPage.Caption := StrSubstNo(SelectTxt, PowerBISelectionElement.Type); + if Evaluate(TempPowerBISelectionElement.Type, Rec.GetFilter(Type)) then + CurrPage.Caption := StrSubstNo(SelectTxt, TempPowerBISelectionElement.Type); end; } \ No newline at end of file diff --git a/src/Apps/W1/PowerBIReports/App/Core/PermissionSets/PowerBiReportBasic.PermissionSet.al b/src/Apps/W1/PowerBIReports/App/Core/PermissionSets/PowerBiReportBasic.PermissionSet.al index 9a8ad3d7f5..2b2f92823b 100644 --- a/src/Apps/W1/PowerBIReports/App/Core/PermissionSets/PowerBiReportBasic.PermissionSet.al +++ b/src/Apps/W1/PowerBIReports/App/Core/PermissionSets/PowerBiReportBasic.PermissionSet.al @@ -49,6 +49,7 @@ permissionset 36951 "PowerBi Report Basic" page "Working Days" = X, page "Working Days Setup" = X, page "Working Days Subform" = X, + page "PBI Report Deploy. Selection" = X, page "Account Categories" = X, query "Account Categories" = X, query "Resources - PBI API" = X, diff --git a/src/Apps/W1/PowerBIReports/App/Core/PowerBIDeployableReport.EnumExt.al b/src/Apps/W1/PowerBIReports/App/Core/PowerBIDeployableReport.EnumExt.al new file mode 100644 index 0000000000..45e4151112 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/PowerBIDeployableReport.EnumExt.al @@ -0,0 +1,41 @@ +namespace Microsoft.PowerBIReports; +using System.Integration.PowerBI; + +enumextension 36950 "Power BI Deployable Report" extends "Power BI Deployable Report" +{ + value(36950; "Finance App") + { + Caption = 'Finance'; + Implementation = "Power BI Deployable Report" = "PBI Finance App"; + } + value(36951; "Sales App") + { + Caption = 'Sales'; + Implementation = "Power BI Deployable Report" = "PBI Sales App"; + } + value(36952; "Purchases App") + { + Caption = 'Purchases'; + Implementation = "Power BI Deployable Report" = "PBI Purchases App"; + } + value(36953; "Inventory App") + { + Caption = 'Inventory'; + Implementation = "Power BI Deployable Report" = "PBI Inventory App"; + } + value(36954; "Inventory Valuation App") + { + Caption = 'Inventory Valuation'; + Implementation = "Power BI Deployable Report" = "PBI Inventory Val. App"; + } + value(36955; "Manufacturing App") + { + Caption = 'Manufacturing'; + Implementation = "Power BI Deployable Report" = "PBI Manufacturing App"; + } + value(36956; "Projects App") + { + Caption = 'Projects'; + Implementation = "Power BI Deployable Report" = "PBI Projects App"; + } +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/PowerBISubscribers.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/PowerBISubscribers.Codeunit.al new file mode 100644 index 0000000000..08bfb27592 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/PowerBISubscribers.Codeunit.al @@ -0,0 +1,29 @@ +namespace Microsoft.PowerBIReports; +using System.Integration.PowerBI; + +codeunit 36959 "Power BI Subscribers" +{ + [EventSubscriber(ObjectType::Codeunit, Codeunit::"PBI Deployment Events", OnReportDeployed, '', false, false)] + local procedure OnReportDeployed(var Report: Interface "Power BI Uploadable Report"; DeployableReportType: Enum "Power BI Deployable Report") + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + SetupHelper: Codeunit "Power BI Report Setup"; + RecRef: RecordRef; + UploadTracker: Interface "Power BI Upload Tracker"; + ReportSetup: Interface "PBI Report Setup"; + ReportName: Text[200]; + begin + if not SetupHelper.FindReportSetup(DeployableReportType, ReportSetup) then + exit; + + Report.GetUploadTracker(UploadTracker); + UploadTracker.Load(Report.GetReportKey()); + ReportName := CopyStr(UploadTracker.GetUploadedReportName(), 1, MaxStrLen(ReportName)); + + PowerBIReportsSetup.GetOrCreate(); + RecRef.GetTable(PowerBIReportsSetup); + RecRef.Field(ReportSetup.GetSetupReportIdFieldNo()).Value := UploadTracker.GetUploadedReportId(); + RecRef.Field(ReportSetup.GetSetupReportNameFieldNo()).Value := ReportName; + RecRef.Modify(); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIDeploymentBufferExt.TableExt.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIDeploymentBufferExt.TableExt.al new file mode 100644 index 0000000000..28cbc36fbb --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIDeploymentBufferExt.TableExt.al @@ -0,0 +1,19 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.PowerBIReports; + +using System.Integration.PowerBI; + +tableextension 36962 "PBI Deployment Buffer Ext." extends "Power BI Deployment Buffer" +{ + fields + { + field(36950; Deploy; Boolean) + { + Caption = 'Deploy'; + DataClassification = SystemMetadata; + } + } +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIFinanceApp.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIFinanceApp.Codeunit.al new file mode 100644 index 0000000000..1554f83f47 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIFinanceApp.Codeunit.al @@ -0,0 +1,50 @@ +namespace Microsoft.PowerBIReports; +using System.Environment; +using System.Integration.PowerBI; + +codeunit 36963 "PBI Finance App" implements "Power BI Deployable Report", "PBI Report Setup" +{ + Access = Internal; + + procedure GetReportName(): Text[200] + begin + exit('Finance'); + end; + + procedure GetStream(var InStr: InStream) + begin + NavApp.GetResource('Finance app.pbix', InStr); + end; + + procedure GetVersion(): Integer + begin + exit(1); + end; + + procedure GetDatasetParameters() Parameters: Dictionary of [Text, Text] + var + EnvironmentInformation: Codeunit "Environment Information"; + begin + Parameters.Add('COMPANY', CompanyName()); + Parameters.Add('ENVIRONMENT', EnvironmentInformation.GetEnvironmentName()); + end; + + procedure GetDeployableReportType(): Enum "Power BI Deployable Report" + begin + exit(Enum::"Power BI Deployable Report"::"Finance App"); + end; + + procedure GetSetupReportIdFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Finance Report Id")); + end; + + procedure GetSetupReportNameFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Finance Report Name")); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIInventoryApp.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIInventoryApp.Codeunit.al new file mode 100644 index 0000000000..551a21d4e7 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIInventoryApp.Codeunit.al @@ -0,0 +1,50 @@ +namespace Microsoft.PowerBIReports; +using System.Environment; +using System.Integration.PowerBI; + +codeunit 36966 "PBI Inventory App" implements "Power BI Deployable Report", "PBI Report Setup" +{ + Access = Internal; + + procedure GetReportName(): Text[200] + begin + exit('Inventory'); + end; + + procedure GetStream(var InStr: InStream) + begin + NavApp.GetResource('Inventory app.pbix', InStr); + end; + + procedure GetVersion(): Integer + begin + exit(1); + end; + + procedure GetDatasetParameters() Parameters: Dictionary of [Text, Text] + var + EnvironmentInformation: Codeunit "Environment Information"; + begin + Parameters.Add('COMPANY', CompanyName()); + Parameters.Add('ENVIRONMENT', EnvironmentInformation.GetEnvironmentName()); + end; + + procedure GetDeployableReportType(): Enum "Power BI Deployable Report" + begin + exit(Enum::"Power BI Deployable Report"::"Inventory App"); + end; + + procedure GetSetupReportIdFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Inventory Report Id")); + end; + + procedure GetSetupReportNameFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Inventory Report Name")); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIInventoryValApp.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIInventoryValApp.Codeunit.al new file mode 100644 index 0000000000..b368e53f1d --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIInventoryValApp.Codeunit.al @@ -0,0 +1,50 @@ +namespace Microsoft.PowerBIReports; +using System.Environment; +using System.Integration.PowerBI; + +codeunit 36967 "PBI Inventory Val. App" implements "Power BI Deployable Report", "PBI Report Setup" +{ + Access = Internal; + + procedure GetReportName(): Text[200] + begin + exit('Inventory Valuation'); + end; + + procedure GetStream(var InStr: InStream) + begin + NavApp.GetResource('Inventory Valuation app.pbix', InStr); + end; + + procedure GetVersion(): Integer + begin + exit(1); + end; + + procedure GetDatasetParameters() Parameters: Dictionary of [Text, Text] + var + EnvironmentInformation: Codeunit "Environment Information"; + begin + Parameters.Add('COMPANY', CompanyName()); + Parameters.Add('ENVIRONMENT', EnvironmentInformation.GetEnvironmentName()); + end; + + procedure GetDeployableReportType(): Enum "Power BI Deployable Report" + begin + exit(Enum::"Power BI Deployable Report"::"Inventory Valuation App"); + end; + + procedure GetSetupReportIdFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Inventory Val. Report Id")); + end; + + procedure GetSetupReportNameFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Inventory Val. Report Name")); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIManufacturingApp.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIManufacturingApp.Codeunit.al new file mode 100644 index 0000000000..5c1889b4e6 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIManufacturingApp.Codeunit.al @@ -0,0 +1,50 @@ +namespace Microsoft.PowerBIReports; +using System.Environment; +using System.Integration.PowerBI; + +codeunit 36968 "PBI Manufacturing App" implements "Power BI Deployable Report", "PBI Report Setup" +{ + Access = Internal; + + procedure GetReportName(): Text[200] + begin + exit('Manufacturing'); + end; + + procedure GetStream(var InStr: InStream) + begin + NavApp.GetResource('Manufacturing app.pbix', InStr); + end; + + procedure GetVersion(): Integer + begin + exit(1); + end; + + procedure GetDatasetParameters() Parameters: Dictionary of [Text, Text] + var + EnvironmentInformation: Codeunit "Environment Information"; + begin + Parameters.Add('COMPANY', CompanyName()); + Parameters.Add('ENVIRONMENT', EnvironmentInformation.GetEnvironmentName()); + end; + + procedure GetDeployableReportType(): Enum "Power BI Deployable Report" + begin + exit(Enum::"Power BI Deployable Report"::"Manufacturing App"); + end; + + procedure GetSetupReportIdFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + end; + + procedure GetSetupReportNameFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Manufacturing Report Name")); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIProjectsApp.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIProjectsApp.Codeunit.al new file mode 100644 index 0000000000..b18ce250ca --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIProjectsApp.Codeunit.al @@ -0,0 +1,50 @@ +namespace Microsoft.PowerBIReports; +using System.Environment; +using System.Integration.PowerBI; + +codeunit 36969 "PBI Projects App" implements "Power BI Deployable Report", "PBI Report Setup" +{ + Access = Internal; + + procedure GetReportName(): Text[200] + begin + exit('Projects'); + end; + + procedure GetStream(var InStr: InStream) + begin + NavApp.GetResource('Projects app.pbix', InStr); + end; + + procedure GetVersion(): Integer + begin + exit(1); + end; + + procedure GetDatasetParameters() Parameters: Dictionary of [Text, Text] + var + EnvironmentInformation: Codeunit "Environment Information"; + begin + Parameters.Add('COMPANY', CompanyName()); + Parameters.Add('ENVIRONMENT', EnvironmentInformation.GetEnvironmentName()); + end; + + procedure GetDeployableReportType(): Enum "Power BI Deployable Report" + begin + exit(Enum::"Power BI Deployable Report"::"Projects App"); + end; + + procedure GetSetupReportIdFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Projects Report Id")); + end; + + procedure GetSetupReportNameFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Projects Report Name")); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIPurchasesApp.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIPurchasesApp.Codeunit.al new file mode 100644 index 0000000000..2f97c34cea --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIPurchasesApp.Codeunit.al @@ -0,0 +1,50 @@ +namespace Microsoft.PowerBIReports; +using System.Environment; +using System.Integration.PowerBI; + +codeunit 36965 "PBI Purchases App" implements "Power BI Deployable Report", "PBI Report Setup" +{ + Access = Internal; + + procedure GetReportName(): Text[200] + begin + exit('Purchases'); + end; + + procedure GetStream(var InStr: InStream) + begin + NavApp.GetResource('Purchase app.pbix', InStr); + end; + + procedure GetVersion(): Integer + begin + exit(1); + end; + + procedure GetDatasetParameters() Parameters: Dictionary of [Text, Text] + var + EnvironmentInformation: Codeunit "Environment Information"; + begin + Parameters.Add('COMPANY', CompanyName()); + Parameters.Add('ENVIRONMENT', EnvironmentInformation.GetEnvironmentName()); + end; + + procedure GetDeployableReportType(): Enum "Power BI Deployable Report" + begin + exit(Enum::"Power BI Deployable Report"::"Purchases App"); + end; + + procedure GetSetupReportIdFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Purchases Report Id")); + end; + + procedure GetSetupReportNameFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Purchases Report Name")); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportDeploySelection.Page.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportDeploySelection.Page.al new file mode 100644 index 0000000000..b378caf71b --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportDeploySelection.Page.al @@ -0,0 +1,146 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.PowerBIReports; + +using System.Integration.PowerBI; + +page 36968 "PBI Report Deploy. Selection" +{ + PageType = NavigatePage; + Caption = 'Deploy Power BI Reports'; + SourceTable = "Power BI Deployment Buffer"; + SourceTableTemporary = true; + InsertAllowed = false; + DeleteAllowed = false; + + layout + { + area(Content) + { + group(IntroGroup) + { + ShowCaption = false; + InstructionalText = 'Select which reports to deploy to your Power BI workspace. Reports that are already scheduled or deployed cannot be selected.'; + } + repeater(Reports) + { + field(Deploy; Rec.Deploy) + { + ApplicationArea = All; + Caption = 'Deploy'; + ToolTip = 'Specifies whether to deploy this report.'; + Editable = DeployEditable; + } + field(ReportName; Rec."Report Name") + { + ApplicationArea = All; + Editable = false; + Caption = 'Report Name'; + ToolTip = 'Specifies the name of the Power BI report.'; + } + field(DeploymentStatus; Rec."Deployment Status") + { + ApplicationArea = All; + Editable = false; + Caption = 'Status'; + ToolTip = 'Specifies the current deployment status.'; + StyleExpr = StatusStyle; + } + } + } + } + + actions + { + area(Processing) + { + action(DeployAction) + { + ApplicationArea = All; + Caption = 'Deploy'; + Image = Approve; + InFooterBar = true; + ToolTip = 'Deploy the selected reports to your Power BI workspace.'; + + trigger OnAction() + begin + DeploySelectedReports(); + CurrPage.Close(); + end; + } + action(SkipAction) + { + ApplicationArea = All; + Caption = 'Skip'; + Image = Cancel; + InFooterBar = true; + ToolTip = 'Skip report deployment.'; + + trigger OnAction() + begin + CurrPage.Close(); + end; + } + } + } + + trigger OnOpenPage() + begin + Rec.LoadReports(); + if Rec.FindSet() then + repeat + if Rec."Deployment Status" = Enum::"Power BI Deployment Status"::"Not Installed" then begin + Rec.Deploy := true; + Rec.Modify(); + end; + until Rec.Next() = 0; + if Rec.FindFirst() then; + end; + + trigger OnAfterGetRecord() + begin + DeployEditable := Rec."Deployment Status" = Enum::"Power BI Deployment Status"::"Not Installed"; + + case Rec."Deployment Status" of + Enum::"Power BI Deployment Status"::"Not Installed": + StatusStyle := 'Standard'; + Enum::"Power BI Deployment Status"::Error: + StatusStyle := 'Unfavorable'; + Enum::"Power BI Deployment Status"::"Up to Date": + StatusStyle := 'Favorable'; + Enum::"Power BI Deployment Status"::"Update Available": + StatusStyle := 'Attention'; + else + StatusStyle := 'Ambiguous'; + end; + end; + + var + DeployEditable: Boolean; + StatusStyle: Text; + + local procedure DeploySelectedReports() + var + PowerBIDeployment: Record "Power BI Deployment"; + PowerBIServiceMgt: Codeunit "Power BI Service Mgt."; + HasSelections: Boolean; + begin + Rec.SetRange(Deploy, true); + if Rec.FindSet() then + repeat + if not PowerBIDeployment.Get(Rec."Report Id") then begin + PowerBIDeployment.Init(); + PowerBIDeployment."Report Id" := Rec."Report Id"; + PowerBIDeployment.Insert(true); + HasSelections := true; + end; + until Rec.Next() = 0; + + if HasSelections then + PowerBIServiceMgt.SynchronizeReportsInBackground(''); + + Rec.SetRange(Deploy); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportSetup.Enum.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportSetup.Enum.al new file mode 100644 index 0000000000..1aa20f6c2d --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportSetup.Enum.al @@ -0,0 +1,35 @@ +namespace Microsoft.PowerBIReports; + +enum 36961 "PBI Report Setup" implements "PBI Report Setup" +{ + Extensible = true; + + value(36960; "Finance App") + { + Implementation = "PBI Report Setup" = "PBI Finance App"; + } + value(36961; "Sales App") + { + Implementation = "PBI Report Setup" = "PBI Sales App"; + } + value(36962; "Purchases App") + { + Implementation = "PBI Report Setup" = "PBI Purchases App"; + } + value(36963; "Inventory App") + { + Implementation = "PBI Report Setup" = "PBI Inventory App"; + } + value(36964; "Inventory Valuation App") + { + Implementation = "PBI Report Setup" = "PBI Inventory Val. App"; + } + value(36965; "Manufacturing App") + { + Implementation = "PBI Report Setup" = "PBI Manufacturing App"; + } + value(36966; "Projects App") + { + Implementation = "PBI Report Setup" = "PBI Projects App"; + } +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportSetup.Interface.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportSetup.Interface.al new file mode 100644 index 0000000000..3448f910d4 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBIReportSetup.Interface.al @@ -0,0 +1,9 @@ +namespace Microsoft.PowerBIReports; +using System.Integration.PowerBI; + +interface "PBI Report Setup" +{ + procedure GetDeployableReportType(): Enum "Power BI Deployable Report"; + procedure GetSetupReportIdFieldNo(): Integer; + procedure GetSetupReportNameFieldNo(): Integer; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBISalesApp.Codeunit.al b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBISalesApp.Codeunit.al new file mode 100644 index 0000000000..fe229474e4 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/App/Core/ReportDeployments/PBISalesApp.Codeunit.al @@ -0,0 +1,50 @@ +namespace Microsoft.PowerBIReports; +using System.Environment; +using System.Integration.PowerBI; + +codeunit 36964 "PBI Sales App" implements "Power BI Deployable Report", "PBI Report Setup" +{ + Access = Internal; + + procedure GetReportName(): Text[200] + begin + exit('Sales'); + end; + + procedure GetStream(var InStr: InStream) + begin + NavApp.GetResource('Sales app.pbix', InStr); + end; + + procedure GetVersion(): Integer + begin + exit(1); + end; + + procedure GetDatasetParameters() Parameters: Dictionary of [Text, Text] + var + EnvironmentInformation: Codeunit "Environment Information"; + begin + Parameters.Add('COMPANY', CompanyName()); + Parameters.Add('ENVIRONMENT', EnvironmentInformation.GetEnvironmentName()); + end; + + procedure GetDeployableReportType(): Enum "Power BI Deployable Report" + begin + exit(Enum::"Power BI Deployable Report"::"Sales App"); + end; + + procedure GetSetupReportIdFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Sales Report Id")); + end; + + procedure GetSetupReportNameFieldNo(): Integer + var + PowerBIReportsSetup: Record "PowerBI Reports Setup"; + begin + exit(PowerBIReportsSetup.FieldNo("Sales Report Name")); + end; +} diff --git a/src/Apps/W1/PowerBIReports/App/Core/Tables/PowerBIReportsSetup.Table.al b/src/Apps/W1/PowerBIReports/App/Core/Tables/PowerBIReportsSetup.Table.al index 166762d317..c84c14bf62 100644 --- a/src/Apps/W1/PowerBIReports/App/Core/Tables/PowerBIReportsSetup.Table.al +++ b/src/Apps/W1/PowerBIReports/App/Core/Tables/PowerBIReportsSetup.Table.al @@ -147,6 +147,13 @@ table 36951 "PowerBI Reports Setup" } } + procedure GetOrCreate() + begin + if Rec.Get() then + exit; + Rec.Init(); + Rec.Insert(); + end; procedure GetTimeZoneDisplayName(): Text[250] var diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AgedPayablesBackDating.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AgedPayablesBackDating.Page.al index cb05260fc3..9f00d99a30 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AgedPayablesBackDating.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AgedPayablesBackDating.Page.al @@ -48,11 +48,8 @@ page 36994 "Aged Payables (Back Dating)" ReportPageLbl: Label 'ReportSection904474b579cc92816425', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AgedReceivablesBackDating.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AgedReceivablesBackDating.Page.al index 007fbcdba9..0b5244ef1e 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AgedReceivablesBackDating.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AgedReceivablesBackDating.Page.al @@ -48,11 +48,8 @@ page 36993 "Aged Receivables (Back Dating)" ReportPageLbl: Label 'ReportSectionfef66fa3cf79c6d85930', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AverageCollectionPeriod.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AverageCollectionPeriod.Page.al index 105ac32b24..bda1d7f118 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AverageCollectionPeriod.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/AverageCollectionPeriod.Page.al @@ -48,11 +48,8 @@ page 36992 "Average Collection Period" ReportPageLbl: Label 'ReportSectionb1d1e3d33a031ad3b0ed', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/BalanceSheetbyMonth.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/BalanceSheetbyMonth.Page.al index 4a2d7a3da5..dc2bb48ecb 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/BalanceSheetbyMonth.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/BalanceSheetbyMonth.Page.al @@ -50,11 +50,8 @@ page 36986 "Balance Sheet by Month" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/BudgetComparison.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/BudgetComparison.Page.al index df397ea34f..16edde69f4 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/BudgetComparison.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/BudgetComparison.Page.al @@ -48,11 +48,8 @@ page 36987 "Budget Comparison" ReportPageLbl: Label 'ReportSection64d670dfa9da1a5b7033', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/DetailedCustLedgerEntries.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/DetailedCustLedgerEntries.Page.al index 54ce0b8eb5..3c2e27c3ad 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/DetailedCustLedgerEntries.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/DetailedCustLedgerEntries.Page.al @@ -48,11 +48,8 @@ page 36997 "Detailed Cust. Ledger Entries" ReportPageLbl: Label 'ReportSection15bface0e851125fb4ea', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/DetailedVendorLedgerEntries.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/DetailedVendorLedgerEntries.Page.al index 3de51b3ebd..176ff6d87a 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/DetailedVendorLedgerEntries.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/DetailedVendorLedgerEntries.Page.al @@ -48,11 +48,8 @@ page 36996 "Detailed Vendor Ledger Entries" ReportPageLbl: Label 'ReportSectione72966404f743d39d212', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/EBITDA.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/EBITDA.Page.al index 377f35450a..4bc74f92dd 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/EBITDA.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/EBITDA.Page.al @@ -48,11 +48,8 @@ page 36991 "EBITDA" ReportPageLbl: Label 'ReportSectionab3743c6203831d31beb', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/FinanceReport.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/FinanceReport.Page.al index fce1123500..3f009ecbcc 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/FinanceReport.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/FinanceReport.Page.al @@ -48,11 +48,8 @@ page 37059 "Finance Report" ReportPageLbl: Label '', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/FinancialOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/FinancialOverview.Page.al index 34d2a758a1..d834131c12 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/FinancialOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/FinancialOverview.Page.al @@ -50,11 +50,7 @@ page 36984 "Financial Overview" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } - diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/IncomeStatementbyMonth.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/IncomeStatementbyMonth.Page.al index 29de9616cb..e749f31644 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/IncomeStatementbyMonth.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/IncomeStatementbyMonth.Page.al @@ -48,11 +48,8 @@ page 36985 "Income Statement by Month" ReportPageLbl: Label 'ReportSectionf72eb4d7e5e35db3b283', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/LatePaymentsReceivables.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/LatePaymentsReceivables.Page.al index aaca8fbdfc..6f1944f85c 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/LatePaymentsReceivables.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/LatePaymentsReceivables.Page.al @@ -48,11 +48,8 @@ page 37113 "Late Payments (Receivables)" ReportPageLbl: Label 'c11895fd214552064bae', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/Liabilities.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/Liabilities.Page.al index 6a71d49033..ee62375769 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/Liabilities.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/Liabilities.Page.al @@ -48,11 +48,8 @@ page 36990 "Liabilities" ReportPageLbl: Label 'ReportSectioncd819efac970874e83c3', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/LiquidityKPIs.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/LiquidityKPIs.Page.al index 0b7e21d006..fc43f5a5a1 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/LiquidityKPIs.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/LiquidityKPIs.Page.al @@ -48,11 +48,8 @@ page 36988 "Liquidity KPIs" ReportPageLbl: Label 'ReportSection6838cf9cda361d088e0a', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/PowerBIGeneralLedgEntries.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/PowerBIGeneralLedgEntries.Page.al index 186d93d063..2779ba22e6 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/PowerBIGeneralLedgEntries.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/PowerBIGeneralLedgEntries.Page.al @@ -48,11 +48,8 @@ page 36995 "PowerBI General Ledg. Entries" ReportPageLbl: Label 'ReportSectionfdc853c4230265e530cc', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/Profitability.Page.al b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/Profitability.Page.al index 98bfc0a96f..db203f37d0 100644 --- a/src/Apps/W1/PowerBIReports/App/Finance/Embedded/Profitability.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Finance/Embedded/Profitability.Page.al @@ -48,11 +48,8 @@ page 36989 "Profitability" ReportPageLbl: Label 'ReportSectionbb4917d9edb6d427282c', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Finance Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Finance App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationOverview.Page.al index b213738919..c7f5011293 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationOverview.Page.al @@ -48,11 +48,8 @@ page 37056 "Inventory Valuation Overview" ReportPageLbl: Label 'ReportSection41d23fcd2b0c70d16059', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Val. Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory Valuation App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationReport.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationReport.Page.al index 361246bbe6..99fc47d937 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationReport.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationReport.Page.al @@ -48,11 +48,8 @@ page 37065 "Inventory Valuation Report" ReportPageLbl: Label '', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Val. Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory Valuation App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationbyItem.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationbyItem.Page.al index 13211e0e00..9cc751b7d0 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationbyItem.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationbyItem.Page.al @@ -48,11 +48,8 @@ page 37057 "Inventory Valuation by Item" ReportPageLbl: Label '47469f126ce6603a9114', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Val. Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory Valuation App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationbyLoc.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationbyLoc.Page.al index e18b5a9351..bc2a63b844 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationbyLoc.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory Valuation/InventoryValuationbyLoc.Page.al @@ -48,11 +48,8 @@ page 37058 "Inventory Valuation by Loc." ReportPageLbl: Label '6a6852c0d882690a617b', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Val. Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory Valuation App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/BinContentsbyItemTracking.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/BinContentsbyItemTracking.Page.al index 5a296f2929..7041563717 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/BinContentsbyItemTracking.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/BinContentsbyItemTracking.Page.al @@ -50,11 +50,8 @@ page 37032 "Bin Contents by Item Tracking" #pragma warning restore AA240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/GrossRequirement.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/GrossRequirement.Page.al index 988a5174e3..276899c091 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/GrossRequirement.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/GrossRequirement.Page.al @@ -48,11 +48,8 @@ page 37027 "Gross Requirement" ReportPageLbl: Label 'ReportSection94c932a22e568b021aba', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryForecasting.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryForecasting.Page.al index 057d6c95e2..8d597fc4fe 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryForecasting.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryForecasting.Page.al @@ -48,11 +48,8 @@ page 37110 "Inventory Forecasting" ReportPageLbl: Label '7fea1d34602a649a1083', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryOverview.Page.al index fe80590a76..eb149e57e1 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryOverview.Page.al @@ -48,11 +48,8 @@ page 37022 "Inventory Overview" ReportPageLbl: Label 'ReportSectione24db7517a44af92f122', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryReport.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryReport.Page.al index d420d3ca81..87f54c6c76 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryReport.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventoryReport.Page.al @@ -48,11 +48,8 @@ page 37064 "Inventory Report" ReportPageLbl: Label '', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyItem.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyItem.Page.al index 910b241058..080200adc2 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyItem.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyItem.Page.al @@ -48,11 +48,8 @@ page 37023 "Inventory by Item" ReportPageLbl: Label 'ReportSection8c3ed3c2c96e298a0824', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyLocation.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyLocation.Page.al index 4e229c6ba0..c3ec79b68f 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyLocation.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyLocation.Page.al @@ -48,11 +48,8 @@ page 37024 "Inventory by Location" ReportPageLbl: Label 'ReportSection2765ed5a1005d04217d1', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyLot.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyLot.Page.al index 4929181c71..18c3eb3e90 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyLot.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybyLot.Page.al @@ -48,11 +48,8 @@ page 37029 "Inventory by Lot" ReportPageLbl: Label 'ReportSectionec09da5413c3755982a4', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybySerialNo.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybySerialNo.Page.al index d5da57c0f8..f0d6a92ae9 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybySerialNo.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/InventorybySerialNo.Page.al @@ -48,11 +48,8 @@ page 37030 "Inventory by Serial No." ReportPageLbl: Label 'ReportSectiond99a75349d3388ca085c', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/ItemAvailability.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/ItemAvailability.Page.al index d4d11c7b4a..0e07abe8fa 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/ItemAvailability.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/ItemAvailability.Page.al @@ -48,11 +48,8 @@ page 37026 "Item Availability" ReportPageLbl: Label 'ReportSection61c190709a57015b0e48', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PowerBIABCAnalysis.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PowerBIABCAnalysis.Page.al index 17c4ce180b..5794ef7380 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PowerBIABCAnalysis.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PowerBIABCAnalysis.Page.al @@ -48,11 +48,8 @@ page 37111 "PowerBI ABC Analysis" ReportPageLbl: Label 'a476d6afc8d5d544193b', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PowerBIBinContents.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PowerBIBinContents.Page.al index ec653765d6..057020ebbe 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PowerBIBinContents.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PowerBIBinContents.Page.al @@ -48,11 +48,8 @@ page 37031 "PowerBI Bin Contents" ReportPageLbl: Label 'ReportSection12b3ff23621e20c1398d', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PurchaseandSalesQuantity.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PurchaseandSalesQuantity.Page.al index 5d9f75d828..ffdfb310a7 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PurchaseandSalesQuantity.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/PurchaseandSalesQuantity.Page.al @@ -48,11 +48,8 @@ page 37025 "Purchase and Sales Quantity" ReportPageLbl: Label 'ReportSection956cd619a014201c65e3', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/ScheduledReceipt.Page.al b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/ScheduledReceipt.Page.al index e540b0bb42..6e4ced86fa 100644 --- a/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/ScheduledReceipt.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Inventory/Embedded/Inventory/ScheduledReceipt.Page.al @@ -48,11 +48,8 @@ page 37028 "Scheduled Receipt" ReportPageLbl: Label 'ReportSection5aaca347e8eb867da682', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Inventory Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Inventory App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/AllocatedHours.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/AllocatedHours.Page.al index 473fdcf371..010d836aa7 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/AllocatedHours.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/AllocatedHours.Page.al @@ -48,11 +48,8 @@ page 37043 "Allocated Hours" ReportPageLbl: Label 'ReportSectionf3f7e4f23b609a9d9cb2', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/AverageProductionsTimes.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/AverageProductionsTimes.Page.al index 9f99db00a3..bca277fd8a 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/AverageProductionsTimes.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/AverageProductionsTimes.Page.al @@ -48,11 +48,8 @@ page 37048 "Average Productions Times" ReportPageLbl: Label 'ReportSection13a328ff231fccdc12f7', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/CapacityVariance.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/CapacityVariance.Page.al index 60e8255fbf..1f92804445 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/CapacityVariance.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/CapacityVariance.Page.al @@ -48,11 +48,8 @@ page 37047 "Capacity Variance" ReportPageLbl: Label 'ReportSection6616bf98be16d1636d03', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ConsumptionVariance.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ConsumptionVariance.Page.al index 4ba9e197c4..4229817b29 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ConsumptionVariance.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ConsumptionVariance.Page.al @@ -48,11 +48,8 @@ page 37046 "Consumption Variance" ReportPageLbl: Label 'ReportSectiona9060ee37f667a3d554d', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ExpectedCapacityNeed.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ExpectedCapacityNeed.Page.al index 003ed89710..5310064fab 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ExpectedCapacityNeed.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ExpectedCapacityNeed.Page.al @@ -50,11 +50,8 @@ page 37044 "Expected Capacity Need" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/FinishedProdOrderBreakdown.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/FinishedProdOrderBreakdown.Page.al index e5e20299fa..1fec465d18 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/FinishedProdOrderBreakdown.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/FinishedProdOrderBreakdown.Page.al @@ -48,11 +48,8 @@ page 37045 "Finished Prod. Order Breakdown" ReportPageLbl: Label 'ReportSectionb4e9630e25c77fccda8a', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ManufacturingReport.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ManufacturingReport.Page.al index 5908fbd403..fe79422388 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ManufacturingReport.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ManufacturingReport.Page.al @@ -48,11 +48,8 @@ page 37063 "Manufacturing Report" ReportPageLbl: Label '', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIMachineCenterLoad.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIMachineCenterLoad.Page.al index bf365a3e9b..ceda4417d8 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIMachineCenterLoad.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIMachineCenterLoad.Page.al @@ -48,11 +48,8 @@ page 37096 "PBI Machine Center Load" ReportPageLbl: Label 'd0b095013b4daa61d75c', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIMachineCenterStatistics.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIMachineCenterStatistics.Page.al index b3ec26930c..12b9f4cc81 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIMachineCenterStatistics.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIMachineCenterStatistics.Page.al @@ -50,11 +50,8 @@ page 37095 "PBI Machine Center Statistics" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIWorkCenterStatistics.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIWorkCenterStatistics.Page.al index dabf23894a..e1c5d6df03 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIWorkCenterStatistics.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PBIWorkCenterStatistics.Page.al @@ -48,11 +48,8 @@ page 37094 "PBI Work Center Statistics" ReportPageLbl: Label 'd0f01228e35f48f4c891', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PowerBIReleasedProdOrders.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PowerBIReleasedProdOrders.Page.al index 6e9b6a034e..dd6bb012b4 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PowerBIReleasedProdOrders.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PowerBIReleasedProdOrders.Page.al @@ -48,11 +48,8 @@ page 37049 "PowerBI Released Prod. Orders" ReportPageLbl: Label '85214b3ea7543b0500e5', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PowerBIWorkCenterLoad.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PowerBIWorkCenterLoad.Page.al index 5edbb5fd38..cb17f535ab 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PowerBIWorkCenterLoad.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/PowerBIWorkCenterLoad.Page.al @@ -48,11 +48,8 @@ page 37042 "PowerBI Work Center Load" ReportPageLbl: Label 'ReportSection83a7395d207d5b47b1a4', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProdOrderList.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProdOrderList.Page.al index 8bd3583735..fbeb3ae4e0 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProdOrderList.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProdOrderList.Page.al @@ -48,11 +48,8 @@ page 37097 "Prod. Order - List" ReportPageLbl: Label 'a400a1281385ff890e3e', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProdOrderRoutingsGantt.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProdOrderRoutingsGantt.Page.al index c222f09ad1..4b9761ca70 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProdOrderRoutingsGantt.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProdOrderRoutingsGantt.Page.al @@ -48,11 +48,8 @@ page 37099 "Prod. Order Routings Gantt" ReportPageLbl: Label 'f0afc9178d3f83210328', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionOrderOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionOrderOverview.Page.al index 274ac8959e..4652cfd693 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionOrderOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionOrderOverview.Page.al @@ -48,11 +48,8 @@ page 37098 "Production Order Overview" ReportPageLbl: Label '8cb7f877949d0b12d931', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionOrderWIP.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionOrderWIP.Page.al index a262622a93..8ef0b80de0 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionOrderWIP.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionOrderWIP.Page.al @@ -48,11 +48,8 @@ page 37107 "Production Order WIP" ReportPageLbl: Label '6acf7a1bcebe65700b22', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionScrap.Page.al b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionScrap.Page.al index b333cfcda3..c7f17a6cca 100644 --- a/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionScrap.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Manufacturing/Embedded/ProductionScrap.Page.al @@ -48,11 +48,8 @@ page 37055 "Production Scrap" ReportPageLbl: Label 'ReportSectionc790f50d90d7b6a6836a', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectInvdSalesbyCust.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectInvdSalesbyCust.Page.al index 636f49f091..b2ed14a10e 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectInvdSalesbyCust.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectInvdSalesbyCust.Page.al @@ -48,11 +48,8 @@ page 37039 "Project Invd. Sales by Cust." ReportPageLbl: Label 'ReportSectioncd82c6e10e816900e80b', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectInvoicedSalesbyType.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectInvoicedSalesbyType.Page.al index c8470a4508..1dc8331319 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectInvoicedSalesbyType.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectInvoicedSalesbyType.Page.al @@ -48,11 +48,8 @@ page 37038 "Project Invoiced Sales by Type" ReportPageLbl: Label 'ReportSection355bfd7d0ab99d6a0620', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectPerformancetoBudget.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectPerformancetoBudget.Page.al index 00e0a36935..4f5ca55d00 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectPerformancetoBudget.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectPerformancetoBudget.Page.al @@ -48,11 +48,8 @@ page 37037 "Project Performance to Budget" ReportPageLbl: Label 'ReportSection4b100a3a42980b76957c', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectProfitability.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectProfitability.Page.al index c19f2cf17a..df085444b7 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectProfitability.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectProfitability.Page.al @@ -48,11 +48,8 @@ page 37035 "Project Profitability" ReportPageLbl: Label 'ReportSection16f4a9483133b8db3e12', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectRealization.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectRealization.Page.al index aa83da3cba..655b466ed4 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectRealization.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectRealization.Page.al @@ -48,11 +48,8 @@ page 37036 "Project Realization" ReportPageLbl: Label 'ReportSection1356fdbebe72ad7283d3', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectTasks.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectTasks.Page.al index 6ec3b65233..a4f34dba1b 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectTasks.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectTasks.Page.al @@ -48,11 +48,8 @@ page 37034 "Project Tasks" ReportPageLbl: Label '89b75bcf2e511c341a05', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectTimeline.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectTimeline.Page.al index 4c482b1ccd..010728912b 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectTimeline.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectTimeline.Page.al @@ -48,11 +48,8 @@ page 37106 "Project Timeline" ReportPageLbl: Label '28ed0b540c9ca0ec0105', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectsOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectsOverview.Page.al index d00ff3895e..0e9f3d7eed 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectsOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectsOverview.Page.al @@ -48,11 +48,8 @@ page 37033 "Projects Overview" ReportPageLbl: Label 'ReportSectionf22cc27c0600033d5e26', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectsReport.Page.al b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectsReport.Page.al index c0db502af5..5db5eeae8f 100644 --- a/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectsReport.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Projects/Embedded/ProjectsReport.Page.al @@ -48,11 +48,8 @@ page 37062 "Projects Report" ReportPageLbl: Label '', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Projects Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Projects App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/DailyPurchases.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/DailyPurchases.Page.al index 355e07af56..278f8e206f 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/DailyPurchases.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/DailyPurchases.Page.al @@ -48,11 +48,8 @@ page 37011 "Daily Purchases" ReportPageLbl: Label 'ReportSection02de1de9adad5ee196e0', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/KeyPurchaseInfluencers.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/KeyPurchaseInfluencers.Page.al index 234461b40d..d543402032 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/KeyPurchaseInfluencers.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/KeyPurchaseInfluencers.Page.al @@ -48,11 +48,8 @@ page 37117 "Key Purchase Influencers" ReportPageLbl: Label 'c0b7f34035721e76e546', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchActualvsBudgetAmt.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchActualvsBudgetAmt.Page.al index 23c34a7c67..251cd18276 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchActualvsBudgetAmt.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchActualvsBudgetAmt.Page.al @@ -48,11 +48,8 @@ page 37021 "Purch. Actual vs. Budget Amt." ReportPageLbl: Label 'ReportSection0cb7f30495bc871b8948', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchActualvsBudgetQty.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchActualvsBudgetQty.Page.al index 2c0c324656..9e1d0f9f7f 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchActualvsBudgetQty.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchActualvsBudgetQty.Page.al @@ -48,11 +48,8 @@ page 37020 "Purch. Actual vs. Budget Qty." ReportPageLbl: Label 'ReportSection0cb7f30495bc871b8948', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseForecasting.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseForecasting.Page.al index e5ec76ea41..7fac26f38c 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseForecasting.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseForecasting.Page.al @@ -48,11 +48,8 @@ page 37112 "Purchase Forecasting" ReportPageLbl: Label '72b59b79a0cb70b56aed', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseQuoteOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseQuoteOverview.Page.al index 1da4852411..e6341082c5 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseQuoteOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseQuoteOverview.Page.al @@ -48,11 +48,8 @@ page 37118 "Purchase Quote Overview" ReportPageLbl: Label '15386889aed0b65c35cc', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseReturnOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseReturnOverview.Page.al index 4cb7590858..fec55d7e99 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseReturnOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchaseReturnOverview.Page.al @@ -48,11 +48,8 @@ page 37116 "Purchase Return Overview" ReportPageLbl: Label '63a53744a60debfbb8ac', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesDecomposition.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesDecomposition.Page.al index f1f11ac158..025edbe655 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesDecomposition.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesDecomposition.Page.al @@ -48,11 +48,8 @@ page 37010 "Purchases Decomposition" ReportPageLbl: Label 'ReportSectiond7fdf374ab65b2861937', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesMovingAnnualTotal.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesMovingAnnualTotal.Page.al index c9b5109c93..99cba7cbca 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesMovingAnnualTotal.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesMovingAnnualTotal.Page.al @@ -48,11 +48,8 @@ page 37013 "Purchases Moving Annual Total" ReportPageLbl: Label 'ReportSection26e891a305a24bb29884', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesMovingAverages.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesMovingAverages.Page.al index 87aa1b03c8..9735a12d38 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesMovingAverages.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesMovingAverages.Page.al @@ -48,11 +48,8 @@ page 37012 "Purchases Moving Averages" ReportPageLbl: Label 'ReportSectionc4bf0b0750800ca6b0c6', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesOverview.Page.al index ec17981dd0..fede013284 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesOverview.Page.al @@ -50,11 +50,8 @@ page 37009 "Purchases Overview" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesPeriodOverPeriod.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesPeriodOverPeriod.Page.al index faafc2aa06..7d859e0bb1 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesPeriodOverPeriod.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesPeriodOverPeriod.Page.al @@ -48,11 +48,8 @@ page 37014 "Purchases Period-Over-Period" ReportPageLbl: Label 'ReportSection2c5c763cbb0914a4201d', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesReport.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesReport.Page.al index 99cfed4737..5599441047 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesReport.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesReport.Page.al @@ -48,11 +48,8 @@ page 37061 "Purchases Report" ReportPageLbl: Label '', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesYearOverYear.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesYearOverYear.Page.al index a5ada90d9c..8e444e32a8 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesYearOverYear.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesYearOverYear.Page.al @@ -48,11 +48,8 @@ page 37015 "Purchases Year-Over-Year" ReportPageLbl: Label 'ReportSection884ee5483252d4c5e096', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyItem.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyItem.Page.al index c88a3fb01b..927ab607cf 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyItem.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyItem.Page.al @@ -48,11 +48,8 @@ page 37016 "Purchases by Item" ReportPageLbl: Label 'ReportSection510c03a74548b078dc31', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyLocation.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyLocation.Page.al index 7e9f6e9e55..dfc3a0b644 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyLocation.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyLocation.Page.al @@ -48,11 +48,8 @@ page 37019 "Purchases by Location" ReportPageLbl: Label 'ReportSection86b19910d517e658b780', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyPurchaser.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyPurchaser.Page.al index 60284f5f7c..1997b796d3 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyPurchaser.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyPurchaser.Page.al @@ -48,11 +48,8 @@ page 37017 "Purchases by Purchaser" ReportPageLbl: Label 'ReportSection2530548032dd85837d8c', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyVendor.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyVendor.Page.al index 01c2066330..dc05eb7cce 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyVendor.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/PurchasesbyVendor.Page.al @@ -48,11 +48,8 @@ page 37018 "Purchases by Vendor" ReportPageLbl: Label 'ReportSectiond03ece9eb5ac094617e2', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/VendorQualityAnalysis.Page.al b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/VendorQualityAnalysis.Page.al index 0d0f5ee108..b97cac4120 100644 --- a/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/VendorQualityAnalysis.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Purchasing/Embedded/VendorQualityAnalysis.Page.al @@ -48,11 +48,8 @@ page 37115 "Vendor Quality Analysis" ReportPageLbl: Label '4507bee7aaf3d01db682', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Purchases Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Purchases App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/CustomerRetentionHistory.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/CustomerRetentionHistory.Page.al index 54adcb2979..9228413898 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/CustomerRetentionHistory.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/CustomerRetentionHistory.Page.al @@ -50,11 +50,8 @@ page 37114 "Customer Retention History" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/CustomerRetentionOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/CustomerRetentionOverview.Page.al index 2fcaad7359..ad3c60d2a3 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/CustomerRetentionOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/CustomerRetentionOverview.Page.al @@ -50,11 +50,8 @@ page 36983 "Customer Retention Overview" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/DailySales.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/DailySales.Page.al index cf6a42adbe..ccbfb6476e 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/DailySales.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/DailySales.Page.al @@ -48,11 +48,8 @@ page 36999 "Daily Sales" ReportPageLbl: Label 'ReportSectionb3680fa80c9685297c06', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/KeySalesInfluencers.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/KeySalesInfluencers.Page.al index 2318ac8320..3297fd2b2f 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/KeySalesInfluencers.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/KeySalesInfluencers.Page.al @@ -48,11 +48,8 @@ page 37102 "Key Sales Influencers" ReportPageLbl: Label 'bbcbdfc8ed18270bc4b0', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/OpportunityOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/OpportunityOverview.Page.al index 1b13320ca0..bb3bae8be8 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/OpportunityOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/OpportunityOverview.Page.al @@ -48,11 +48,8 @@ page 37103 "Opportunity Overview" ReportPageLbl: Label '7eb61bb06993742c9b40', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/ReturnOrderOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/ReturnOrderOverview.Page.al index a8e4cc8388..ab7eeaa0f1 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/ReturnOrderOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/ReturnOrderOverview.Page.al @@ -48,11 +48,8 @@ page 37105 "Return Order Overview" ReportPageLbl: Label 'f346c5b1f9260944d3cf', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesActualvsBudgetQty.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesActualvsBudgetQty.Page.al index 37106c7726..8e7d4dd7db 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesActualvsBudgetQty.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesActualvsBudgetQty.Page.al @@ -48,11 +48,8 @@ page 37007 "Sales Actual vs. Budget Qty." ReportPageLbl: Label 'ReportSection05f91a4884be2b5c94ed', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesDecomposition.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesDecomposition.Page.al index e18162ebc8..53fec64c09 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesDecomposition.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesDecomposition.Page.al @@ -48,11 +48,8 @@ page 37101 "Sales Decomposition" ReportPageLbl: Label '3da0bb1d844d8c019741', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesDemographics.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesDemographics.Page.al index 743283abb2..6685e4445b 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesDemographics.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesDemographics.Page.al @@ -48,11 +48,8 @@ page 37100 "Sales Demographics" ReportPageLbl: Label '04b18fd32b63ebcd4349', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesForecasting.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesForecasting.Page.al index 034b700338..4816cff4f2 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesForecasting.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesForecasting.Page.al @@ -50,11 +50,8 @@ page 37109 "Sales Forecasting" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMonthToDate.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMonthToDate.Page.al index 6f60e1bd3c..c9e9dbf3c7 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMonthToDate.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMonthToDate.Page.al @@ -48,11 +48,8 @@ page 37003 "Sales Month-To-Date" ReportPageLbl: Label 'ReportSection7d903e33b708c20e3be1', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMovingAnnualTotal.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMovingAnnualTotal.Page.al index 3065bfa6aa..1fae3fbbfe 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMovingAnnualTotal.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMovingAnnualTotal.Page.al @@ -48,11 +48,8 @@ page 37001 "Sales Moving Annual Total" ReportPageLbl: Label 'ReportSection713e48d18640066bc508', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMovingAverage.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMovingAverage.Page.al index 5564244e7a..2862f79674 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMovingAverage.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesMovingAverage.Page.al @@ -50,11 +50,8 @@ page 37000 "Sales Moving Average" #pragma warning restore AA0240 trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesOverview.Page.al index f9953e7663..76f6a1b630 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesOverview.Page.al @@ -48,11 +48,8 @@ page 36998 "Sales Overview" ReportPageLbl: Label 'ReportSection918285c1bd8f1b7ef96c', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesPeriodOverPeriod.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesPeriodOverPeriod.Page.al index 032dd35fcc..f50bed19c0 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesPeriodOverPeriod.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesPeriodOverPeriod.Page.al @@ -48,11 +48,8 @@ page 37002 "Sales Period-Over-Period" ReportPageLbl: Label 'ReportSection2480499c371d97221c09', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesQuoteOverview.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesQuoteOverview.Page.al index 6f9ff01ee8..e91da3c558 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesQuoteOverview.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesQuoteOverview.Page.al @@ -48,11 +48,8 @@ page 37104 "Sales Quote Overview" ReportPageLbl: Label '15a022da356609401ade', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesReport.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesReport.Page.al index bd9955d2c7..99dc3640e7 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesReport.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesReport.Page.al @@ -48,11 +48,8 @@ page 37060 "Sales Report" ReportPageLbl: Label '', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyCustomer.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyCustomer.Page.al index b248923fcb..c47c7d4f75 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyCustomer.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyCustomer.Page.al @@ -48,11 +48,8 @@ page 37005 "Sales by Customer" ReportPageLbl: Label 'ReportSection48bbd51044e094b7a9a2', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyItem.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyItem.Page.al index 3eeb98a2a4..1991b0a24f 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyItem.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyItem.Page.al @@ -48,11 +48,8 @@ page 37004 "Sales by Item" ReportPageLbl: Label 'ReportSection913651e69467cecd580d', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyLocation.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyLocation.Page.al index 8bd12f6031..f2f772e96d 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyLocation.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyLocation.Page.al @@ -48,11 +48,8 @@ page 37066 "Sales by Location" ReportPageLbl: Label 'da16feb02b930c2292e0', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyProjects.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyProjects.Page.al index 6cb2e63d1b..980e9575a3 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyProjects.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbyProjects.Page.al @@ -48,11 +48,8 @@ page 37119 "Sales by Projects" ReportPageLbl: Label '3f3a8251c4943f8c8fb2', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbySalesperson.Page.al b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbySalesperson.Page.al index 24922a40f6..aec46659cf 100644 --- a/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbySalesperson.Page.al +++ b/src/Apps/W1/PowerBIReports/App/Sales/Embedded/SalesbySalesperson.Page.al @@ -48,11 +48,8 @@ page 37006 "Sales by Salesperson" ReportPageLbl: Label 'ReportSectiond95fc6aa44e9e612cbc4', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } diff --git a/src/Apps/W1/PowerBIReports/App/_Obsolete/Manufacturing/Embedded/CurrentUtilization.Page.al b/src/Apps/W1/PowerBIReports/App/_Obsolete/Manufacturing/Embedded/CurrentUtilization.Page.al index 6fcebb7b00..48ec762384 100644 --- a/src/Apps/W1/PowerBIReports/App/_Obsolete/Manufacturing/Embedded/CurrentUtilization.Page.al +++ b/src/Apps/W1/PowerBIReports/App/_Obsolete/Manufacturing/Embedded/CurrentUtilization.Page.al @@ -51,11 +51,8 @@ page 37040 "Current Utilization" ReportPageLbl: Label 'ReportSection1cb4eb25650060b6dbd0', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } #endif diff --git a/src/Apps/W1/PowerBIReports/App/_Obsolete/Manufacturing/Embedded/HistoricalUtilization.Page.al b/src/Apps/W1/PowerBIReports/App/_Obsolete/Manufacturing/Embedded/HistoricalUtilization.Page.al index e15424f7ce..7f3a6820a7 100644 --- a/src/Apps/W1/PowerBIReports/App/_Obsolete/Manufacturing/Embedded/HistoricalUtilization.Page.al +++ b/src/Apps/W1/PowerBIReports/App/_Obsolete/Manufacturing/Embedded/HistoricalUtilization.Page.al @@ -51,11 +51,8 @@ page 37041 "Historical Utilization" ReportPageLbl: Label 'ReportSectionf9d212728e1d71a00044', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Manufacturing Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Manufacturing App"); end; } #endif diff --git a/src/Apps/W1/PowerBIReports/App/_Obsolete/Sales/Embedded/SalesActualvsBudgetAmt.Page.al b/src/Apps/W1/PowerBIReports/App/_Obsolete/Sales/Embedded/SalesActualvsBudgetAmt.Page.al index 28a3fe644f..fc42f26adc 100644 --- a/src/Apps/W1/PowerBIReports/App/_Obsolete/Sales/Embedded/SalesActualvsBudgetAmt.Page.al +++ b/src/Apps/W1/PowerBIReports/App/_Obsolete/Sales/Embedded/SalesActualvsBudgetAmt.Page.al @@ -51,11 +51,8 @@ page 37008 "Sales Actual vs. Budget Amt." ReportPageLbl: Label 'ReportSectionce3be3bc80c816f2646b', Locked = true; trigger OnOpenPage() - var - PowerBIReportsSetup: Record "PowerBI Reports Setup"; begin - SetupHelper.EnsureUserAcceptedPowerBITerms(); - ReportId := SetupHelper.GetReportIdAndEnsureSetup(CurrPage.Caption(), PowerBIReportsSetup.FieldNo("Sales Report Id")); + ReportId := SetupHelper.OpenPowerBIEmbeddedReportPageValidation("PBI Report Setup"::"Sales App"); end; } #endif diff --git a/src/Apps/W1/PowerBIReports/App/app.json b/src/Apps/W1/PowerBIReports/App/app.json index 50cf661e1a..40237cd967 100644 --- a/src/Apps/W1/PowerBIReports/App/app.json +++ b/src/Apps/W1/PowerBIReports/App/app.json @@ -36,5 +36,6 @@ "features": [ "TranslationFile", "NoImplicitWith" - ] + ], + "resourceFolders": [".resources"] } diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/cultures/en-US.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/cultures/en-US.tmdl index b5e46e61e4..51ef3dd005 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/cultures/en-US.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/cultures/en-US.tmdl @@ -30876,19 +30876,7 @@ cultureInfo en-US "ConceptualProperty": "CLE Closed At Date" } }, - "State": "Generated", - "Terms": [ - { - "Closed At Date": { - "State": "Suggested", - "Source": { - "Type": "External", - "Agent": "PowerBI.VisualColumnRename" - }, - "Weight": 0.9 - } - } - ] + "State": "Generated" }, "customer_ledger_entries.cle_late_payment_": { "Definition": { diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/database.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/database.tmdl index 3d2c0d97c6..19b27aa405 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/database.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/database.tmdl @@ -1,3 +1,3 @@ database - compatibilityLevel: 1567 + compatibilityLevel: 1600 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/expressions.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/expressions.tmdl index 6af58a06cf..73b453fa27 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/expressions.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/expressions.tmdl @@ -31,12 +31,13 @@ expression 'Balance Sheet G/L Entries' = ``` {"description", "Description"} } ), + ClosingCodes = List.Buffer(Table.Column(#"Close Income Statement Codes", "sourceCode")), // Add Closing Entry flag #"Added Closing Entry column" = - if CheckUpgradeQuery("closeIncomeStmtSourceCodes") + if IsCloseIncomeStmtUpgraded then // Set Closing Entry to True or False if the record's source code exists in the mappping table - Table.AddColumn(#"Renamed Columns", "Closing Entry", each List.Contains( Table.ToList(#"Close Income Statement Codes") ,[Source Code]), type logical) + Table.AddColumn(#"Renamed Columns", "Closing Entry", each List.Contains(ClosingCodes, [Source Code]), type logical) else let #"Added Custom" = Table.AddColumn(#"Renamed Columns", "Closing Entry", each false, type logical), @@ -58,7 +59,7 @@ expression 'Income Statement G/L Entries' = ``` let Source = Dynamics365BusinessCentral.ApiContentsWithOptions(ENVIRONMENT, COMPANY,API_ENDPOINT, []), TableData = - if CheckUpgradeQuery("closeIncomeStmtSourceCodes") + if IsCloseIncomeStmtUpgraded then Source{[Name="incomeStmtGeneralLedgerEntries",Signature="table"]}[Data] else @@ -80,11 +81,12 @@ expression 'Income Statement G/L Entries' = ``` } ), #"Renamed Columns" = Table.RenameColumns(#"Changed Type",{{"accountNo", "G/L Account No."}, {"postingDate", "Posting Date"}, {"amount", "Amt."},{"sourceCode","Source Code"},{"sourceNo","Source No."},{"sourceType","Source Type"},{"entryNo","Entry No."},{"description","Description"}}), + ClosingCodes = List.Buffer(Table.Column(#"Close Income Statement Codes", "sourceCode")), // Add Closing Entry flag #"Added Closing Entry column" = - if CheckUpgradeQuery("closeIncomeStmtSourceCodes") + if IsCloseIncomeStmtUpgraded then - Table.AddColumn(#"Renamed Columns", "Closing Entry", each List.Contains( Table.ToList(#"Close Income Statement Codes") ,[Source Code]), type logical) + Table.AddColumn(#"Renamed Columns", "Closing Entry", each List.Contains(ClosingCodes, [Source Code]), type logical) else Table.AddColumn(#"Renamed Columns", "Closing Entry", each false, type logical), #"Removed Columns" = Table.RemoveColumns(#"Added Closing Entry column",{"ETag"}) @@ -474,22 +476,3 @@ expression 'Build Closing Entry Filter' = annotation PBI_ResultType = Function -expression CheckUpgradeQuery = - let - IsQueryValid = (upgradeQuery as text) as logical => - let - Source = Dynamics365BusinessCentral.ApiContentsWithOptions(ENVIRONMENT, COMPANY,API_ENDPOINT, []), - upgradeQuerySource = Source{[Name=upgradeQuery,Signature="table"]}[Data], - result = try upgradeQuerySource otherwise null, - isValid = result <> null - in - isValid - in - IsQueryValid - lineageTag: 2c1614f9-b893-40fa-8fc5-7646ff11f549 - queryGroup: 'Parameters and Functions\Functions' - - annotation PBI_NavigationStepName = Navigation - - annotation PBI_ResultType = Function - diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/model.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/model.tmdl index e35ee694cc..00efe4782d 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/model.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/model.tmdl @@ -38,7 +38,7 @@ queryGroup 'Parameters and Functions\Connection Parameters' annotation __PBI_TimeIntelligenceEnabled = 0 -annotation PBI_QueryOrder = ["Balance Sheet G/L Entries","Income Statement G/L Entries","Close Income Statement G/L Entries","G/L Entries","G/L Budget Entries","ENVIRONMENT","COMPANY","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","GetDimCode","GetDimCodeCaption","GetDimNameCaption","GetParentGLAccountNo","Balance Sheet Measures","GLAccountPosting","GLAccountEndTotal","G/L Account","G/L Account Category","Dimension Sets","G/L Budget","Income Statement Measures","Activity KPIs","Liquidity KPIs","Profitability KPIs","Date Table Setup","Working Days","Standard Calendar Time Intelligence","Fiscal Calendar Time Intelligence","Weekly Calendar Time Intelligence","Customer Ledger Entries","Vendor Ledger Entries","Vendors","Customers","Power BI Account Categories","API_ENDPOINT","CheckUpgradeQuery","Close Income Statement Codes","ConvertUTC","Build Closing Entry Filter","Closing Entry Filter Table","Back Links"] +annotation PBI_QueryOrder = ["Balance Sheet G/L Entries","Income Statement G/L Entries","Close Income Statement G/L Entries","G/L Entries","G/L Budget Entries","ENVIRONMENT","COMPANY","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","GetDimCode","GetDimCodeCaption","GetDimNameCaption","GetParentGLAccountNo","Balance Sheet Measures","GLAccountPosting","GLAccountEndTotal","G/L Account","G/L Account Category","Dimension Sets","G/L Budget","Income Statement Measures","Activity KPIs","Liquidity KPIs","Profitability KPIs","Date Table Setup","Working Days","Standard Calendar Time Intelligence","Fiscal Calendar Time Intelligence","Weekly Calendar Time Intelligence","Customer Ledger Entries","Vendor Ledger Entries","Vendors","Customers","Power BI Account Categories","API_ENDPOINT","Close Income Statement Codes","ConvertUTC","Build Closing Entry Filter","Closing Entry Filter Table","Back Links","Company Encoding","IsCloseIncomeStmtUpgraded"] annotation __BNorm = 1 @@ -82,6 +82,8 @@ ref table 'Closing Entry Filter Table' ref table ENVIRONMENT ref table 'Back Links' ref table 'Translated Localized Labels' +ref table 'Company Encoding' +ref table IsCloseIncomeStmtUpgraded ref cultureInfo en-US ref cultureInfo es-ES diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/Company Encoding.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/Company Encoding.tmdl new file mode 100644 index 0000000000..77f6454109 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/Company Encoding.tmdl @@ -0,0 +1,44 @@ +table 'Company Encoding' + isHidden + lineageTag: 9e6a9d70-a0a6-4618-87e3-fd62f05afb38 + + column ParameterValue + dataType: string + isHidden + lineageTag: fae850ad-94d1-42ad-8e46-5759ca39e7ed + summarizeBy: none + sourceColumn: ParameterValue + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + column 'Encoded Company Name' + dataType: string + isHidden + lineageTag: 6f662f9a-0754-40be-b980-04f652e3d84b + summarizeBy: none + sourceColumn: Encoded Company Name + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + partition 'Company Encoding' = m + mode: import + source = + let + Source = #table( + {"ParameterValue"}, + {{COMPANY}} + ), + #"Added Encoded Column" = Table.AddColumn(Source, "Encoded Company Name", each Uri.EscapeDataString(COMPANY), type text) + in + #"Added Encoded Column" + + changedProperty = IsHidden + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Table + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/ENVIRONMENT.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/ENVIRONMENT.tmdl index 0c0481ff20..6d68b313f1 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/ENVIRONMENT.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/ENVIRONMENT.tmdl @@ -16,7 +16,7 @@ table ENVIRONMENT partition ENVIRONMENT = m mode: import queryGroup: 'Parameters and Functions\Connection Parameters' - source = "Sandbox" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "a48072_p48015_US_28-0" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/G%2FL Entries.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/G%2FL Entries.tmdl index e73cccd586..d89c8750fd 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/G%2FL Entries.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/G%2FL Entries.tmdl @@ -155,7 +155,7 @@ table 'G/L Entries' VAR BusinessCentral = "https://businesscentral.dynamics.com/" VAR TenantID = VALUES('Date Table Setup'[tenantID]) VAR Environment = VALUES(ENVIRONMENT[ENVIRONMENT]) - VAR Company = "?company=" & VALUES(COMPANY[COMPANY]) + VAR Company = "?company=" & VALUES('Company Encoding'[Encoded Company Name]) RETURN BusinessCentral & TenantID & "/" & Environment & "/" & Company displayFolder: _G/L Entry Measures\_G/L Entry Back Links lineageTag: b08bfde7-4752-45b9-b9ce-53d9c0ce7665 @@ -296,7 +296,7 @@ table 'G/L Entries' source = let #"Combine Queries" = - if CheckUpgradeQuery("closeIncomeStmtSourceCodes") then + if IsCloseIncomeStmtUpgraded then Table.Combine( { #"Balance Sheet G/L Entries", diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/IsCloseIncomeStmtUpgraded.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/IsCloseIncomeStmtUpgraded.tmdl new file mode 100644 index 0000000000..c9c14e8f49 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.SemanticModel/definition/tables/IsCloseIncomeStmtUpgraded.tmdl @@ -0,0 +1,26 @@ +table IsCloseIncomeStmtUpgraded + lineageTag: 01f9da70-4cc2-4bc6-a8da-d7ccdcf2f06e + + column IsCloseIncomeStmtUpgraded + dataType: boolean + formatString: """TRUE"";""TRUE"";""FALSE""" + lineageTag: 7de2637e-1359-4626-8420-9ef58864b6e4 + summarizeBy: none + sourceColumn: IsCloseIncomeStmtUpgraded + + annotation SummarizationSetBy = Automatic + + partition IsCloseIncomeStmtUpgraded = m + mode: import + queryGroup: 'Parameters and Functions' + source = + let + Codes = #"Close Income Statement Codes", + HasRealCodes = not List.IsEmpty(List.RemoveItems(Table.Column(Codes, "sourceCode"), {""})) + in + HasRealCodes + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Logical + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.pbix deleted file mode 100644 index a1d71f728d..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Finance app/Finance app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/database.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/database.tmdl index 3d2c0d97c6..19b27aa405 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/database.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/database.tmdl @@ -1,3 +1,3 @@ database - compatibilityLevel: 1567 + compatibilityLevel: 1600 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/model.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/model.tmdl index 54138b5ce0..288284aa07 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/model.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/model.tmdl @@ -29,7 +29,7 @@ queryGroup 'Parameters and Functions\Functions' annotation __PBI_TimeIntelligenceEnabled = 0 -annotation PBI_QueryOrder = ["Item","Location","ENVIRONMENT","COMPANY","Value Entries","Working Days","Date Table Setup","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","Dimension Sets","API_ENDPOINT","ConvertUTC","Item Category"] +annotation PBI_QueryOrder = ["Item","Location","ENVIRONMENT","COMPANY","Value Entries","Working Days","Date Table Setup","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","Dimension Sets","API_ENDPOINT","ConvertUTC","Item Category","Company Encoding"] annotation __TEdtr = 1 @@ -50,6 +50,7 @@ ref table COMPANY ref table ENVIRONMENT ref table 'Item Category' ref table 'Translated Localized Labels' +ref table 'Company Encoding' ref cultureInfo en-US ref cultureInfo cs-CZ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Company Encoding.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Company Encoding.tmdl new file mode 100644 index 0000000000..6bef52dba4 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Company Encoding.tmdl @@ -0,0 +1,44 @@ +table 'Company Encoding' + isHidden + lineageTag: a0a0d2e5-59f8-4da0-b914-15a4a1ec7617 + + column ParameterValue + dataType: string + isHidden + lineageTag: 220fa446-0d20-4ffb-88ec-96bc688e6cb6 + summarizeBy: none + sourceColumn: ParameterValue + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + column 'Encoded Company Name' + dataType: string + isHidden + lineageTag: 7d57cb18-28e5-4e60-9f4c-2dc6f293fda0 + summarizeBy: none + sourceColumn: Encoded Company Name + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + partition 'Company Encoding' = m + mode: import + source = + let + Source = #table( + {"ParameterValue"}, + {{COMPANY}} + ), + #"Added Encoded Column" = Table.AddColumn(Source, "Encoded Company Name", each Uri.EscapeDataString(COMPANY), type text) + in + #"Added Encoded Column" + + changedProperty = IsHidden + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Table + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/ENVIRONMENT.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/ENVIRONMENT.tmdl index c15eb33b37..c9feead0d3 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/ENVIRONMENT.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/ENVIRONMENT.tmdl @@ -16,7 +16,7 @@ table ENVIRONMENT partition ENVIRONMENT = m mode: import queryGroup: 'Parameters and Functions' - source = "SANDBOX" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "a48072_p48015_US_28-0" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Item.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Item.tmdl index 025087fd7a..7c126fa148 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Item.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Item.tmdl @@ -304,5 +304,5 @@ table Item annotation PBI_NavigationStepName = Navigation - annotation PBI_ResultType = Table + annotation PBI_ResultType = Exception diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Value Entries.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Value Entries.tmdl index 86af63193b..69888d97c1 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Value Entries.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.SemanticModel/definition/tables/Value Entries.tmdl @@ -358,7 +358,7 @@ table 'Value Entries' VAR BusinessCentral = "https://businesscentral.dynamics.com/" VAR TenantID = VALUES('Date Table Setup'[tenantID]) VAR Environment = VALUES(ENVIRONMENT[ENVIRONMENT]) - VAR Company = "?company=" & VALUES(COMPANY[COMPANY]) + VAR Company = "?company=" & VALUES('Company Encoding'[Encoded Company Name]) RETURN BusinessCentral & TenantID & "/" & Environment & "/" & Company isHidden displayFolder: _Inventory Value Entry Measures\Back Links diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.pbix deleted file mode 100644 index fd467f2bea..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory Valuation app/Inventory Valuation app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/DAXQueries/.pbi/daxQueries.json b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/DAXQueries/.pbi/daxQueries.json deleted file mode 100644 index 64c4aeec6d..0000000000 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/DAXQueries/.pbi/daxQueries.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "version": "1.0.0", - "tabOrder": [ - "Query 1" - ], - "defaultTab": "Query 1" -} \ No newline at end of file diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/database.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/database.tmdl index 3d2c0d97c6..19b27aa405 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/database.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/database.tmdl @@ -1,3 +1,3 @@ database - compatibilityLevel: 1567 + compatibilityLevel: 1600 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/model.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/model.tmdl index 88ab1e7584..6d9fde5738 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/model.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/model.tmdl @@ -29,7 +29,7 @@ queryGroup 'Parameters and Functions\Functions' annotation __PBI_TimeIntelligenceEnabled = 0 -annotation PBI_QueryOrder = ["Item Ledger Entries","Warehouse Entries","Item","Location","Customer","Vendor","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","Working Days","Date Table Setup","Purchase Lines","Zone","Bin","Dimension Sets","Sales Lines","Assembly Lines","Assembly Headers","Project Planning Lines","Warehouse Journal Lines (To Bin)","Warehouse Journal Lines (From Bin)","Warehouse Activity Lines","Production Order Lines","Prod Order Component Lines","Service Lines","Transfer Lines","Requisition Line","Planning Component Lines","Lot No","Serial No","ENVIRONMENT","COMPANY","API_ENDPOINT","ConvertUTC","Item Category","ABC Classes"] +annotation PBI_QueryOrder = ["Item Ledger Entries","Warehouse Entries","Item","Location","Customer","Vendor","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","Working Days","Date Table Setup","Purchase Lines","Zone","Bin","Dimension Sets","Sales Lines","Assembly Lines","Assembly Headers","Project Planning Lines","Warehouse Journal Lines (To Bin)","Warehouse Journal Lines (From Bin)","Warehouse Activity Lines","Production Order Lines","Prod Order Component Lines","Service Lines","Transfer Lines","Requisition Line","Planning Component Lines","Lot No","Serial No","ENVIRONMENT","COMPANY","API_ENDPOINT","ConvertUTC","Item Category","ABC Classes","Company Encoding"] annotation __TEdtr = 1 @@ -74,6 +74,7 @@ ref table 'Item Category' ref table 'ABC Classes' ref table 'ABC Classification' ref table 'Translated Localized Labels' +ref table 'Company Encoding' ref perspective 'Fiscal Calendar' ref perspective 'Standard Calendar' diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/Company Encoding.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/Company Encoding.tmdl new file mode 100644 index 0000000000..0963963ace --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/Company Encoding.tmdl @@ -0,0 +1,44 @@ +table 'Company Encoding' + isHidden + lineageTag: 9104d373-1d32-4da1-8038-0c54219dde4d + + column ParameterValue + dataType: string + isHidden + lineageTag: d39a75fd-04cb-49c7-849e-29f3e129ee76 + summarizeBy: none + sourceColumn: ParameterValue + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + column 'Encoded Company Name' + dataType: string + isHidden + lineageTag: d8f09e29-5dbd-45b0-9345-0a2fcadae11e + summarizeBy: none + sourceColumn: Encoded Company Name + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + partition 'Company Encoding' = m + mode: import + source = + let + Source = #table( + {"ParameterValue"}, + {{COMPANY}} + ), + #"Added Encoded Column" = Table.AddColumn(Source, "Encoded Company Name", each Uri.EscapeDataString(COMPANY), type text) + in + #"Added Encoded Column" + + changedProperty = IsHidden + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Table + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/ENVIRONMENT.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/ENVIRONMENT.tmdl index ed049a6757..76f49b8a8e 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/ENVIRONMENT.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/ENVIRONMENT.tmdl @@ -16,7 +16,7 @@ table ENVIRONMENT partition ENVIRONMENT = m mode: import queryGroup: 'Parameters and Functions' - source = "Sandbox" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "a48072_p48015_US_28-0" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/Item Ledger Entries.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/Item Ledger Entries.tmdl index 3192c9020d..9c6653511b 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/Item Ledger Entries.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.SemanticModel/definition/tables/Item Ledger Entries.tmdl @@ -68,10 +68,9 @@ table 'Item Ledger Entries' measure 'Base Link' = VAR BusinessCentral = "https://businesscentral.dynamics.com/" - //VAR TenantID = VALUES('TENANT ID'[TENANT ID]) VAR TenantID = VALUES('Date Table Setup'[tenantID]) VAR Environment = VALUES(ENVIRONMENT[ENVIRONMENT]) - VAR Company = "?company=" & VALUES(COMPANY[COMPANY]) + VAR Company = "?company=" & VALUES('Company Encoding'[Encoded Company Name]) RETURN BusinessCentral & TenantID & "/" & Environment & "/" & Company displayFolder: _Back Links lineageTag: 5ce10cd7-9520-4a7c-a650-b492a4dd903b @@ -531,7 +530,7 @@ table 'Item Ledger Entries' in #"Renamed Columns1" - annotation PBI_ResultType = Table + annotation PBI_ResultType = Exception annotation PBI_NavigationStepName = Navigation diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.pbix deleted file mode 100644 index 99c1913a6c..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Inventory app/Inventory app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/7471b800c5bc6e707c91/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/7471b800c5bc6e707c91/page.json index b302889e9e..f421f35c5f 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/7471b800c5bc6e707c91/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/7471b800c5bc6e707c91/page.json @@ -224,6 +224,21 @@ } ] } + }, + { + "name": "91f27b8c68ea25144c02", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "Drillthrough" } ] }, @@ -328,6 +343,22 @@ "Property": "Work Center No." } } + }, + { + "name": "94b666bc703e5113a58c", + "boundFilter": "91f27b8c68ea25144c02", + "asAggregation": false, + "qnaSingleSelectRequired": false, + "fieldExpr": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + } } ] }, diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/85214b3ea7543b0500e5/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/85214b3ea7543b0500e5/page.json index 1a79d46fc6..fa3149cf45 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/85214b3ea7543b0500e5/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/85214b3ea7543b0500e5/page.json @@ -212,6 +212,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "d5a946f788909d0e0086", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ] }, diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/8cb7f877949d0b12d931/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/8cb7f877949d0b12d931/page.json index 95f872eedc..fe57db20eb 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/8cb7f877949d0b12d931/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/8cb7f877949d0b12d931/page.json @@ -7,8 +7,73 @@ "width": 1280, "filterConfig": { "filters": [ + { + "name": "13183ffb98a18a406e06", + "ordinal": 0, + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Date" + } + }, + "Property": "Date" + } + }, + "type": "Categorical", + "howCreated": "User" + }, + { + "name": "b667b00b24bd9e58c90b", + "ordinal": 1, + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Dimension Sets" + } + }, + "Property": "Global Dimension 1" + } + }, + "type": "Categorical", + "howCreated": "User" + }, + { + "name": "6a142f73bde2b12045c1", + "ordinal": 2, + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Dimension Sets" + } + }, + "Property": "Global Dimension 2" + } + }, + "type": "Categorical", + "howCreated": "User" + }, + { + "name": "1decf6106c29c902912c", + "ordinal": 3, + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Production Orders" + } + }, + "Property": "Prod Order Source No." + } + }, + "type": "Categorical", + "howCreated": "User" + }, { "name": "f100540f05a53c3e0633", + "ordinal": 4, "field": { "Column": { "Expression": { @@ -82,23 +147,9 @@ ] } }, - { - "name": "13183ffb98a18a406e06", - "field": { - "Column": { - "Expression": { - "SourceRef": { - "Entity": "Date" - } - }, - "Property": "Date" - } - }, - "type": "Categorical", - "howCreated": "User" - }, { "name": "86fb10445e4321a6d345", + "ordinal": 5, "field": { "Column": { "Expression": { @@ -112,53 +163,9 @@ "type": "Categorical", "howCreated": "User" }, - { - "name": "1decf6106c29c902912c", - "field": { - "Column": { - "Expression": { - "SourceRef": { - "Entity": "Production Orders" - } - }, - "Property": "Prod Order Source No." - } - }, - "type": "Categorical", - "howCreated": "User" - }, - { - "name": "b667b00b24bd9e58c90b", - "field": { - "Column": { - "Expression": { - "SourceRef": { - "Entity": "Dimension Sets" - } - }, - "Property": "Global Dimension 1" - } - }, - "type": "Categorical", - "howCreated": "User" - }, - { - "name": "6a142f73bde2b12045c1", - "field": { - "Column": { - "Expression": { - "SourceRef": { - "Entity": "Dimension Sets" - } - }, - "Property": "Global Dimension 2" - } - }, - "type": "Categorical", - "howCreated": "User" - }, { "name": "93a1b4298d88696d46d5", + "ordinal": 6, "field": { "Column": { "Expression": { @@ -174,6 +181,7 @@ }, { "name": "c8cdc9d31b269b5199ad", + "ordinal": 7, "field": { "Column": { "Expression": { @@ -189,6 +197,7 @@ }, { "name": "520e0941638bd07bc00b", + "ordinal": 8, "field": { "Column": { "Expression": { @@ -204,6 +213,7 @@ }, { "name": "e65a264270633e5d6a8b", + "ordinal": 9, "field": { "Column": { "Expression": { @@ -219,6 +229,7 @@ }, { "name": "a28eae1daa75aec80039", + "ordinal": 10, "field": { "Column": { "Expression": { @@ -234,6 +245,7 @@ }, { "name": "95340fe717719464ac03", + "ordinal": 11, "field": { "Column": { "Expression": { @@ -246,7 +258,24 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "3b75eec2d09a0448d98e", + "ordinal": 12, + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } - ] + ], + "filterSortOrder": "Custom" } } \ No newline at end of file diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/8cb7f877949d0b12d931/visuals/f7521b05e040da1620d5/visual.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/8cb7f877949d0b12d931/visuals/f7521b05e040da1620d5/visual.json index 5f6b60bfca..d487e288b2 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/8cb7f877949d0b12d931/visuals/f7521b05e040da1620d5/visual.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/8cb7f877949d0b12d931/visuals/f7521b05e040da1620d5/visual.json @@ -385,6 +385,90 @@ } ] } + }, + { + "name": "bb89f3c16ad6603bdc37", + "field": { + "Measure": { + "Expression": { + "SourceRef": { + "Entity": "Production Orders" + } + }, + "Property": "Total Actual Cost" + } + }, + "type": "Advanced" + }, + { + "name": "418613d5552a64d48116", + "field": { + "Measure": { + "Expression": { + "SourceRef": { + "Entity": "Production Orders" + } + }, + "Property": "Actual Material Cost" + } + }, + "type": "Advanced" + }, + { + "name": "7e9ac9508edb6e6a0970", + "field": { + "Measure": { + "Expression": { + "SourceRef": { + "Entity": "Production Orders" + } + }, + "Property": "Actual Capacity Cost" + } + }, + "type": "Advanced" + }, + { + "name": "8b9af8cca40436846025", + "field": { + "Measure": { + "Expression": { + "SourceRef": { + "Entity": "Production Orders" + } + }, + "Property": "Actual Subcontracted Cost" + } + }, + "type": "Advanced" + }, + { + "name": "b00c7b7cd4a95066a010", + "field": { + "Measure": { + "Expression": { + "SourceRef": { + "Entity": "Production Orders" + } + }, + "Property": "Actual Manufacturing Overhead Cost" + } + }, + "type": "Advanced" + }, + { + "name": "3e91c1ae543453071a42", + "field": { + "Measure": { + "Expression": { + "SourceRef": { + "Entity": "Production Orders" + } + }, + "Property": "Actual Capacity Overhead Cost" + } + }, + "type": "Advanced" } ] } diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/949e7a93960706625a9a/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/949e7a93960706625a9a/page.json index 342d01a6fb..2099fef85d 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/949e7a93960706625a9a/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/949e7a93960706625a9a/page.json @@ -182,6 +182,22 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "b2216da3bee9a9c59014", + "ordinal": 11, + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Custom" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection13a328ff231fccdc12f7/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection13a328ff231fccdc12f7/page.json index 6ebdc0f36c..e9d32e98ab 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection13a328ff231fccdc12f7/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection13a328ff231fccdc12f7/page.json @@ -201,6 +201,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "b40cd4b16776b1d5809a", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection1cb4eb25650060b6dbd0/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection1cb4eb25650060b6dbd0/page.json index 1568ebd105..b954cc0264 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection1cb4eb25650060b6dbd0/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection1cb4eb25650060b6dbd0/page.json @@ -222,6 +222,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "1a93faf46a6302dda4e3", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection6616bf98be16d1636d03/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection6616bf98be16d1636d03/page.json index 60ff94a663..449e9aac76 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection6616bf98be16d1636d03/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection6616bf98be16d1636d03/page.json @@ -241,6 +241,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "7d3f1a21d70b212542d3", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection83a7395d207d5b47b1a4/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection83a7395d207d5b47b1a4/page.json index 336bd470e1..d3e3c57dc7 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection83a7395d207d5b47b1a4/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSection83a7395d207d5b47b1a4/page.json @@ -286,6 +286,21 @@ "howCreated": "User", "isHiddenInViewMode": true, "isLockedInViewMode": true + }, + { + "name": "b9d9cd0318493dcd1d39", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectiona2c7d37ca03217072470/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectiona2c7d37ca03217072470/page.json index b56e5a2e18..47914c5512 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectiona2c7d37ca03217072470/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectiona2c7d37ca03217072470/page.json @@ -283,6 +283,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "7ed491b60b7e40a30084", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectiona9060ee37f667a3d554d/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectiona9060ee37f667a3d554d/page.json index 83221ca2e2..bc34cde3c0 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectiona9060ee37f667a3d554d/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectiona9060ee37f667a3d554d/page.json @@ -234,6 +234,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "aabeb294925e732553b6", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionb4e9630e25c77fccda8a/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionb4e9630e25c77fccda8a/page.json index 810ba8d96c..d34c736ac7 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionb4e9630e25c77fccda8a/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionb4e9630e25c77fccda8a/page.json @@ -227,6 +227,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "5564fa084b88d00e2e62", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionc790f50d90d7b6a6836a/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionc790f50d90d7b6a6836a/page.json index e449ca4cc2..6ad3c8be3d 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionc790f50d90d7b6a6836a/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionc790f50d90d7b6a6836a/page.json @@ -66,6 +66,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "d7cf4977c00d7d3d8105", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionf3f7e4f23b609a9d9cb2/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionf3f7e4f23b609a9d9cb2/page.json index 198f87de14..2897a7838c 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionf3f7e4f23b609a9d9cb2/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionf3f7e4f23b609a9d9cb2/page.json @@ -261,6 +261,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "92218eca0c3acab09761", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionf9d212728e1d71a00044/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionf9d212728e1d71a00044/page.json index 76f751c02e..28182c00c3 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionf9d212728e1d71a00044/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/ReportSectionf9d212728e1d71a00044/page.json @@ -171,6 +171,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "38f5d1e037a293b70ae4", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Ascending" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/a400a1281385ff890e3e/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/a400a1281385ff890e3e/page.json index bb01f603d0..eb1dd1a5e1 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/a400a1281385ff890e3e/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/a400a1281385ff890e3e/page.json @@ -246,6 +246,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "ef40ef880a5c87865989", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ] }, diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/d0b095013b4daa61d75c/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/d0b095013b4daa61d75c/page.json index 197f64b806..5cb9923ef9 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/d0b095013b4daa61d75c/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/d0b095013b4daa61d75c/page.json @@ -235,6 +235,22 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "1acee294e0636706decc", + "ordinal": 12, + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ], "filterSortOrder": "Custom" diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/d0f01228e35f48f4c891/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/d0f01228e35f48f4c891/page.json index 40803dd9ea..50d1a78327 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/d0f01228e35f48f4c891/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/d0f01228e35f48f4c891/page.json @@ -171,6 +171,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "def4d0dbd6280c44a0d6", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ] } diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/f0afc9178d3f83210328/page.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/f0afc9178d3f83210328/page.json index 44dbfa1c4d..55fdd1c728 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/f0afc9178d3f83210328/page.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.Report/definition/pages/f0afc9178d3f83210328/page.json @@ -141,6 +141,21 @@ }, "type": "Categorical", "howCreated": "User" + }, + { + "name": "65e5e63b090bc2e990e5", + "field": { + "Column": { + "Expression": { + "SourceRef": { + "Entity": "Work Center" + } + }, + "Property": "Subcontracting" + } + }, + "type": "Categorical", + "howCreated": "User" } ] }, diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/database.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/database.tmdl index 3d2c0d97c6..19b27aa405 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/database.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/database.tmdl @@ -1,3 +1,3 @@ database - compatibilityLevel: 1567 + compatibilityLevel: 1600 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/model.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/model.tmdl index 92399e8aa2..d65e5f8537 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/model.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/model.tmdl @@ -29,7 +29,7 @@ queryGroup 'Date Table' annotation __PBI_TimeIntelligenceEnabled = 0 -annotation PBI_QueryOrder = ["Item","Dimension Sets","ENVIRONMENT","COMPANY","API_ENDPOINT","Production Order Lines","Prod Order Components","Prod Order Routing Lines","Prod Order Capacity Need","Item Ledger Entries","Capacity Ledger Entries","Calendar Entries","Work Center","Date Table Setup","Working Days","Machine Center","Location","Routing Link","Routing","Production Orders","Production Orders (Released or Finished)","Work Center Group","Value Entries","Manufacturing Setup","DimensionSets_DataSource","DimensionSet_Blank","Dimensions","Inventory Adjustment Entry Order Line","Back Links","Time Factors","Item Category","ConvertUTC","ConvertManufacturingStartEndTimes"] +annotation PBI_QueryOrder = ["Item","Dimension Sets","ENVIRONMENT","COMPANY","API_ENDPOINT","Production Order Lines","Prod Order Components","Prod Order Routing Lines","Prod Order Capacity Need","Item Ledger Entries","Capacity Ledger Entries","Calendar Entries","Work Center","Date Table Setup","Working Days","Machine Center","Location","Routing Link","Routing","Production Orders","Production Orders (Released or Finished)","Work Center Group","Value Entries","Manufacturing Setup","DimensionSets_DataSource","DimensionSet_Blank","Dimensions","Inventory Adjustment Entry Order Line","Back Links","Time Factors","Item Category","ConvertUTC","ConvertManufacturingStartEndTimes","Company Encoding"] annotation __TEdtr = 1 @@ -67,6 +67,7 @@ ref table 'Back Links' ref table 'Time Factors' ref table 'Item Category' ref table 'Translated Localized Labels' +ref table 'Company Encoding' ref cultureInfo en-US ref cultureInfo cs-CZ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Back Links.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Back Links.tmdl index 1dab82603d..48c0d33952 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Back Links.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Back Links.tmdl @@ -7,7 +7,7 @@ table 'Back Links' VAR BusinessCentral = "https://businesscentral.dynamics.com/" VAR TenantID = VALUES('Date Table Setup'[tenantID]) VAR Environment = VALUES(ENVIRONMENT[ENVIRONMENT]) - VAR Company = "?company=" & VALUES(COMPANY[COMPANY]) + VAR Company = "?company=" & VALUES('Company Encoding'[Encoded Company Name]) RETURN BusinessCentral & TenantID & "/" & Environment & "/" & Company isHidden lineageTag: d4610d12-08d0-4e03-93f0-369ded9c2630 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/COMPANY.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/COMPANY.tmdl index 29c3a1823b..15e4a7ba94 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/COMPANY.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/COMPANY.tmdl @@ -22,7 +22,7 @@ table COMPANY partition COMPANY = m mode: import queryGroup: 'Connection Details' - source = "CRONUS USA, Inc." meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "Cronus" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Capacity Ledger Entries.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Capacity Ledger Entries.tmdl index a885dc53db..f14a77c4f9 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Capacity Ledger Entries.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Capacity Ledger Entries.tmdl @@ -415,11 +415,14 @@ table 'Capacity Ledger Entries' column Subcontracting dataType: boolean + isHidden formatString: """TRUE"";""TRUE"";""FALSE""" lineageTag: 2d8940a5-5103-41d1-85e7-26975582d66f summarizeBy: none sourceColumn: Subcontracting + changedProperty = IsHidden + annotation SummarizationSetBy = Automatic column 'Capacity Unit of Measure' diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Company Encoding.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Company Encoding.tmdl new file mode 100644 index 0000000000..35620e1cbb --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Company Encoding.tmdl @@ -0,0 +1,44 @@ +table 'Company Encoding' + isHidden + lineageTag: 595fb95c-0edb-4fa6-9999-90c4cc77fb92 + + column ParameterValue + dataType: string + isHidden + lineageTag: 0b87d89b-497d-4684-90c3-bce538e6c6b7 + summarizeBy: none + sourceColumn: ParameterValue + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + column 'Encoded Company Name' + dataType: string + isHidden + lineageTag: 1e269faf-2b83-41db-b3fa-65bb44931871 + summarizeBy: none + sourceColumn: Encoded Company Name + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + partition 'Company Encoding' = m + mode: import + source = + let + Source = #table( + {"ParameterValue"}, + {{COMPANY}} + ), + #"Added Encoded Column" = Table.AddColumn(Source, "Encoded Company Name", each Uri.EscapeDataString(COMPANY), type text) + in + #"Added Encoded Column" + + changedProperty = IsHidden + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Table + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/ENVIRONMENT.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/ENVIRONMENT.tmdl index 3cb3ba3f64..51d9e2c274 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/ENVIRONMENT.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/ENVIRONMENT.tmdl @@ -16,7 +16,7 @@ table ENVIRONMENT partition ENVIRONMENT = m mode: import queryGroup: 'Connection Details' - source = "PRODUCTION" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "a48072_p48015_US_28-0" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Item.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Item.tmdl index 71daa3c855..8e0d00a78e 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Item.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Item.tmdl @@ -272,7 +272,7 @@ table Item in #"Changed Type" - annotation PBI_ResultType = Table + annotation PBI_ResultType = Exception annotation PBI_NavigationStepName = Navigation diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Production Orders.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Production Orders.tmdl index 850332e180..8700e39499 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Production Orders.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Production Orders.tmdl @@ -405,7 +405,8 @@ table 'Production Orders' var InvAdjOrderLineNo = SELECTCOLUMNS(InventoryAdjustmentLines, 'Inventory Adjustment Entry Order Line'[orderLineNo]) VAR CapacityLedgerEntries = CALCULATETABLE('Capacity Ledger Entries', - REMOVEFILTERS('Capacity Ledger Entries'), + REMOVEFILTERS('Capacity Ledger Entries'), + VALUES('Capacity Ledger Entries'[workCenterNo]), 'Capacity Ledger Entries'[Order No.] = InvAdjOrderNo, 'Capacity Ledger Entries'[Order Line No.] = InvAdjOrderLineNo, 'Capacity Ledger Entries'[Item No.] = InvAdjItemNo, @@ -1030,5 +1031,5 @@ table 'Production Orders' annotation PBI_NavigationStepName = Navigation - annotation PBI_ResultType = Table + annotation PBI_ResultType = Exception diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Work Center.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Work Center.tmdl index cb8bae84e0..07f4107228 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Work Center.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/definition/tables/Work Center.tmdl @@ -325,6 +325,15 @@ table 'Work Center' annotation SummarizationSetBy = Automatic + column Subcontracting + dataType: boolean + formatString: """TRUE"";""TRUE"";""FALSE""" + lineageTag: 5a312239-41c6-44ea-beb5-3a53e3394f5c + summarizeBy: none + sourceColumn: Subcontracting + + annotation SummarizationSetBy = Automatic + partition 'Work Center-b82d66f0-7e4c-4e99-8958-f16b054d3596' = m mode: import queryGroup: 'Dimension Tables' @@ -355,9 +364,10 @@ table 'Work Center' ), #"Added Column" = Table.AddColumn( #"Renamed Columns", "Work Center Key", each "Work Center" & " " & [#"Work Center No."], type text - ) + ), + #"Added Subcontracting Column" = Table.AddColumn(#"Added Column", "Subcontracting", each if [#"Work Center Subcontractor No."] <> "" then true else false, Logical.Type) in - #"Added Column" + #"Added Subcontracting Column" annotation PBI_ResultType = Table diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/diagramLayout.json b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/diagramLayout.json index 2e4b28bfee..3c471ed922 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/diagramLayout.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.SemanticModel/diagramLayout.json @@ -387,6 +387,16 @@ "width": 234 }, "zIndex": 0 + }, + { + "location": {}, + "nodeIndex": "Company Encoding", + "nodeLineageTag": "595fb95c-0edb-4fa6-9999-90c4cc77fb92", + "size": { + "height": 128, + "width": 234 + }, + "zIndex": 0 } ], "name": "All tables", @@ -949,7 +959,7 @@ "ordinal": 6, "scrollPosition": { "x": 0, - "y": 59.072749691738586 + "y": 16.019728729963006 }, "nodes": [ { diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.pbix deleted file mode 100644 index 3faefe895f..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Manufacturing app/Manufacturing app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/database.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/database.tmdl index 3d2c0d97c6..19b27aa405 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/database.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/database.tmdl @@ -1,3 +1,3 @@ database - compatibilityLevel: 1567 + compatibilityLevel: 1600 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/model.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/model.tmdl index a82b0b2c48..e5bebcc4f3 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/model.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/model.tmdl @@ -30,7 +30,7 @@ queryGroup 'Date Table' annotation __PBI_TimeIntelligenceEnabled = 0 -annotation PBI_QueryOrder = ["Project Planning Line","Project Ledger Entry","Project","Customer","Tasks","Dimension Sets","Purchases","ENVIRONMENT","COMPANY","Dimensions","DimensionSets_DataSource","DimensionSet_Blank","Purchase Lines Outstanding","Purchase Lines Received Not Invoiced","API_ENDPOINT","Date Table Setup","Working Days","ConvertUTC","Items","G/L Accounts","Resources","Type"] +annotation PBI_QueryOrder = ["Project Planning Line","Project Ledger Entry","Project","Customer","Tasks","Dimension Sets","Purchases","ENVIRONMENT","COMPANY","Dimensions","DimensionSets_DataSource","DimensionSet_Blank","Company Encoding","Purchase Lines Outstanding","Purchase Lines Received Not Invoiced","API_ENDPOINT","Date Table Setup","Working Days","ConvertUTC","Items","G/L Accounts","Resources","Type"] annotation __TEdtr = 1 @@ -57,6 +57,7 @@ ref table Items ref table 'G/L Accounts' ref table Resources ref table Type +ref table 'Company Encoding' ref cultureInfo en-US ref cultureInfo cs-CZ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Company Encoding.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Company Encoding.tmdl new file mode 100644 index 0000000000..8a02aa7654 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Company Encoding.tmdl @@ -0,0 +1,44 @@ +table 'Company Encoding' + isHidden + lineageTag: dfa38049-b6a7-4985-b754-44ee9f836e32 + + column ParameterValue + dataType: string + isHidden + lineageTag: f37fc280-0a49-4975-9389-64f75aa2804f + summarizeBy: none + sourceColumn: ParameterValue + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + column 'Encoded Company Name' + dataType: string + isHidden + lineageTag: d7542b24-c883-42e0-a7d8-7eef5455f11a + summarizeBy: none + sourceColumn: Encoded Company Name + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + partition 'Company Encoding' = m + mode: import + source = + let + Source = #table( + {"ParameterValue"}, + {{COMPANY}} + ), + #"Added Encoded Column" = Table.AddColumn(Source, "Encoded Company Name", each Uri.EscapeDataString(COMPANY), type text) + in + #"Added Encoded Column" + + changedProperty = IsHidden + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Table + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/ENVIRONMENT.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/ENVIRONMENT.tmdl index de5dbd8319..c7c508cd31 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/ENVIRONMENT.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/ENVIRONMENT.tmdl @@ -16,7 +16,7 @@ table ENVIRONMENT partition ENVIRONMENT = m mode: import queryGroup: 'Parameters and Functions' - source = "Sandbox" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "a48072_p48015_US_28-0" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Project Planning Line.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Project Planning Line.tmdl index 34cc5009af..19a62648cc 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Project Planning Line.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Project Planning Line.tmdl @@ -532,7 +532,7 @@ table 'Project Planning Line' in #"Added Type Key" - annotation PBI_ResultType = Table + annotation PBI_ResultType = Exception annotation PBI_NavigationStepName = Navigation diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Project.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Project.tmdl index c38b95cfed..e5548a7574 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Project.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.SemanticModel/definition/tables/Project.tmdl @@ -119,7 +119,7 @@ table Project VAR BusinessCentral = "https://businesscentral.dynamics.com/" VAR TenantID = VALUES('Date Table Setup'[tenantID]) VAR Environment = VALUES(ENVIRONMENT[ENVIRONMENT]) - VAR Company = "?company=" & VALUES(COMPANY[COMPANY]) + VAR Company = "?company=" & VALUES('Company Encoding'[Encoded Company Name]) RETURN BusinessCentral & TenantID & "/" & Environment & "/" & Company isHidden displayFolder: _Project Measures\Back Links diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.pbix deleted file mode 100644 index edb70ba75d..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Projects app/Projects app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/cultures/en-US.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/cultures/en-US.tmdl index 6e06bab72a..51fc920670 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/cultures/en-US.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/cultures/en-US.tmdl @@ -14707,16 +14707,6 @@ cultureInfo en-US }, "Weight": 0.599 } - }, - { - "Variance %": { - "State": "Suggested", - "Source": { - "Type": "External", - "Agent": "PowerBI.VisualColumnRename" - }, - "Weight": 0.9 - } } ] }, @@ -22107,19 +22097,7 @@ cultureInfo en-US "ConceptualProperty": "No. of Distinct Items" } }, - "State": "Generated", - "Terms": [ - { - "Unique Items": { - "State": "Suggested", - "Source": { - "Type": "External", - "Agent": "PowerBI.VisualColumnRename" - }, - "Weight": 0.9 - } - } - ] + "State": "Generated" }, "purchase_value_entries.posted_purchase_invoice_quantity": { "Definition": { diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/database.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/database.tmdl index 3d2c0d97c6..19b27aa405 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/database.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/database.tmdl @@ -1,3 +1,3 @@ database - compatibilityLevel: 1567 + compatibilityLevel: 1600 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/model.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/model.tmdl index fcc437ce05..0e51ac224d 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/model.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/model.tmdl @@ -32,7 +32,7 @@ queryGroup 'Purchase Types' annotation __PBI_TimeIntelligenceEnabled = 0 -annotation PBI_QueryOrder = ["Item","Location","Vendor","Purchaser","Dimension Sets","InvertSignsPurchaseLines","InvertSignsResourceLedgerEntries","InvertSignsGeneralLedgerEntries","ENVIRONMENT","COMPANY","API_ENDPOINT","ConvertUTC","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","GetDimCode","GetDimCodeCaption","GetDimNameCaption","Documents","Purchase Budget","Date Table Setup","Working Days","Standard Calendar Time Intelligence","Fiscal Calendar Time Intelligence","Weekly Calendar Time Intelligence","Purchase Budget Name","Item Category","Purchase Lines","Purchase Value Entries","Resources","G/L Accounts","Purchase Invoice Lines","Purchase Credit Lines","Type","Project","Capacity Ledger Entries","Reason Codes","Item Ledger Document Type","Purchase Document Type","Purchase Line Type"] +annotation PBI_QueryOrder = ["Item","Location","Vendor","Purchaser","Dimension Sets","InvertSignsPurchaseLines","InvertSignsResourceLedgerEntries","InvertSignsGeneralLedgerEntries","ENVIRONMENT","COMPANY","API_ENDPOINT","ConvertUTC","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","GetDimCode","GetDimCodeCaption","GetDimNameCaption","Documents","Purchase Budget","Date Table Setup","Working Days","Standard Calendar Time Intelligence","Fiscal Calendar Time Intelligence","Weekly Calendar Time Intelligence","Purchase Budget Name","Item Category","Purchase Lines","Purchase Value Entries","Resources","G/L Accounts","Purchase Invoice Lines","Purchase Credit Lines","Type","Project","Capacity Ledger Entries","Reason Codes","Item Ledger Document Type","Purchase Document Type","Purchase Line Type","Company Encoding"] annotation __TEdtr = 1 @@ -73,6 +73,7 @@ ref table 'Reason Codes' ref table 'Item Ledger Document Type' ref table 'Purchase Document Type' ref table 'Purchase Line Type' +ref table 'Company Encoding' ref cultureInfo en-US ref cultureInfo da-DK diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Company Encoding.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Company Encoding.tmdl new file mode 100644 index 0000000000..e7cd874c86 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Company Encoding.tmdl @@ -0,0 +1,44 @@ +table 'Company Encoding' + isHidden + lineageTag: 2aa38e5d-adf9-43af-8e26-395c32968ab4 + + column ParameterValue + dataType: string + isHidden + lineageTag: 42877ecb-90b3-4b4a-b771-a3a5eb43f486 + summarizeBy: none + sourceColumn: ParameterValue + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + column 'Encoded Company Name' + dataType: string + isHidden + lineageTag: df24d753-3834-4602-9f9d-f2dc629888ae + summarizeBy: none + sourceColumn: Encoded Company Name + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + partition 'Company Encoding' = m + mode: import + source = + let + Source = #table( + {"ParameterValue"}, + {{COMPANY}} + ), + #"Added Encoded Column" = Table.AddColumn(Source, "Encoded Company Name", each Uri.EscapeDataString(COMPANY), type text) + in + #"Added Encoded Column" + + changedProperty = IsHidden + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Table + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Documents.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Documents.tmdl index 4f064f01ce..6fe18240a7 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Documents.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Documents.tmdl @@ -74,7 +74,7 @@ table Documents VAR BusinessCentral = "https://businesscentral.dynamics.com/" VAR TenantID = VALUES('Date Table Setup'[tenantID]) VAR Environment = VALUES(ENVIRONMENT[ENVIRONMENT]) - VAR Company = "?company=" & VALUES(COMPANY[COMPANY]) + VAR Company = "?company=" & VALUES('Company Encoding'[Encoded Company Name]) RETURN BusinessCentral & TenantID & "/" & Environment & "/" & Company displayFolder: _Purchase Measures\_Back Links lineageTag: bbc266f9-2fa9-45e4-adf0-278036f716f0 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/ENVIRONMENT.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/ENVIRONMENT.tmdl index 53830b5ef1..a8d04485b3 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/ENVIRONMENT.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/ENVIRONMENT.tmdl @@ -16,7 +16,7 @@ table ENVIRONMENT partition ENVIRONMENT = m mode: import queryGroup: 'Parameters and Functions' - source = "Sandbox" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "a48072_p48015_US_28-0" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Item.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Item.tmdl index 313657a552..894715aea9 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Item.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.SemanticModel/definition/tables/Item.tmdl @@ -301,7 +301,7 @@ table Item in #"Added Type Key" - annotation PBI_ResultType = Table + annotation PBI_ResultType = Exception annotation PBI_NavigationStepName = Navigation diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.pbix deleted file mode 100644 index 9e2510dab6..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Purchase app/Purchase app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/model.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/model.tmdl index 7393882c7b..a4b4a84c8e 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/model.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/model.tmdl @@ -33,7 +33,7 @@ queryGroup 'Document and Line Type Enums' annotation __PBI_TimeIntelligenceEnabled = 0 -annotation PBI_QueryOrder = ["Sales Lines","Sales Value Entries","Sales Budget","Item","Location","Customer","Salesperson","Sales Budget Name","Dimension Sets","ENVIRONMENT","COMPANY","API_ENDPOINT","ConvertUTC","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","GetDimCode","GetDimCodeCaption","GetDimNameCaption","InvertSignsSalesLines","InvertSignsResourceLedgerEntries","InvertSignsGeneralLedgerEntries","Standard Calendar Time Intelligence","Fiscal Calendar Time Intelligence","Weekly Calendar Time Intelligence","Date Table Setup","Working Days","Opportunity Entries","Contacts","Reason Codes","Item Category","Opportunities","Sales Cycle Stages","Close Opportunity Codes","Resources","G/L Accounts","Documents","Sales Invoice Lines","Sales Credit Lines","Type","Sales Invoice Project Ledger Entries","Sales Credit Project Ledger Entries","Project Ledger Entries","Project","Sales Line Type","Sales Document Type","Project Journal Line Type","Item Ledger Document Type"] +annotation PBI_QueryOrder = ["Sales Lines","Sales Value Entries","Sales Budget","Item","Location","Customer","Salesperson","Sales Budget Name","Dimension Sets","ENVIRONMENT","COMPANY","API_ENDPOINT","ConvertUTC","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","GetDimCode","GetDimCodeCaption","GetDimNameCaption","InvertSignsSalesLines","InvertSignsResourceLedgerEntries","InvertSignsGeneralLedgerEntries","Standard Calendar Time Intelligence","Fiscal Calendar Time Intelligence","Weekly Calendar Time Intelligence","Date Table Setup","Working Days","Opportunity Entries","Contacts","Reason Codes","Item Category","Opportunities","Sales Cycle Stages","Close Opportunity Codes","Resources","G/L Accounts","Documents","Sales Invoice Lines","Sales Credit Lines","Type","Sales Invoice Project Ledger Entries","Sales Credit Project Ledger Entries","Project Ledger Entries","Project","Sales Line Type","Sales Document Type","Project Journal Line Type","Item Ledger Document Type","Company Encoding"] annotation __TEdtr = 1 @@ -82,6 +82,7 @@ ref table 'Sales Document Type' ref table 'Project Journal Line Type' ref table 'Item Ledger Document Type' ref table 'Lost Customer Month Range' +ref table 'Company Encoding' ref cultureInfo en-US ref cultureInfo es-ES diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Company Encoding.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Company Encoding.tmdl new file mode 100644 index 0000000000..46e5baf992 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Company Encoding.tmdl @@ -0,0 +1,44 @@ +table 'Company Encoding' + isHidden + lineageTag: 179fe678-f573-465e-a78e-bd4ced81294c + + column ParameterValue + dataType: string + isHidden + lineageTag: 6826bae4-1d98-4037-a748-0f4ebacf63ed + summarizeBy: none + sourceColumn: ParameterValue + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + column 'Encoded Company Name' + dataType: string + isHidden + lineageTag: fbbe6b29-c8a7-4614-9247-de7cbffe05e0 + summarizeBy: none + sourceColumn: Encoded Company Name + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + partition 'Company Encoding' = m + mode: import + source = + let + Source = #table( + {"ParameterValue"}, + {{COMPANY}} + ), + #"Added Encoded Column" = Table.AddColumn(Source, "Encoded Company Name", each Uri.EscapeDataString(COMPANY), type text) + in + #"Added Encoded Column" + + changedProperty = IsHidden + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Table + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Documents.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Documents.tmdl index 1f6382476d..52075b48bd 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Documents.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Documents.tmdl @@ -14,7 +14,7 @@ table Documents VAR BusinessCentral = "https://businesscentral.dynamics.com/" VAR TenantID = VALUES('Date Table Setup'[tenantID]) VAR Environment = VALUES(ENVIRONMENT[ENVIRONMENT]) - VAR Company = "?company=" & VALUES(COMPANY[COMPANY]) + VAR Company = "?company=" & VALUES('Company Encoding'[Encoded Company Name]) RETURN BusinessCentral & TenantID & "/" & Environment & "/" & Company displayFolder: _Sales Measures\_Back Links lineageTag: db385722-aa98-416b-bd98-d5058fe788c9 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/ENVIRONMENT.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/ENVIRONMENT.tmdl index 99940a028a..378177aa40 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/ENVIRONMENT.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/ENVIRONMENT.tmdl @@ -16,7 +16,7 @@ table ENVIRONMENT partition ENVIRONMENT = m mode: import queryGroup: 'Parameters and Functions' - source = "Sandbox" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "a48072_p48015_US_28-0" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Sales Lines.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Sales Lines.tmdl index b9122b5780..5e4aa6504d 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Sales Lines.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Sales Lines.tmdl @@ -814,5 +814,5 @@ table 'Sales Lines' annotation PBI_NavigationStepName = Navigation - annotation PBI_ResultType = Table + annotation PBI_ResultType = Exception diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Sales Value Entries.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Sales Value Entries.tmdl index 160a8aa9dd..9e7cf84746 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Sales Value Entries.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.SemanticModel/definition/tables/Sales Value Entries.tmdl @@ -12,7 +12,7 @@ table 'Sales Value Entries' CALCULATE ( DISTINCTCOUNT('Sales Value Entries'[Document No.]), - 'Sales Value Entries'[Document Type] = "Sales Invoice" + 'Sales Value Entries'[Document Type] = "Posted Sales Invoice" ) formatString: #,0 displayFolder: _Sales Value Entry Measures\_Counters diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.pbix deleted file mode 100644 index 8e76a6bba1..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Sales app/Sales app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Subscription Billing app/Subscription Billing app.SemanticModel/definition/database.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Subscription Billing app/Subscription Billing app.SemanticModel/definition/database.tmdl deleted file mode 100644 index 3d2c0d97c6..0000000000 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Subscription Billing app/Subscription Billing app.SemanticModel/definition/database.tmdl +++ /dev/null @@ -1,3 +0,0 @@ -database - compatibilityLevel: 1567 - diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Subscription Billing app/Subscription Billing app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Subscription Billing app/Subscription Billing app.pbix deleted file mode 100644 index d75b207a0e..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Subscription Billing app/Subscription Billing app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/database.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/database.tmdl index 3d2c0d97c6..19b27aa405 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/database.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/database.tmdl @@ -1,3 +1,3 @@ database - compatibilityLevel: 1567 + compatibilityLevel: 1600 diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/model.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/model.tmdl index bf1d823599..23c95c1ae0 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/model.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/model.tmdl @@ -38,7 +38,7 @@ queryGroup 'Parameters and Functions\Connection Parameters' annotation __PBI_TimeIntelligenceEnabled = 0 -annotation PBI_QueryOrder = ["ENVIRONMENT","COMPANY","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","GetDimCode","GetDimCodeCaption","GetDimNameCaption","Sustainability Accounts","Sustainability Account Category","Date Table Setup","Working Days","API_ENDPOINT","Sustainability Ledger Entries","Country/Region","Responsibility Centre","Dimension Sets","Sustainability Goals","Employee Ledger Entries","Employees","Employee Qualifications","Emission Fees","Employee Absences","Sustainability Sub-Account Categories","ConvertUTC"] +annotation PBI_QueryOrder = ["ENVIRONMENT","COMPANY","Dimensions","DimensionSet_Blank","DimensionSets_DataSource","GetDimCode","GetDimCodeCaption","GetDimNameCaption","Sustainability Accounts","Sustainability Account Category","Date Table Setup","Working Days","API_ENDPOINT","Sustainability Ledger Entries","Country/Region","Responsibility Centre","Dimension Sets","Sustainability Goals","Employee Ledger Entries","Employees","Employee Qualifications","Emission Fees","Employee Absences","Sustainability Sub-Account Categories","ConvertUTC","Company Encoding"] annotation __BNorm = 1 @@ -69,6 +69,7 @@ ref table 'Sustainability Sub-Account Categories' ref table Employees ref table ENVIRONMENT ref table 'Translated Localized Labels' +ref table 'Company Encoding' ref cultureInfo en-US ref cultureInfo es-ES diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/COMPANY.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/COMPANY.tmdl index 6dc1b019a0..2b669ace90 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/COMPANY.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/COMPANY.tmdl @@ -26,7 +26,7 @@ table COMPANY partition COMPANY = m mode: import queryGroup: 'Parameters and Functions\Connection Parameters' - source = "Contoso" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "Cronus" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/Company Encoding.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/Company Encoding.tmdl new file mode 100644 index 0000000000..7604f9cd15 --- /dev/null +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/Company Encoding.tmdl @@ -0,0 +1,44 @@ +table 'Company Encoding' + isHidden + lineageTag: 9c90edc8-916f-43e8-9346-ce341fef5308 + + column ParameterValue + dataType: string + isHidden + lineageTag: f716da2e-0bce-4863-b541-a3d4eb862bdd + summarizeBy: none + sourceColumn: ParameterValue + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + column 'Encoded Company Name' + dataType: string + isHidden + lineageTag: 42e44aff-1524-47b8-b388-570cea27942b + summarizeBy: none + sourceColumn: Encoded Company Name + + changedProperty = IsHidden + + annotation SummarizationSetBy = Automatic + + partition 'Company Encoding' = m + mode: import + source = + let + Source = #table( + {"ParameterValue"}, + {{COMPANY}} + ), + #"Added Encoded Column" = Table.AddColumn(Source, "Encoded Company Name", each Uri.EscapeDataString(COMPANY), type text) + in + #"Added Encoded Column" + + changedProperty = IsHidden + + annotation PBI_NavigationStepName = Navigation + + annotation PBI_ResultType = Table + diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/ENVIRONMENT.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/ENVIRONMENT.tmdl index ea61988799..c161e3b15e 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/ENVIRONMENT.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/ENVIRONMENT.tmdl @@ -16,7 +16,7 @@ table ENVIRONMENT partition ENVIRONMENT = m mode: import queryGroup: 'Parameters and Functions\Connection Parameters' - source = "Sandbox" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] + source = "a48072_p48015_US_28-0" meta [IsParameterQuery=true, Type="Text", IsParameterQueryRequired=true] changedProperty = IsHidden diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/Sustainability Ledger Entries.tmdl b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/Sustainability Ledger Entries.tmdl index 5f52c399cc..d41b54e7a4 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/Sustainability Ledger Entries.tmdl +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.SemanticModel/definition/tables/Sustainability Ledger Entries.tmdl @@ -559,11 +559,11 @@ table 'Sustainability Ledger Entries' VAR BusinessCentral = "https://businesscentral.dynamics.com/" VAR TenantID = - VALUES ( 'Date Table Setup'[tenantID] ) //VAR TenantID = VALUES('Date Table Setup'[tenantID]) + VALUES ( 'Date Table Setup'[tenantID] ) VAR Environment = VALUES ( ENVIRONMENT[ENVIRONMENT] ) VAR Company = - "?company=" & VALUES ( COMPANY[COMPANY] ) + "?company=" & VALUES ( 'Company Encoding'[Encoded Company Name] ) RETURN BusinessCentral & TenantID & "/" & Environment & "/" & Company displayFolder: _Back Links diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.pbix b/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.pbix deleted file mode 100644 index 9d3724e7f1..0000000000 Binary files a/src/Apps/W1/PowerBIReports/Power BI Files/Sustainability app/Sustainability app.pbix and /dev/null differ diff --git a/src/Apps/W1/PowerBIReports/Power BI Files/Translations/LocProject.json b/src/Apps/W1/PowerBIReports/Power BI Files/Translations/LocProject.json index 1f1f69400a..926074bcce 100644 --- a/src/Apps/W1/PowerBIReports/Power BI Files/Translations/LocProject.json +++ b/src/Apps/W1/PowerBIReports/Power BI Files/Translations/LocProject.json @@ -17,9 +17,9 @@ "Languages": "cs-CZ;da-DK;de-AT;de-CH;de-DE;en-AU;en-CA;en-GB;en-NZ;es-ES;es-MX;fi-FI;fr-BE;fr-CA;fr-CH;fr-FR;is-IS;it-CH;it-IT;nb-NO;nl-BE;nl-NL;sv-SE", "CopyOption": "UsePlaceholders" }, - { - "SourceFile": "src\\Apps\\W1\\PowerBIReports\\Power BI Files\\Translations\\Subscription Billing app\\SubscriptionBillingApp.en-US.resx", - "OutputPath": "src\\Apps\\W1\\PowerBIReports\\Power BI Files\\Translations\\Subscription Billing app\\SubscriptionBillingApp.{Lang}.resx", + { + "SourceFile": "src\\Apps\\W1\\Subscription Billing\\Power BI Files\\Translations\\SubscriptionBillingApp.en-US.resx", + "OutputPath": "src\\Apps\\W1\\Subscription Billing\\Power BI Files\\Translations\\SubscriptionBillingApp.{Lang}.resx", "Languages": "cs-CZ;da-DK;de-AT;de-CH;de-DE;en-AU;en-CA;en-GB;en-NZ;es-ES;es-MX;fi-FI;fr-BE;fr-CA;fr-CH;fr-FR;is-IS;it-CH;it-IT;nb-NO;nl-BE;nl-NL;sv-SE", "CopyOption": "UsePlaceholders" }, diff --git a/src/Apps/W1/Quality Management/Demo Data/DemoData/1.Setup Data/CreateQMNoSeries.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/1.Setup Data/CreateQMNoSeries.Codeunit.al new file mode 100644 index 0000000000..d3d0dd723c --- /dev/null +++ b/src/Apps/W1/Quality Management/Demo Data/DemoData/1.Setup Data/CreateQMNoSeries.Codeunit.al @@ -0,0 +1,17 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.DemoData.QualityManagement; + +using Microsoft.QualityManagement.Configuration; + +codeunit 5709 "Create QM No Series" +{ + trigger OnRun() + var + QltyAutoConfigure: Codeunit "Qlty. Auto Configure"; + begin + QltyAutoConfigure.EnsureBasicSetupExists(false); + end; +} \ No newline at end of file diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMGenerationRule.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMGenerationRule.Codeunit.al similarity index 100% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMGenerationRule.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMGenerationRule.Codeunit.al diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMGenerationRuleManu.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMGenerationRuleManu.Codeunit.al similarity index 100% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMGenerationRuleManu.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMGenerationRuleManu.Codeunit.al diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMInspTemplateHdr.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMInspTemplateHdr.Codeunit.al similarity index 74% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMInspTemplateHdr.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMInspTemplateHdr.Codeunit.al index 77023d39a8..771e8a49b2 100644 --- a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMInspTemplateHdr.Codeunit.al +++ b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMInspTemplateHdr.Codeunit.al @@ -5,6 +5,7 @@ namespace Microsoft.DemoData.QualityManagement; using Microsoft.DemoTool.Helpers; +using Microsoft.QualityManagement.Configuration.Template; codeunit 5596 "Create QM Insp. Template Hdr" { @@ -23,6 +24,7 @@ codeunit 5596 "Create QM Insp. Template Hdr" ContosoQualityManagement.InsertQualityInspectionTemplateHdr(ProductionFood(), ProductionFoodDescLbl); ContosoQualityManagement.InsertQualityInspectionTemplateHdr(Receive(), ReceiveDescLbl); ContosoQualityManagement.InsertQualityInspectionTemplateHdr(ScheduleChange(), ScheduleChangeDescLbl); + ContosoQualityManagement.InsertQualityInspectionTemplateHdr(Beans(), BeansDescLbl, Enum::"Qlty. Sample Size Source"::"Percent of Quantity", 2); end; procedure BicycleChecklist(): Code[20] @@ -75,15 +77,21 @@ codeunit 5596 "Create QM Insp. Template Hdr" exit(ScheduleChangeTok); end; + procedure Beans(): Code[20] + begin + exit(BeansTok); + end; + var - BicycleChecklistTok: Label 'BICYCLECHECKLIST', MaxLength = 20; - CarTok: Label 'CAR', MaxLength = 20; - PackagingTok: Label 'PACKAGING', MaxLength = 20; - PathogenTok: Label 'PATHOGEN', MaxLength = 20; - ProductionTok: Label 'PRODUCTION', MaxLength = 20; - ProductionFoodTok: Label 'PRODUCTIONFOOD', MaxLength = 20; - ReceiveTok: Label 'RECEIVE', MaxLength = 20; - ScheduleChangeTok: Label 'SCHEDULECHANGE', MaxLength = 20; + BicycleChecklistTok: Label 'BICYCLECHECKLIST', Locked = true, MaxLength = 20; + CarTok: Label 'CAR', Locked = true, MaxLength = 20; + PackagingTok: Label 'PACKAGING', Locked = true, MaxLength = 20; + PathogenTok: Label 'PATHOGEN', Locked = true, MaxLength = 20; + ProductionTok: Label 'PRODUCTION', Locked = true, MaxLength = 20; + ProductionFoodTok: Label 'PRODUCTIONFOOD', Locked = true, MaxLength = 20; + ReceiveTok: Label 'RECEIVE', Locked = true, MaxLength = 20; + ScheduleChangeTok: Label 'SCHEDULECHANGE', Locked = true, MaxLength = 20; + BeansTok: Label 'BEANS', Locked = true, MaxLength = 20; BicycleChecklistDescLbl: Label 'Bicycle Checklist', MaxLength = 100; CorrectiveActionDescLbl: Label 'Corrective Action', MaxLength = 100; @@ -93,4 +101,5 @@ codeunit 5596 "Create QM Insp. Template Hdr" ProductionFoodDescLbl: Label 'Food Production Example', MaxLength = 100; ReceiveDescLbl: Label 'Receiving Example', MaxLength = 100; ScheduleChangeDescLbl: Label 'Scheduler Change', MaxLength = 100; + BeansDescLbl: Label 'Coffee beans, receipt, bag', MaxLength = 100; } diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMInspTemplateLine.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMInspTemplateLine.Codeunit.al similarity index 85% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMInspTemplateLine.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMInspTemplateLine.Codeunit.al index a5ee6f3ed6..7ddf6a3096 100644 --- a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMInspTemplateLine.Codeunit.al +++ b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMInspTemplateLine.Codeunit.al @@ -58,5 +58,13 @@ codeunit 5597 "Create QM Insp. Template Line" ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.ScheduleChange(), 10000, CreateQualityTest.ReasonCode(), ''); ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.ScheduleChange(), 20000, CreateQualityTest.Explanation(), ''); + + ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.Beans(), 10000, CreateQualityTest.BagWeight(), ''); + ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.Beans(), 20000, CreateQualityTest.PackagingVisual(), ''); + ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.Beans(), 30000, CreateQualityTest.Labeling(), ''); + ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.Beans(), 40000, CreateQualityTest.CoffeeDefect(), ''); + ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.Beans(), 50000, CreateQualityTest.CoffeeUniformity(), ''); + ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.Beans(), 60000, CreateQualityTest.Moisture(), ''); + ContosoQualityManagement.InsertQualityInspectionTemplateLine(CreateQMInspTemplateHdr.Beans(), 70000, CreateQualityTest.Comment(), ''); end; } diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMResultConditConf.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMResultConditConf.Codeunit.al similarity index 97% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMResultConditConf.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMResultConditConf.Codeunit.al index 3b4d5152ab..efe9c14321 100644 --- a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQMResultConditConf.Codeunit.al +++ b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQMResultConditConf.Codeunit.al @@ -96,7 +96,7 @@ codeunit 5599 "Create QM Result Condit. Conf." ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.NcrRequirement(), 0, 0, CreateQualityTest.NcrRequirement(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.NcrRequirement(), 0, 0, CreateQualityTest.NcrRequirement(), CreateQualityInspResult.Pass(), '<>''''', 2, Enum::"Qlty. Result Visibility"::Promoted); - ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.Odor(), 0, 0, CreateQualityTest.Odor(), CreateQualityInspResult.Fail(), StrSubstNo('<>%1', CreateQualityTestLookupValue.NoOdor()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); + ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.Odor(), 0, 0, CreateQualityTest.Odor(), CreateQualityInspResult.Fail(), StrSubstNo('%1|%2', CreateQualityTestLookupValue.BadOdor(), CreateQualityTestLookupValue.MildOdor()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.Odor(), 0, 0, CreateQualityTest.Odor(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.Odor(), 0, 0, CreateQualityTest.Odor(), CreateQualityInspResult.Pass(), CreateQualityTestLookupValue.NoOdor(), 2, Enum::"Qlty. Result Visibility"::Promoted); @@ -112,7 +112,7 @@ codeunit 5599 "Create QM Result Condit. Conf." ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.PackageWidth(), 0, 0, CreateQualityTest.PackageWidth(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.PackageWidth(), 0, 0, CreateQualityTest.PackageWidth(), CreateQualityInspResult.Pass(), '10..25', 2, Enum::"Qlty. Result Visibility"::Promoted); - ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.PackagingVisual(), 0, 0, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Fail(), StrSubstNo('<>%1', CreateQualityTestLookupValue.Undamaged()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); + ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.PackagingVisual(), 0, 0, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Fail(), StrSubstNo('%1|%2', CreateQualityTestLookupValue.Heavy(), CreateQualityTestLookupValue.Light()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.PackagingVisual(), 0, 0, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Test, CreateQualityTest.PackagingVisual(), 0, 0, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Pass(), CreateQualityTestLookupValue.Undamaged(), 2, Enum::"Qlty. Result Visibility"::Promoted); @@ -200,7 +200,7 @@ codeunit 5599 "Create QM Result Condit. Conf." ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Car(), 0, 110000, CreateQualityTest.CustomerServiceRepre(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Car(), 0, 110000, CreateQualityTest.CustomerServiceRepre(), CreateQualityInspResult.Pass(), '<>''''', 2, Enum::"Qlty. Result Visibility"::Promoted); - ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 10000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Fail(), StrSubstNo('<>%1', CreateQualityTestLookupValue.Undamaged()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); + ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 10000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Fail(), StrSubstNo('%1|%2', CreateQualityTestLookupValue.Heavy(), CreateQualityTestLookupValue.Light()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 10000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 10000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Pass(), CreateQualityTestLookupValue.Undamaged(), 2, Enum::"Qlty. Result Visibility"::Promoted); @@ -216,7 +216,7 @@ codeunit 5599 "Create QM Result Condit. Conf." ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 40000, CreateQualityTest.PackageHeight(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 40000, CreateQualityTest.PackageHeight(), CreateQualityInspResult.Pass(), '15..30', 2, Enum::"Qlty. Result Visibility"::Promoted); - ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 50000, CreateQualityTest.ShippingLabel(), CreateQualityInspResult.Fail(), StrSubstNo('<>%1', CreateQualityTestLookupValue.Good()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); + ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 50000, CreateQualityTest.ShippingLabel(), CreateQualityInspResult.Fail(), StrSubstNo('%1|%2|%3', CreateQualityTestLookupValue.BadPosition(), CreateQualityTestLookupValue.Blurred(), CreateQualityTestLookupValue.Damage()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 50000, CreateQualityTest.ShippingLabel(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Packaging(), 0, 50000, CreateQualityTest.ShippingLabel(), CreateQualityInspResult.Pass(), CreateQualityTestLookupValue.Good(), 2, Enum::"Qlty. Result Visibility"::Promoted); @@ -232,11 +232,11 @@ codeunit 5599 "Create QM Result Condit. Conf." ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Pathogen(), 0, 30000, CreateQualityTest.EcoliPresent(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Pathogen(), 0, 30000, CreateQualityTest.EcoliPresent(), CreateQualityInspResult.Pass(), CreateQualityTestLookupValue.Absent(), 2, Enum::"Qlty. Result Visibility"::Promoted); - ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.ProductionFood(), 0, 10000, CreateQualityTest.Odor(), CreateQualityInspResult.Fail(), StrSubstNo('<>%1', CreateQualityTestLookupValue.NoOdor()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); + ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.ProductionFood(), 0, 10000, CreateQualityTest.Odor(), CreateQualityInspResult.Fail(), StrSubstNo('%1|%2', CreateQualityTestLookupValue.BadOdor(), CreateQualityTestLookupValue.MildOdor()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.ProductionFood(), 0, 10000, CreateQualityTest.Odor(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.ProductionFood(), 0, 10000, CreateQualityTest.Odor(), CreateQualityInspResult.Pass(), CreateQualityTestLookupValue.NoOdor(), 2, Enum::"Qlty. Result Visibility"::Promoted); - ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.ProductionFood(), 0, 20000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Fail(), StrSubstNo('<>%1', CreateQualityTestLookupValue.Undamaged()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); + ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.ProductionFood(), 0, 20000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Fail(), StrSubstNo('%1|%2', CreateQualityTestLookupValue.Heavy(), CreateQualityTestLookupValue.Light()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.ProductionFood(), 0, 20000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.ProductionFood(), 0, 20000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Pass(), CreateQualityTestLookupValue.Undamaged(), 2, Enum::"Qlty. Result Visibility"::Promoted); @@ -256,7 +256,7 @@ codeunit 5599 "Create QM Result Condit. Conf." ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Receive(), 0, 30000, CreateQualityTest.PackageWidth(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Receive(), 0, 30000, CreateQualityTest.PackageWidth(), CreateQualityInspResult.Pass(), '10..25', 2, Enum::"Qlty. Result Visibility"::Promoted); - ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Receive(), 0, 40000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Fail(), StrSubstNo('<>%1', CreateQualityTestLookupValue.Undamaged()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); + ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Receive(), 0, 40000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Fail(), StrSubstNo('%1|%2', CreateQualityTestLookupValue.Heavy(), CreateQualityTestLookupValue.Light()), 1, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Receive(), 0, 40000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.InProgress(), '', 0, Enum::"Qlty. Result Visibility"::"Configuration only"); ContosoQualityManagement.InsertQltyIResultConditConf(Enum::"Qlty. Result Condition Type"::Template, CreateQMInspTemplateHdr.Receive(), 0, 40000, CreateQualityTest.PackagingVisual(), CreateQualityInspResult.Pass(), CreateQualityTestLookupValue.Undamaged(), 2, Enum::"Qlty. Result Visibility"::Promoted); diff --git a/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityInspResult.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityInspResult.Codeunit.al new file mode 100644 index 0000000000..6b30ded811 --- /dev/null +++ b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityInspResult.Codeunit.al @@ -0,0 +1,58 @@ +// ------------------------------------------------------------------------------------------------ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. +// ------------------------------------------------------------------------------------------------ +namespace Microsoft.DemoData.QualityManagement; + +using Microsoft.DemoTool.Helpers; +using Microsoft.QualityManagement.Configuration; +using Microsoft.QualityManagement.Configuration.Result; + +codeunit 5595 "Create Quality Insp. Result" +{ + InherentEntitlements = X; + InherentPermissions = X; + + var + QltyAutoConfigure: Codeunit "Qlty. Auto Configure"; + + trigger OnRun() + var + ContosoQualityManagement: Codeunit "Contoso Quality Management"; + begin + ContosoQualityManagement.InsertQualityInspectionResult(Fail(), FailDescription(), 1, Enum::"Qlty. Result Copy Behavior"::"Automatically copy the result", Enum::"Qlty. Result Visibility"::"Configuration only", '<>0', '<>""', 'No', Enum::"Qlty. Result Category"::"Not acceptable", Enum::"Qlty. Result Finish Allowed"::"Allow Finish"); + ContosoQualityManagement.InsertQualityInspectionResult(InProgress(), InProgressDescription(), 0, Enum::"Qlty. Result Copy Behavior"::"Automatically copy the result", Enum::"Qlty. Result Visibility"::"Configuration only", '', '', '', Enum::"Qlty. Result Category"::Uncategorized, Enum::"Qlty. Result Finish Allowed"::"Allow Finish"); + ContosoQualityManagement.InsertQualityInspectionResult(Pass(), PassDescription(), 2, Enum::"Qlty. Result Copy Behavior"::"Automatically copy the result", Enum::"Qlty. Result Visibility"::Promoted, '<>0', '<>""', 'Yes', Enum::"Qlty. Result Category"::Acceptable, Enum::"Qlty. Result Finish Allowed"::"Allow Finish"); + end; + + procedure Fail(): Code[20] + begin + exit(CopyStr(QltyAutoConfigure.GetDefaultFailResult(), 1, 20)); + end; + + procedure FailDescription(): Text[100] + begin + exit(CopyStr(QltyAutoConfigure.GetDefaultFailResultDescription(), 1, 100)); + end; + + procedure InProgress(): Code[20] + begin + exit(CopyStr(QltyAutoConfigure.GetDefaultInProgressResult(), 1, 20)); + end; + + procedure InProgressDescription(): Text[100] + begin + exit(CopyStr(QltyAutoConfigure.GetDefaultInProgressResultDescription(), 1, 100)); + end; + + procedure Pass(): Code[20] + begin + exit(CopyStr(QltyAutoConfigure.GetDefaultPassResult(), 1, 20)); + end; + + procedure PassDescription(): Text[100] + begin + exit(CopyStr(QltyAutoConfigure.GetDefaultPassResultDescription(), 1, 100)); + end; + +} diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQualityLookupValue.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityLookupValue.Codeunit.al similarity index 77% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQualityLookupValue.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityLookupValue.Codeunit.al index 2ee4058ea6..cb30bfb0df 100644 --- a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQualityLookupValue.Codeunit.al +++ b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityLookupValue.Codeunit.al @@ -209,39 +209,39 @@ codeunit 5594 "Create Quality Lookup Value" end; var - EcoliPresentTok: Label 'ECOLIPRESENT', MaxLength = 20; - NcrClassificationTok: Label 'NCRCLASSIFICATION', MaxLength = 20; - OdorTok: Label 'ODOR', MaxLength = 20; - PackagingVisualTok: Label 'PACKAGINGVISUAL', MaxLength = 20; - ShippingLabelTok: Label 'SHIPPINGLABEL', MaxLength = 20; - TypeOfCarTok: Label 'TYPEOFCAR', MaxLength = 20; - CoffeeDefectTok: Label 'COFFEE_DEFECT', MaxLength = 20; - CoffeeUniformityTok: Label 'COFFEE_UNIFORMITY', MaxLength = 20; - - AbsentTok: Label 'ABSENT', MaxLength = 100; - PresentTok: Label 'PRESENT', MaxLength = 100; - MajorTok: Label 'MAJOR', MaxLength = 100; - MinorTok: Label 'MINOR', MaxLength = 100; - BadOdorTok: Label 'BADODOR', MaxLength = 100; - MildOdorTok: Label 'MILDODOR', MaxLength = 100; - NoOdorTok: Label 'NOODOR', MaxLength = 100; - HeavyTok: Label 'HEAVY', MaxLength = 100; - LightTok: Label 'LIGHT', MaxLength = 100; - UndamagedTok: Label 'UNDAMAGED', MaxLength = 100; - BadPositionTok: Label 'BADPOSITION', MaxLength = 100; - BlurredTok: Label 'BLURRED', MaxLength = 100; - DamageTok: Label 'DAMAGE', MaxLength = 100; - GoodTok: Label 'GOOD', MaxLength = 100; - ACarTok: Label 'ACAR', MaxLength = 100; - ICarTok: Label 'ICAR', MaxLength = 100; - SCarTok: Label 'SCAR', MaxLength = 100; - CoffeeDefectColorTok: Label 'COLOR', MaxLength = 100; - CoffeeDefectForeignTok: Label 'FOREIGN', MaxLength = 100; - CoffeeDefectInsectTok: Label 'INSECT', MaxLength = 100; - CoffeeDefectOdorTok: Label 'ODOR', MaxLength = 100; - CoffeeUniformityIrregularTok: Label 'IRREGULAR', MaxLength = 100; - CoffeeUniformityMixedTok: Label 'MIXED', MaxLength = 100; - CoffeeUniformityUniformTok: Label 'UNIFORM', MaxLength = 100; + EcoliPresentTok: Label 'ECOLIPRESENT', Locked = true, MaxLength = 20; + NcrClassificationTok: Label 'NCRCLASSIFICATION', Locked = true, MaxLength = 20; + OdorTok: Label 'ODOR', Locked = true, MaxLength = 20; + PackagingVisualTok: Label 'PACKAGINGVISUAL', Locked = true, MaxLength = 20; + ShippingLabelTok: Label 'SHIPPINGLABEL', Locked = true, MaxLength = 20; + TypeOfCarTok: Label 'TYPEOFCAR', Locked = true, MaxLength = 20; + CoffeeDefectTok: Label 'COFFEE_DEFECT', Locked = true, MaxLength = 20; + CoffeeUniformityTok: Label 'COFFEE_UNIFORMITY', Locked = true, MaxLength = 20; + + AbsentTok: Label 'ABSENT', Locked = true, MaxLength = 100; + PresentTok: Label 'PRESENT', Locked = true, MaxLength = 100; + MajorTok: Label 'MAJOR', Locked = true, MaxLength = 100; + MinorTok: Label 'MINOR', Locked = true, MaxLength = 100; + BadOdorTok: Label 'BADODOR', Locked = true, MaxLength = 100; + MildOdorTok: Label 'MILDODOR', Locked = true, MaxLength = 100; + NoOdorTok: Label 'NOODOR', Locked = true, MaxLength = 100; + HeavyTok: Label 'HEAVY', Locked = true, MaxLength = 100; + LightTok: Label 'LIGHT', Locked = true, MaxLength = 100; + UndamagedTok: Label 'UNDAMAGED', Locked = true, MaxLength = 100; + BadPositionTok: Label 'BADPOSITION', Locked = true, MaxLength = 100; + BlurredTok: Label 'BLURRED', Locked = true, MaxLength = 100; + DamageTok: Label 'DAMAGE', Locked = true, MaxLength = 100; + GoodTok: Label 'GOOD', Locked = true, MaxLength = 100; + ACarTok: Label 'ACAR', Locked = true, MaxLength = 100; + ICarTok: Label 'ICAR', Locked = true, MaxLength = 100; + SCarTok: Label 'SCAR', Locked = true, MaxLength = 100; + CoffeeDefectColorTok: Label 'COLOR', Locked = true, MaxLength = 100; + CoffeeDefectForeignTok: Label 'FOREIGN', Locked = true, MaxLength = 100; + CoffeeDefectInsectTok: Label 'INSECT', Locked = true, MaxLength = 100; + CoffeeDefectOdorTok: Label 'ODOR', Locked = true, MaxLength = 100; + CoffeeUniformityIrregularTok: Label 'IRREGULAR', Locked = true, MaxLength = 100; + CoffeeUniformityMixedTok: Label 'MIXED', Locked = true, MaxLength = 100; + CoffeeUniformityUniformTok: Label 'UNIFORM', Locked = true, MaxLength = 100; NoEColiDetectedLbl: Label 'No E-Coli detected', MaxLength = 250; AnyEColiDetectedLbl: Label 'Any E-Coli detected', MaxLength = 250; diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQualityTest.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityTest.Codeunit.al similarity index 80% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQualityTest.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityTest.Codeunit.al index 9c4e539f66..bb842af2dc 100644 --- a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/2.Master Data/CreateQualityTest.Codeunit.al +++ b/src/Apps/W1/Quality Management/Demo Data/DemoData/2.Master Data/CreateQualityTest.Codeunit.al @@ -246,42 +246,42 @@ codeunit 5593 "Create Quality Test" end; var - ApcPerGramTok: Label 'APCPERGRAM', MaxLength = 20; - BrakesCheckTok: Label 'BRAKESCHECK', MaxLength = 20; - CarContainmentTok: Label 'CARCONTAINMENT', MaxLength = 20; - CarRequestedDateTok: Label 'CARREQUESTEDDATE', MaxLength = 20; - CarTypeTok: Label 'CARTYPE', MaxLength = 20; - ColiformCountTok: Label 'COLIFORMCOUNT', MaxLength = 20; - CorrectiveActionTok: Label 'CORRECTIVEACTION', MaxLength = 20; - CustomerServiceRepreTok: Label 'CUSTOMERSERVICEREPRE', MaxLength = 20; - DescriptionOfNonConfTok: Label 'DESCRIPTIONOFNONCONF', MaxLength = 20; - EcoliPresentTok: Label 'ECOLIPRESENT', MaxLength = 20; - ExplanationTok: Label 'EXPLANATION', MaxLength = 20; - GearShiftCheckTok: Label 'GEARSHIFTCHECK', MaxLength = 20; - HandlebarAlignedTok: Label 'HANDLEBARALIGNED', MaxLength = 20; - LblNcrDetailTok: Label 'LBLNCRDETAIL', MaxLength = 20; - LblNcrPlannedActionTok: Label 'LBLNCRPLANNEDACTION', MaxLength = 20; - LblVerificationTok: Label 'LBLVERIFICATION', MaxLength = 20; - NcrClassificationTok: Label 'NCRCLASSIFICATION', MaxLength = 20; - NcrObjectiveEvidenceTok: Label 'NCROBJECTIVEEVIDENCE', MaxLength = 20; - NcrRequirementTok: Label 'NCRREQUIREMENT', MaxLength = 20; - OdorTok: Label 'ODOR', MaxLength = 20; - PackageHeightTok: Label 'PACKAGEHEIGHT', MaxLength = 20; - PackageLengthTok: Label 'PACKAGELENGTH', MaxLength = 20; - PackageWidthTok: Label 'PACKAGEWIDTH', MaxLength = 20; - PackagingVisualTok: Label 'PACKAGINGVISUAL', MaxLength = 20; - ReasonCodeTok: Label 'REASONCODE', MaxLength = 20; - RootCauseFindingsTok: Label 'ROOTCAUSEFINDINGS', MaxLength = 20; - ShippingLabelTok: Label 'SHIPPINGLABEL', MaxLength = 20; - TemperatureTok: Label 'TEMPERATURE', MaxLength = 20; - VerificationOfEffectiTok: Label 'VERIFICATIONOFFFECTI', MaxLength = 20; - VisualWeldCheckTok: Label 'VISUALWELDCHECK', MaxLength = 20; - CoffeeUniformityTok: Label 'COFFEE_UNIFORMITY', MaxLength = 20; - CoffeeDefectTok: Label 'COFFEE_DEFECT', MaxLength = 20; - CommentTok: Label 'COMMENT', MaxLength = 20; - MoistureTok: Label 'MOISTURE', MaxLength = 20; - LabelingTok: Label 'LABELING', MaxLength = 20; - BagWeightTok: Label 'BAG_WEIGHT', MaxLength = 20; + ApcPerGramTok: Label 'APCPERGRAM', Locked = true, MaxLength = 20; + BrakesCheckTok: Label 'BRAKESCHECK', Locked = true, MaxLength = 20; + CarContainmentTok: Label 'CARCONTAINMENT', Locked = true, MaxLength = 20; + CarRequestedDateTok: Label 'CARREQUESTEDDATE', Locked = true, MaxLength = 20; + CarTypeTok: Label 'CARTYPE', Locked = true, MaxLength = 20; + ColiformCountTok: Label 'COLIFORMCOUNT', Locked = true, MaxLength = 20; + CorrectiveActionTok: Label 'CORRECTIVEACTION', Locked = true, MaxLength = 20; + CustomerServiceRepreTok: Label 'CUSTOMERSERVICEREPRE', Locked = true, MaxLength = 20; + DescriptionOfNonConfTok: Label 'DESCRIPTIONOFNONCONF', Locked = true, MaxLength = 20; + EcoliPresentTok: Label 'ECOLIPRESENT', Locked = true, MaxLength = 20; + ExplanationTok: Label 'EXPLANATION', Locked = true, MaxLength = 20; + GearShiftCheckTok: Label 'GEARSHIFTCHECK', Locked = true, MaxLength = 20; + HandlebarAlignedTok: Label 'HANDLEBARALIGNED', Locked = true, MaxLength = 20; + LblNcrDetailTok: Label 'LBLNCRDETAIL', Locked = true, MaxLength = 20; + LblNcrPlannedActionTok: Label 'LBLNCRPLANNEDACTION', Locked = true, MaxLength = 20; + LblVerificationTok: Label 'LBLVERIFICATION', Locked = true, MaxLength = 20; + NcrClassificationTok: Label 'NCRCLASSIFICATION', Locked = true, MaxLength = 20; + NcrObjectiveEvidenceTok: Label 'NCROBJECTIVEEVIDENCE', Locked = true, MaxLength = 20; + NcrRequirementTok: Label 'NCRREQUIREMENT', Locked = true, MaxLength = 20; + OdorTok: Label 'ODOR', Locked = true, MaxLength = 20; + PackageHeightTok: Label 'PACKAGEHEIGHT', Locked = true, MaxLength = 20; + PackageLengthTok: Label 'PACKAGELENGTH', Locked = true, MaxLength = 20; + PackageWidthTok: Label 'PACKAGEWIDTH', Locked = true, MaxLength = 20; + PackagingVisualTok: Label 'PACKAGINGVISUAL', Locked = true, MaxLength = 20; + ReasonCodeTok: Label 'REASONCODE', Locked = true, MaxLength = 20; + RootCauseFindingsTok: Label 'ROOTCAUSEFINDINGS', Locked = true, MaxLength = 20; + ShippingLabelTok: Label 'SHIPPINGLABEL', Locked = true, MaxLength = 20; + TemperatureTok: Label 'TEMPERATURE', Locked = true, MaxLength = 20; + VerificationOfEffectiTok: Label 'VERIFICATIONOFFFECTI', Locked = true, MaxLength = 20; + VisualWeldCheckTok: Label 'VISUALWELDCHECK', Locked = true, MaxLength = 20; + CoffeeUniformityTok: Label 'COFFEE_UNIFORMITY', Locked = true, MaxLength = 20; + CoffeeDefectTok: Label 'COFFEE_DEFECT', Locked = true, MaxLength = 20; + CommentTok: Label 'COMMENT', Locked = true, MaxLength = 20; + MoistureTok: Label 'MOISTURE', Locked = true, MaxLength = 20; + LabelingTok: Label 'LABELING', Locked = true, MaxLength = 20; + BagWeightTok: Label 'BAGWEIGHT', Locked = true, MaxLength = 20; ApcPerGramDescLbl: Label 'Aerobic Plate Count per Gram', MaxLength = 100; BrakesCheckDescLbl: Label 'Brakes Check', MaxLength = 100; @@ -292,7 +292,7 @@ codeunit 5593 "Create Quality Test" CorrectiveActionDescLbl: Label 'Corrective Action', MaxLength = 100; CustomerServiceRepreDescLbl: Label 'Customer Service Representative', MaxLength = 100; DescriptionOfNonConfDescLbl: Label 'Description of Non Conformance', MaxLength = 100; - EcoliPresentDescLbl: Label 'ECOLIPRESENT', MaxLength = 100; + EcoliPresentDescLbl: Label 'E. coli Present', MaxLength = 100; ExplanationDescLbl: Label 'Explanation', MaxLength = 100; GearShiftCheckDescLbl: Label 'Gear Shift Check', MaxLength = 100; HandlebarAlignedDescLbl: Label 'Handlebar Aligned', MaxLength = 100; @@ -304,7 +304,7 @@ codeunit 5593 "Create Quality Test" NcrRequirementDescLbl: Label 'Requirement / Clause No.(s)', MaxLength = 100; OdorDescLbl: Label 'Odor', MaxLength = 100; PackageHeightDescLbl: Label 'Package Height', MaxLength = 100; - PackageLengthDescLbl: Label 'package length', MaxLength = 100; + PackageLengthDescLbl: Label 'Package Length', MaxLength = 100; PackageWidthDescLbl: Label 'Package Width', MaxLength = 100; PackagingVisualDescLbl: Label 'Packaging Visual', MaxLength = 100; ReasonCodeDescLbl: Label 'Reason Code', MaxLength = 100; @@ -313,10 +313,10 @@ codeunit 5593 "Create Quality Test" TemperatureDescLbl: Label 'Temperature', MaxLength = 100; VerificationOfEffectiDescLbl: Label 'Verification of Effectiveness', MaxLength = 100; VisualWeldCheckDescLbl: Label 'Visual Weld Check', MaxLength = 100; - CoffeeUniformityDescLbl: Label 'Coffee bean uniformity', MaxLength = 100; - CoffeeDefectDescLbl: Label 'Coffee bean defect type', MaxLength = 100; - CommentDescLbl: Label 'Additional comments', MaxLength = 100; - MoistureDescLbl: Label 'Moisture content (%)', MaxLength = 100; - LabelingDescLbl: Label 'Labeling correct and readable', MaxLength = 100; - BagWeightDescLbl: Label 'Bag weight (kg)', MaxLength = 100; + CoffeeUniformityDescLbl: Label 'Coffee Bean Uniformity', MaxLength = 100; + CoffeeDefectDescLbl: Label 'Coffee Bean Defect Type', MaxLength = 100; + CommentDescLbl: Label 'Additional Comments', MaxLength = 100; + MoistureDescLbl: Label 'Moisture Content (%)', MaxLength = 100; + LabelingDescLbl: Label 'Labeling Correct and Readable', MaxLength = 100; + BagWeightDescLbl: Label 'Bag Weight (kg)', MaxLength = 100; } diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/QualityManagementModule.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoData/QualityManagementModule.Codeunit.al similarity index 96% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/QualityManagementModule.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoData/QualityManagementModule.Codeunit.al index dd77fecbb3..4ba0a81051 100644 --- a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoData/QualityManagementModule.Codeunit.al +++ b/src/Apps/W1/Quality Management/Demo Data/DemoData/QualityManagementModule.Codeunit.al @@ -25,6 +25,7 @@ codeunit 5592 "Quality Management Module" implements "Contoso Demo Data Module" procedure CreateSetupData() begin + Codeunit.Run(Codeunit::"Create QM No Series"); end; procedure CreateMasterData() diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoTool/ContosoHelpers/ContosoQualityManagement.Codeunit.al b/src/Apps/W1/Quality Management/Demo Data/DemoTool/ContosoHelpers/ContosoQualityManagement.Codeunit.al similarity index 91% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoTool/ContosoHelpers/ContosoQualityManagement.Codeunit.al rename to src/Apps/W1/Quality Management/Demo Data/DemoTool/ContosoHelpers/ContosoQualityManagement.Codeunit.al index 7f547f9264..70840bb465 100644 --- a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoTool/ContosoHelpers/ContosoQualityManagement.Codeunit.al +++ b/src/Apps/W1/Quality Management/Demo Data/DemoTool/ContosoHelpers/ContosoQualityManagement.Codeunit.al @@ -147,6 +147,29 @@ codeunit 5710 "Contoso Quality Management" QltyInspectionTemplateHdr.Insert(true); end; + procedure InsertQualityInspectionTemplateHdr(Code: Code[20]; Description: Text[100]; SampleSource: Enum "Qlty. Sample Size Source"; SamplePercentage: Decimal) + var + QltyInspectionTemplateHdr: Record "Qlty. Inspection Template Hdr."; + Exists: Boolean; + begin + if QltyInspectionTemplateHdr.Get(Code) then begin + Exists := true; + + if not OverwriteData then + exit; + end; + + QltyInspectionTemplateHdr.Validate(Code, Code); + QltyInspectionTemplateHdr.Validate(Description, Description); + QltyInspectionTemplateHdr.Validate("Sample Source", SampleSource); + QltyInspectionTemplateHdr.Validate("Sample Percentage", SamplePercentage); + + if Exists then + QltyInspectionTemplateHdr.Modify(true) + else + QltyInspectionTemplateHdr.Insert(true); + end; + procedure InsertQualityInspectionTemplateLine(TemplateCode: Code[20]; LineNo: Integer; TestCode: Code[20]; Description: Text[100]) var QltyInspectionTemplateLine: Record "Qlty. Inspection Template Line"; diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoTool/QltyContosoDemoDataModule.EnumExt.al b/src/Apps/W1/Quality Management/Demo Data/DemoTool/QltyContosoDemoDataModule.EnumExt.al similarity index 100% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/DemoTool/QltyContosoDemoDataModule.EnumExt.al rename to src/Apps/W1/Quality Management/Demo Data/DemoTool/QltyContosoDemoDataModule.EnumExt.al diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/ExtensionLogo.png b/src/Apps/W1/Quality Management/Demo Data/ExtensionLogo.png similarity index 100% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/ExtensionLogo.png rename to src/Apps/W1/Quality Management/Demo Data/ExtensionLogo.png diff --git a/src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/app.json b/src/Apps/W1/Quality Management/Demo Data/app.json similarity index 100% rename from src/Apps/W1/QualityManagementContosoCoffeeDemoDataset/app.json rename to src/Apps/W1/Quality Management/Demo Data/app.json diff --git a/src/Apps/W1/Quality Management/Test Library/src/QltyInspectionUtility.Codeunit.al b/src/Apps/W1/Quality Management/Test Library/src/QltyInspectionUtility.Codeunit.al index 568f56711b..86a3051844 100644 --- a/src/Apps/W1/Quality Management/Test Library/src/QltyInspectionUtility.Codeunit.al +++ b/src/Apps/W1/Quality Management/Test Library/src/QltyInspectionUtility.Codeunit.al @@ -54,7 +54,7 @@ codeunit 139940 "Qlty. Inspection Utility" LibraryUtility: Codeunit "Library - Utility"; NoSeriesCodeunit: Codeunit "No. Series"; DefaultResult2PassCodeLbl: Label 'PASS', Locked = true; - SupervisorRoleIDTok: Label 'QltyMngmnt - Edit', Locked = true; + AdminSupervisorRoleIDTok: Label 'QltyMgmt - Admin', Locked = true; internal procedure EnsureSetupExists() var @@ -62,7 +62,7 @@ codeunit 139940 "Qlty. Inspection Utility" UserPermissionsLibrary: Codeunit "User Permissions Library"; begin QltyAutoConfigure.EnsureBasicSetupExists(false); - UserPermissionsLibrary.AssignPermissionSetToUser(UserSecurityId(), SupervisorRoleIDTok); + UserPermissionsLibrary.AssignPermissionSetToUser(UserSecurityId(), AdminSupervisorRoleIDTok); end; internal procedure CreateABasicTemplateAndInstanceOfAInspection(var OutCreatedQltyInspectionHeader: Record "Qlty. Inspection Header"; var OutQltyInspectionTemplateHdr: Record "Qlty. Inspection Template Hdr.") diff --git a/src/Apps/W1/Quality Management/app/src/AccessControl/QltyPermissionMgmt.Codeunit.al b/src/Apps/W1/Quality Management/app/src/AccessControl/QltyPermissionMgmt.Codeunit.al index d56651c00d..14f0978e9c 100644 --- a/src/Apps/W1/Quality Management/app/src/AccessControl/QltyPermissionMgmt.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/AccessControl/QltyPermissionMgmt.Codeunit.al @@ -28,7 +28,7 @@ codeunit 20406 "Qlty. Permission Mgmt." ActionChangeItemTrackingLbl: Label 'change item tracking'; ActionChangeSourceQuantityLbl: Label 'change source quantity'; ActionEditLineCommentLbl: Label 'edit line note/comment'; - SupervisorRoleIDTxt: Label 'QltyMngmnt - Edit', Locked = true; + AdminSupervisorRoleIDTxt: Label 'QltyMgmt - Admin', Locked = true; UserDoesNotHavePermissionToErr: Label 'The user [%1] does not have permission to [%2].', Comment = '%1=User id, %2=permission being attempted'; /// @@ -64,7 +64,7 @@ codeunit 20406 "Qlty. Permission Mgmt." /// True if the user can change other users' inspections; otherwise, false. internal procedure CanChangeOtherInspections(): Boolean begin - exit(HasSupervisorRole()); + exit(HasAdminSupervisorRole()); end; /// @@ -90,10 +90,22 @@ codeunit 20406 "Qlty. Permission Mgmt." /// internal procedure VerifyCanReopenInspection() begin - if not CanModifyTableData(Database::"Qlty. Inspection Header") then + if not CanReopenInspection() then Error(UserDoesNotHavePermissionToErr, UserId(), ActionReopenInspectionLbl); end; + /// + /// Checks if the current user can reopen an inspection. + /// + /// True if the user can reopen an inspection; otherwise, false. + local procedure CanReopenInspection(): Boolean + begin + if not CanModifyTableData(Database::"Qlty. Inspection Header") then + exit(false); + + exit(HasAdminSupervisorRole()); + end; + /// /// Verifies the current user can delete an open inspection. Throws an error if not permitted. /// @@ -121,7 +133,7 @@ codeunit 20406 "Qlty. Permission Mgmt." if not CanDeleteTableData(Database::"Qlty. Inspection Header") then exit(false); - exit(HasSupervisorRole()); + exit(HasAdminSupervisorRole()); end; /// @@ -160,7 +172,7 @@ codeunit 20406 "Qlty. Permission Mgmt." if not CanModifyTableData(Database::"Qlty. Inspection Header") then exit(false); - exit(HasSupervisorRole()); + exit(HasAdminSupervisorRole()); end; /// @@ -195,15 +207,15 @@ codeunit 20406 "Qlty. Permission Mgmt." end; #region Verify Permissions - local procedure HasSupervisorRole() IsAssigned: Boolean + local procedure HasAdminSupervisorRole() IsAssigned: Boolean var UserPermissions: Codeunit "User Permissions"; CurrentExtensionModuleInfo: ModuleInfo; begin - IsAssigned := HasUserPermissionSetDirectlyAssigned(UserSecurityId(), SupervisorRoleIDTxt); + IsAssigned := HasUserPermissionSetDirectlyAssigned(UserSecurityId(), AdminSupervisorRoleIDTxt); if not IsAssigned then if NavApp.GetCurrentModuleInfo(CurrentExtensionModuleInfo) then - IsAssigned := UserPermissions.HasUserPermissionSetAssigned(UserSecurityId(), CompanyName(), SupervisorRoleIDTxt, 0, CurrentExtensionModuleInfo.Id()); + IsAssigned := UserPermissions.HasUserPermissionSetAssigned(UserSecurityId(), CompanyName(), AdminSupervisorRoleIDTxt, 0, CurrentExtensionModuleInfo.Id()); if not IsAssigned then IsAssigned := UserPermissions.IsSuper(UserSecurityId()); end; diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/GenerationRule/JobQueue/QltyScheduleInspection.Report.al b/src/Apps/W1/Quality Management/app/src/Configuration/GenerationRule/JobQueue/QltyScheduleInspection.Report.al index 5c7151a0f8..4d477e53c9 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/GenerationRule/JobQueue/QltyScheduleInspection.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/GenerationRule/JobQueue/QltyScheduleInspection.Report.al @@ -5,6 +5,7 @@ namespace Microsoft.QualityManagement.Configuration.GenerationRule.JobQueue; using Microsoft.QualityManagement.Configuration.GenerationRule; +using Microsoft.QualityManagement.Configuration.SourceConfiguration; using Microsoft.QualityManagement.Document; using Microsoft.QualityManagement.Setup; @@ -14,6 +15,7 @@ report 20412 "Qlty. Schedule Inspection" AdditionalSearchTerms = 'Periodic inspections'; ToolTip = 'Run this report to bulk create inspections based on generation rules for the selected template, or schedule it in the job queue for periodic inspection creation.'; ProcessingOnly = true; + AccessByPermission = tabledata "Qlty. Inspection Gen. Rule" = R; ApplicationArea = QualityManagement; UsageCategory = Tasks; AllowScheduling = true; @@ -71,6 +73,7 @@ report 20412 "Qlty. Schedule Inspection" CreatedQltyInspectionIds: List of [Code[20]]; ZeroInspectionsCreatedMsg: Label 'No inspections were created.'; SomeInspectionsWereCreatedQst: Label '%1 inspections were created. Do you want to see them?', Comment = '%1=the count of inspections that were created.'; + NoSourceConfigForScheduleErr: Label 'Cannot schedule inspections because no enabled source configuration with a table filter exists for source table %1. Navigate to the Quality Inspection Source Configuration page and ensure at least one enabled configuration exists for this table with a From Table Filter defined.', Comment = '%1=the source table number'; trigger OnInitReport() begin @@ -102,22 +105,42 @@ report 20412 "Qlty. Schedule Inspection" procedure CreateInspectionsThatMatchRule(QltyInspectionGenRule: Record "Qlty. Inspection Gen. Rule") var QltyJobQueueManagement: Codeunit "Qlty. Job Queue Management"; - SourceRecordRef: RecordRef; begin if QltyInspectionGenRule."Activation Trigger" = QltyInspectionGenRule."Activation Trigger"::Disabled then exit; QltyJobQueueManagement.CheckIfGenerationRuleCanBeScheduled(QltyInspectionGenRule); - SourceRecordRef.Open(QltyInspectionGenRule."Source Table No."); - if QltyInspectionGenRule."Condition Filter" <> '' then - SourceRecordRef.SetView(QltyInspectionGenRule."Condition Filter"); - QltyInspectionGenRule.SetRecFilter(); if QltyInspectionGenRule."Schedule Group" <> '' then QltyInspectionGenRule.SetRange("Schedule Group", QltyInspectionGenRule."Schedule Group"); QltyInspectionGenRule.SetRange("Template Code", QltyInspectionGenRule."Template Code"); - if SourceRecordRef.FindSet() then - QltyInspectionCreate.CreateMultipleInspectionsWithoutDisplaying(SourceRecordRef, GuiAllowed(), QltyInspectionGenRule, CreatedQltyInspectionIds); + + CreateInspectionsPerSourceConfigFilter(QltyInspectionGenRule); + end; + + local procedure CreateInspectionsPerSourceConfigFilter(var QltyInspectionGenRule: Record "Qlty. Inspection Gen. Rule") + var + QltyInspectSourceConfig: Record "Qlty. Inspect. Source Config."; + SourceRecordRef: RecordRef; + begin + QltyInspectSourceConfig.SetRange("From Table No.", QltyInspectionGenRule."Source Table No."); + QltyInspectSourceConfig.SetRange("To Type", QltyInspectSourceConfig."To Type"::Inspection); + QltyInspectSourceConfig.SetRange(Enabled, true); + QltyInspectSourceConfig.SetFilter("From Table Filter", '<>%1', ''); + if not QltyInspectSourceConfig.FindSet() then + Error(NoSourceConfigForScheduleErr, QltyInspectionGenRule."Source Table No."); + + repeat + Clear(SourceRecordRef); + SourceRecordRef.Open(QltyInspectionGenRule."Source Table No."); + if QltyInspectionGenRule."Condition Filter" <> '' then + SourceRecordRef.SetView(QltyInspectionGenRule."Condition Filter"); + SourceRecordRef.FilterGroup(20); + SourceRecordRef.SetView(QltyInspectSourceConfig."From Table Filter"); + SourceRecordRef.FilterGroup(0); + if SourceRecordRef.FindSet() then + QltyInspectionCreate.CreateMultipleInspectionsWithoutDisplaying(SourceRecordRef, GuiAllowed(), QltyInspectionGenRule, CreatedQltyInspectionIds); + until QltyInspectSourceConfig.Next() = 0; end; } \ No newline at end of file diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/QltyAutoConfigure.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Configuration/QltyAutoConfigure.Codeunit.al index 010bd4d2f9..ba571a4452 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/QltyAutoConfigure.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/QltyAutoConfigure.Codeunit.al @@ -93,12 +93,37 @@ codeunit 20402 "Qlty. Auto Configure" OpenLedgerToInspectTok: Label 'ITEMLDGROPENINSPECT', MaxLength = 20, Locked = true; OpenLedgerToInspectDescriptionTxt: Label 'Open Item Ledger Entry to Inspection', MaxLength = 100; - internal procedure GetDefaultPassResult(): Text + procedure GetDefaultPassResult(): Text begin exit(DefaultResult2PassCodeTok); end; - internal procedure EnsureBasicSetupExists(ShowMessage: Boolean) + procedure GetDefaultFailResult(): Text + begin + exit(DefaultResult1FailCodeTok); + end; + + procedure GetDefaultInProgressResult(): Text + begin + exit(DefaultResult0InProgressCodeTok); + end; + + procedure GetDefaultPassResultDescription(): Text + begin + exit(DefaultResult2PassDescriptionTxt); + end; + + procedure GetDefaultFailResultDescription(): Text + begin + exit(DefaultResult1FailDescriptionTxt); + end; + + procedure GetDefaultInProgressResultDescription(): Text + begin + exit(DefaultResult0InProgressDescriptionTxt); + end; + + procedure EnsureBasicSetupExists(ShowMessage: Boolean) begin EnsureSetupRecordExists(); EnsureResultExists(); diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/Result/QltyInspectionResultList.Page.al b/src/Apps/W1/Quality Management/app/src/Configuration/Result/QltyInspectionResultList.Page.al index 1a6aae33ea..6aea727286 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/Result/QltyInspectionResultList.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/Result/QltyInspectionResultList.Page.al @@ -135,7 +135,7 @@ page 20416 "Qlty. Inspection Result List" ApplicationArea = QualityManagement; Caption = 'Update Tests, Templates, and Inspections'; ToolTip = 'Adds newly created results to existing quality tests and templates, adjusts evaluation sequences, and updates promoted results. Inspections based on these templates are also updated.'; - Image = Copy; + Image = CopyToTask; trigger OnAction() var diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionCopyTemplate.Report.al b/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionCopyTemplate.Report.al index b517e4890c..fdbe195f85 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionCopyTemplate.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionCopyTemplate.Report.al @@ -24,7 +24,7 @@ report 20402 "Qlty. Inspection Copy Template" { MaxIteration = 1; RequestFilterFields = Code, Description; - RequestFilterHeading = 'Quality Inspection Template to copy from'; + RequestFilterHeading = 'Source Template'; trigger OnPreDataItem() begin @@ -42,7 +42,7 @@ report 20402 "Qlty. Inspection Copy Template" { RequestFilterFields = "No.", Description, "Description 2", "Lot Nos."; DataItemTableView = sorting("No.") where("No." = filter(<> '')); - RequestFilterHeading = 'Item numbers to use for creating template codes'; + RequestFilterHeading = 'Items (for Bulk Copy)'; trigger OnAfterGetRecord() begin @@ -66,19 +66,20 @@ report 20402 "Qlty. Inspection Copy Template" { group(General) { - Caption = 'General'; + Caption = 'Options'; + InstructionalText = 'Choose how to create the new template(s). Use Bulk Copy to create one template per item, or specify a single target code and description below.'; field(ChooseFromItems; CreateFromItems) { ApplicationArea = All; - Caption = 'Create from Item Nos.'; - ToolTip = 'Specifies whether to create templates from items.'; + Caption = 'Bulk Copy from Items'; + ToolTip = 'Specifies whether to create multiple templates based on items. When enabled, a new template is created for each item selected in the Items tab, using the item number as the template code. When disabled, a single template is created using the target code and description specified below.'; } } group(NonItem) { - Caption = 'Code and Description for duplicating not based on an item'; - InstructionalText = 'Use this with Create From Item Nos. turned off.'; + Caption = 'Target Template'; + InstructionalText = 'Specify the code and description for the new template.'; Visible = not CreateFromItems; field(ChooseTargetName; TargetDestinationName) @@ -86,14 +87,14 @@ report 20402 "Qlty. Inspection Copy Template" ApplicationArea = All; Enabled = not CreateFromItems; Caption = 'Target Code'; - ToolTip = 'Specifies whether to name a specific template target'; + ToolTip = 'Specifies the code for the new template. This must be unique and will identify the copied template.'; } field(ChooseTargetDescription; Description) { ApplicationArea = All; Enabled = not CreateFromItems; Caption = 'Target Description'; - ToolTip = 'Specifies the description of the target template inspection'; + ToolTip = 'Specifies the description for the new template.'; } } } @@ -106,7 +107,7 @@ report 20402 "Qlty. Inspection Copy Template" Description: Text[100]; ThisWillReplaceTemplateConfigQst: Label 'This will duplicates the source template to %1 templates for %1 items.\\Target template names will use the item no. as their name.\\ If an existing template exists then template lines will be added, but will not be removed. Any removal of template lines must be done manually, or via a process such as a configuration package. \\ Do you want to proceed?', Comment = '%1=how many templates will be added/updated'; ASingleTemplateErr: Label 'A single template must be chosen. The filters supplied result in %1 templates.', Comment = '%1=the expected number of templates'; - MustSpecifyACodeAndDescriptionErr: Label 'When using this report and not copying items you must specify a destination code and description.'; + MustSpecifyACodeAndDescriptionErr: Label 'You must specify a target code and description when Bulk Copy from Items is disabled.'; local procedure CopyTemplateFromItem(CopyFromQltyInspectionTemplateHdr: Record "Qlty. Inspection Template Hdr."; Item: Record Item) var diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplate.Page.al b/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplate.Page.al index 93a2047966..d576634720 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplate.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplate.Page.al @@ -132,7 +132,7 @@ page 20402 "Qlty. Inspection Template" AccessByPermission = tabledata "Qlty. Inspection Header" = I; Caption = 'Create Inspection'; ToolTip = 'Specifies to create a new Quality Inspection using this template.'; - Image = CreateForm; + Image = BulletList; Promoted = true; PromotedCategory = Process; PromotedIsBig = true; @@ -173,7 +173,7 @@ page 20402 "Qlty. Inspection Template" ToolTip = 'View existing Quality Inspection Generation Rules related to this template. A Quality Inspection generation rule defines when you want to ask a set of questions defined in a template. You connect a template to a source table, and set the criteria to use that template with the table filter. When these filter criteria is met, then it will choose that template.'; AboutTitle = 'Inspection Generation Rules'; AboutText = 'View inspection generation rules for this template. These rules define when the questions in the template are asked.'; - Image = FilterLines; + Image = CopyFromTask; Promoted = true; PromotedCategory = Process; PromotedIsBig = true; @@ -186,7 +186,7 @@ page 20402 "Qlty. Inspection Template" { Caption = 'Existing Inspections'; ToolTip = 'Review existing inspections created using this template.'; - Image = TaskQualityMeasure; + Image = CheckList; Promoted = true; PromotedCategory = Process; PromotedIsBig = true; diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplateEdit.Page.al b/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplateEdit.Page.al index 6f03ca8b73..e1ba8b75b1 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplateEdit.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplateEdit.Page.al @@ -127,7 +127,7 @@ page 20440 "Qlty. Inspection Template Edit" ShowCaption = false; Editable = false; Caption = ' '; - Tooltip = ' '; + ToolTip = 'Select to test the expression with the selected inspection and inspection line.'; trigger OnDrillDown() var @@ -161,7 +161,7 @@ page 20440 "Qlty. Inspection Template Edit" { ApplicationArea = All; Caption = 'Add Test'; - Image = CopyFromTask; + Image = TaskQualityMeasure; ToolTip = 'Click here to use a Quality Inspection test in this expression.'; Promoted = true; PromotedIsBig = true; diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplateList.Page.al b/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplateList.Page.al index b841107f4b..7d69ccd9ae 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplateList.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/Template/QltyInspectionTemplateList.Page.al @@ -74,7 +74,7 @@ page 20404 "Qlty. Inspection Template List" ToolTip = 'Specifies to create a new Quality Inspection using this template.'; AboutTitle = 'More ways to create inspections'; AboutText = 'Use this action to create a manual inspection from the selected template. You can also create inspections directly from other pages, such as output journals, production order routing lines, consumption journals, purchase orders, sales returns, and item tracking lines.'; - Image = CreateForm; + Image = BulletList; Promoted = true; PromotedCategory = Process; PromotedIsBig = true; @@ -116,7 +116,7 @@ page 20404 "Qlty. Inspection Template List" Scope = Repeater; Caption = 'Inspection Generation Rules'; ToolTip = 'View existing Quality Inspection Generation Rules related to this template. A Quality Inspection generation rule defines when you want to ask a set of questions defined in a template. You connect a template to a source table, and set the criteria to use that template with the table filter. When these filter criteria is met, then it will choose that template.'; - Image = FilterLines; + Image = CopyFromTask; Promoted = true; PromotedCategory = Process; PromotedIsBig = true; @@ -129,7 +129,7 @@ page 20404 "Qlty. Inspection Template List" Scope = Repeater; Caption = 'Existing Inspections'; ToolTip = 'Review existing inspections created using this template.'; - Image = TaskQualityMeasure; + Image = CheckList; Promoted = true; PromotedCategory = Process; PromotedIsBig = true; diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTest.Table.al b/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTest.Table.al index e569b82c46..9f4cbf3006 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTest.Table.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTest.Table.al @@ -366,7 +366,7 @@ table 20401 "Qlty. Test" trigger OnDelete() begin - CheckDeleteConstraints(false); + CheckDeleteConstraints(true); end; procedure CheckDeleteConstraints(AskQuestion: Boolean) diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTestCard.Page.al b/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTestCard.Page.al index 3ed7f135f5..bff0a20cc2 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTestCard.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTestCard.Page.al @@ -16,7 +16,6 @@ page 20479 "Qlty. Test Card" Caption = 'Quality Test'; AboutTitle = 'About Quality Test details'; AboutText = 'Use this page to define questions, measurements, allowed values, and default passing conditions. Add tests to templates to use them in quality inspections.'; - DeleteAllowed = false; PageType = Card; SourceTable = "Qlty. Test"; SourceTableView = sorting(Code); @@ -545,6 +544,7 @@ page 20479 "Qlty. Test Card" } } +#if not CLEAN29 actions { area(Processing) @@ -554,6 +554,10 @@ page 20479 "Qlty. Test Card" Caption = 'Delete'; Image = Delete; ToolTip = 'Deletes this test. A test can only be deleted if it is not being used on an existing inspection.'; + Visible = false; + ObsoleteState = Pending; + ObsoleteReason = 'Deletion is handled by standard page behavior through the OnDelete trigger on Qlty. Test table.'; + ObsoleteTag = '29.0'; trigger OnAction() begin @@ -566,11 +570,17 @@ page 20479 "Qlty. Test Card" area(Promoted) { +#pragma warning disable AL0432 actionref(DeleteRecordSafe_Promoted; DeleteRecordSafe) +#pragma warning restore AL0432 { + ObsoleteState = Pending; + ObsoleteReason = 'Deletion is handled by standard page behavior through the OnDelete trigger on Qlty. Test table.'; + ObsoleteTag = '29.0'; } } } +#endif var QltyResultConditionMgmt: Codeunit "Qlty. Result Condition Mgmt."; diff --git a/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTests.Page.al b/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTests.Page.al index 125107abc7..8123724450 100644 --- a/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTests.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Configuration/Template/Test/QltyTests.Page.al @@ -20,7 +20,6 @@ page 20401 "Qlty. Tests" CardPageId = "Qlty. Test Card"; Editable = false; InsertAllowed = false; - DeleteAllowed = false; RefreshOnActivate = true; PageType = List; SourceTable = "Qlty. Test"; @@ -219,6 +218,7 @@ page 20401 "Qlty. Tests" } } +#if not CLEAN29 actions { area(Processing) @@ -229,6 +229,10 @@ page 20401 "Qlty. Tests" Image = Delete; Scope = Repeater; ToolTip = 'Deletes this test. A test can only be deleted if it is not being used on an existing inspection.'; + Visible = false; + ObsoleteState = Pending; + ObsoleteReason = 'Deletion is handled by standard page behavior through the OnDelete trigger on Qlty. Test table.'; + ObsoleteTag = '29.0'; trigger OnAction() begin @@ -238,13 +242,20 @@ page 20401 "Qlty. Tests" end; } } + area(Promoted) { +#pragma warning disable AL0432 actionref(DeleteRecordSafe_Promoted; DeleteRecordSafe) +#pragma warning restore AL0432 { + ObsoleteState = Pending; + ObsoleteReason = 'Deletion is handled by standard page behavior through the OnDelete trigger on Qlty. Test table.'; + ObsoleteTag = '29.0'; } } } +#endif var QltyResultConditionMgmt: Codeunit "Qlty. Result Condition Mgmt."; @@ -270,11 +281,6 @@ page 20401 "Qlty. Tests" begin end; - trigger OnDeleteRecord(): Boolean - begin - Rec.CheckDeleteConstraints(true); - end; - trigger OnAfterGetRecord() begin UpdateRowData(); diff --git a/src/Apps/W1/Quality Management/app/src/Dispositions/InventoryAdjustment/QltyCreateNegativeAdjmt.Report.al b/src/Apps/W1/Quality Management/app/src/Dispositions/InventoryAdjustment/QltyCreateNegativeAdjmt.Report.al index 50646cc301..76df939988 100644 --- a/src/Apps/W1/Quality Management/app/src/Dispositions/InventoryAdjustment/QltyCreateNegativeAdjmt.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Dispositions/InventoryAdjustment/QltyCreateNegativeAdjmt.Report.al @@ -13,12 +13,13 @@ using Microsoft.Warehouse.Structure; report 20408 "Qlty. Create Negative Adjmt." { Caption = 'Quality Management - Create Negative Inventory Adjustment'; - ApplicationArea = QualityManagement; AdditionalSearchTerms = 'write-off, dispose'; + ToolTip = 'Use this report to decrease inventory quantity, such as when disposing of samples after destructive testing or writing off stock due to damage or spoilage'; ProcessingOnly = true; + AccessByPermission = tabledata "Qlty. Inspection Header" = R; UsageCategory = Tasks; + ApplicationArea = QualityManagement; AllowScheduling = false; - ToolTip = 'Use this to decrease inventory quantity, such as when disposing of samples after destructive testing or writing off stock due to damage or spoilage'; dataset { diff --git a/src/Apps/W1/Quality Management/app/src/Dispositions/ItemTracking/QltyChangeItemTracking.Report.al b/src/Apps/W1/Quality Management/app/src/Dispositions/ItemTracking/QltyChangeItemTracking.Report.al index a062c3f9c2..53d947ef6d 100644 --- a/src/Apps/W1/Quality Management/app/src/Dispositions/ItemTracking/QltyChangeItemTracking.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Dispositions/ItemTracking/QltyChangeItemTracking.Report.al @@ -15,13 +15,14 @@ using Microsoft.Warehouse.Structure; report 20409 "Qlty. Change Item Tracking" { Caption = 'Quality Management - Change Item Tracking'; - ApplicationArea = QualityManagement; + AdditionalSearchTerms = 'Change lot number, Change serial number, Change package number, Change Expiration Date'; + ToolTip = 'Use this to update item tracking information.'; ProcessingOnly = true; + AccessByPermission = tabledata "Qlty. Inspection Header" = R; UsageCategory = Tasks; + ApplicationArea = QualityManagement; AllowScheduling = false; Extensible = true; - AdditionalSearchTerms = 'Change lot number, Change serial number, Change package number, Change Expiration Date'; - ToolTip = 'Use this to update item tracking information.'; dataset { diff --git a/src/Apps/W1/Quality Management/app/src/Dispositions/Move/QltyMoveInventory.Report.al b/src/Apps/W1/Quality Management/app/src/Dispositions/Move/QltyMoveInventory.Report.al index 9f3b0578ab..027155ef26 100644 --- a/src/Apps/W1/Quality Management/app/src/Dispositions/Move/QltyMoveInventory.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Dispositions/Move/QltyMoveInventory.Report.al @@ -14,8 +14,9 @@ report 20404 "Qlty. Move Inventory" Caption = 'Quality Management - Move Inventory'; AdditionalSearchTerms = 'Quarantine'; ProcessingOnly = true; - ApplicationArea = QualityManagement; + AccessByPermission = tabledata "Qlty. Inspection Header" = R; UsageCategory = Tasks; + ApplicationArea = QualityManagement; AllowScheduling = false; dataset diff --git a/src/Apps/W1/Quality Management/app/src/Dispositions/Purchase/QltyCreatePurchaseReturn.Report.al b/src/Apps/W1/Quality Management/app/src/Dispositions/Purchase/QltyCreatePurchaseReturn.Report.al index 2aaa35c948..eeed95c71f 100644 --- a/src/Apps/W1/Quality Management/app/src/Dispositions/Purchase/QltyCreatePurchaseReturn.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Dispositions/Purchase/QltyCreatePurchaseReturn.Report.al @@ -12,12 +12,13 @@ using Microsoft.Warehouse.Structure; report 20411 "Qlty. Create Purchase Return" { - ApplicationArea = PurchReturnOrder; Caption = 'Quality Management - Create Purchase Return Order'; - UsageCategory = Tasks; + ToolTip = 'Use this to create a Purchase Return Order from a Quality Inspection.'; ProcessingOnly = true; + AccessByPermission = tabledata "Qlty. Inspection Header" = R; + UsageCategory = Tasks; + ApplicationArea = PurchReturnOrder; AllowScheduling = false; - ToolTip = 'Use this to create a Purchase Return Order from a Quality Inspection.'; dataset { diff --git a/src/Apps/W1/Quality Management/app/src/Dispositions/PutAway/QltyCreateInternalPutaway.Report.al b/src/Apps/W1/Quality Management/app/src/Dispositions/PutAway/QltyCreateInternalPutaway.Report.al index 291f881cf6..7eb6b5cdf9 100644 --- a/src/Apps/W1/Quality Management/app/src/Dispositions/PutAway/QltyCreateInternalPutaway.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Dispositions/PutAway/QltyCreateInternalPutaway.Report.al @@ -13,8 +13,9 @@ report 20406 "Qlty. Create Internal Put-away" { Caption = 'Quality Management - Create Internal Put-away'; ProcessingOnly = true; - ApplicationArea = Warehouse; + AccessByPermission = tabledata "Qlty. Inspection Header" = R; UsageCategory = Tasks; + ApplicationArea = Warehouse; AllowScheduling = false; dataset diff --git a/src/Apps/W1/Quality Management/app/src/Dispositions/Transfer/QltyCreateTransferOrder.Report.al b/src/Apps/W1/Quality Management/app/src/Dispositions/Transfer/QltyCreateTransferOrder.Report.al index 2e0b4f3f04..b9728e9da1 100644 --- a/src/Apps/W1/Quality Management/app/src/Dispositions/Transfer/QltyCreateTransferOrder.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Dispositions/Transfer/QltyCreateTransferOrder.Report.al @@ -12,11 +12,12 @@ using Microsoft.Warehouse.Structure; report 20410 "Qlty. Create Transfer Order" { Caption = 'Quality Management - Create Transfer Order'; - ApplicationArea = QualityManagement; + ToolTip = 'Use this to transfer items to another location.'; ProcessingOnly = true; + AccessByPermission = tabledata "Qlty. Inspection Header" = R; UsageCategory = Tasks; + ApplicationArea = QualityManagement; AllowScheduling = false; - ToolTip = 'Use this to transfer items to another location.'; dataset { diff --git a/src/Apps/W1/Quality Management/app/src/Document/QltyCreateInspection.Report.al b/src/Apps/W1/Quality Management/app/src/Document/QltyCreateInspection.Report.al index b63439ced1..29d1fff784 100644 --- a/src/Apps/W1/Quality Management/app/src/Document/QltyCreateInspection.Report.al +++ b/src/Apps/W1/Quality Management/app/src/Document/QltyCreateInspection.Report.al @@ -15,8 +15,9 @@ report 20400 "Qlty. Create Inspection" { Caption = 'Create Quality Inspection'; ProcessingOnly = true; + AccessByPermission = tabledata "Qlty. Inspection Header" = R; UsageCategory = ReportsAndAnalysis; - ApplicationArea = All; + ApplicationArea = QualityManagement; Permissions = tabledata "Qlty. Inspection Header" = Rim, tabledata "Qlty. Inspection Line" = Rim; diff --git a/src/Apps/W1/Quality Management/app/src/Document/QltyInspection.Page.al b/src/Apps/W1/Quality Management/app/src/Document/QltyInspection.Page.al index 56b6f3adc9..4640f2b797 100644 --- a/src/Apps/W1/Quality Management/app/src/Document/QltyInspection.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Document/QltyInspection.Page.al @@ -102,6 +102,7 @@ page 20406 "Qlty. Inspection" field(Status; Rec.Status) { Editable = false; + StyleExpr = StatusStyleExpr; } field("Finished Date"; Rec."Finished Date") { @@ -115,9 +116,11 @@ page 20406 "Qlty. Inspection" field("Result Code"; Rec."Result Code") { Importance = Additional; + StyleExpr = ResultStyleExpr; } field("Result Description"; Rec."Result Description") { + StyleExpr = ResultStyleExpr; } field("Evaluation Sequence"; Rec."Evaluation Sequence") { @@ -475,9 +478,15 @@ page 20406 "Qlty. Inspection" ToolTip = 'Create a new re-inspection based on this inspection. If the inspection is still open, it will be finished first. Finishing may be blocked if the current result does not allow it.'; trigger OnAction() + var + CreatedReinspectionHeader: Record "Qlty. Inspection Header"; begin - Rec.CreateReinspection(); + Rec.CreateReinspection(CreatedReinspectionHeader); CurrPage.Update(false); + if not IsNullGuid(CreatedReinspectionHeader.SystemId) then begin + Commit(); + Page.Run(Page::"Qlty. Inspection", CreatedReinspectionHeader); + end; end; } action(ChangeStatusFinish) @@ -646,7 +655,7 @@ page 20406 "Qlty. Inspection" PromotedCategory = Report; Caption = 'Non Conformance Report'; ToolTip = 'Specifies the Non Conformance Report has a layout suitable for quality inspection templates that typically contain Non Conformance Report questions.'; - Image = PrintReport; + Image = Report; Promoted = true; PromotedIsBig = true; PromotedOnly = true; @@ -666,7 +675,7 @@ page 20406 "Qlty. Inspection" PromotedCategory = Report; Caption = 'Inspection Report'; ToolTip = 'General purpose inspection report.'; - Image = PrintReport; + Image = Report; Promoted = true; PromotedIsBig = true; PromotedOnly = true; @@ -732,7 +741,7 @@ page 20406 "Qlty. Inspection" { Caption = 'Item Availability by'; Image = ItemAvailability; - action(tItemAvailabilityByEvent) + action(ItemAvailabilityByEvent) { ApplicationArea = Suite; Caption = 'Event'; @@ -748,7 +757,7 @@ page 20406 "Qlty. Inspection" AvailItemAvailabilityFormsMgt.ShowItemAvailabilityFromItem(Item, "Item Availability Type"::"Event"); end; } - action(Period) + action(ItemAvailabilityByPeriod) { ApplicationArea = Suite; Caption = 'Period'; @@ -759,7 +768,7 @@ page 20406 "Qlty. Inspection" "Variant Filter" = field("Source Variant Code"); ToolTip = 'Show the projected quantity of the item over time according to time periods, such as day, week, or month.'; } - action(Variant) + action(ItemAvailabilityByVariant) { ApplicationArea = Planning; Caption = 'Variant'; @@ -770,7 +779,7 @@ page 20406 "Qlty. Inspection" "Variant Filter" = field("Source Variant Code"); ToolTip = 'View the current and projected quantity of the item for each variant.'; } - action(Location) + action(ItemAvailabilityByLocation) { ApplicationArea = Suite; Caption = 'Location'; @@ -781,7 +790,7 @@ page 20406 "Qlty. Inspection" "Variant Filter" = field("Source Variant Code"); ToolTip = 'View the actual and projected quantity of the item per location.'; } - action(Lot) + action(ItemAvailabilityByLot) { ApplicationArea = ItemTracking; Caption = 'Lot'; @@ -790,7 +799,7 @@ page 20406 "Qlty. Inspection" RunPageLink = "No." = field("Source Item No."); ToolTip = 'View the current and projected quantity of the item for each lot.'; } - action(BinContents) + action(ItemAvailabilityByBinContents) { ApplicationArea = Warehouse; Caption = 'Bin Contents'; @@ -801,7 +810,7 @@ page 20406 "Qlty. Inspection" ToolTip = 'View the quantities of the item in each bin where it exists. You can see all the important parameters relating to bin content, and you can modify certain bin content parameters in this window.'; } } - action(tShowTransfers) + action(ShowRelatedTransferDocuments) { Caption = 'Show Related Transfer Documents'; Image = TransferOrder; @@ -820,27 +829,17 @@ page 20406 "Qlty. Inspection" QltyPermissionMgmt: Codeunit "Qlty. Permission Mgmt."; QltyMiscHelpers: Codeunit "Qlty. Misc Helpers"; Camera: Codeunit Camera; + ResultStyleExpr: Text; CameraAvailable: Boolean; IsOpen: Boolean; CanReopen: Boolean; CanFinish: Boolean; CanChangeLotTracking, CanChangeSerialTracking, CanChangePackageTracking : Boolean; - VisibleCustom10: Boolean; - VisibleCustom9: Boolean; - VisibleCustom8: Boolean; - VisibleCustom7: Boolean; - VisibleCustom6: Boolean; - VisibleCustom5: Boolean; - VisibleCustom4: Boolean; - VisibleCustom3: Boolean; - VisibleCustom2: Boolean; - VisibleCustom1: Boolean; - VisibleDocumentNo: Boolean; - VisibleDocumentLineNo: Boolean; - VisibleSourceTaskNo: Boolean; - VisibleSourceSubType: Boolean; - VisibleSourceType: Boolean; + VisibleCustom1, VisibleCustom2, VisibleCustom3, VisibleCustom4, VisibleCustom5, VisibleCustom6, VisibleCustom7, VisibleCustom8, VisibleCustom9, VisibleCustom10 : Boolean; + VisibleDocumentNo, VisibleDocumentLineNo : Boolean; + VisibleSourceTaskNo, VisibleSourceType, VisibleSourceSubType : Boolean; CanChangeQuantity: Boolean; + StatusStyleExpr: Text; trigger OnOpenPage() begin @@ -850,6 +849,8 @@ page 20406 "Qlty. Inspection" trigger OnAfterGetRecord() begin UpdateControlVisibilityStates(true); + + ResultStyleExpr := Rec.GetResultStyle(); end; trigger OnModifyRecord(): Boolean @@ -862,9 +863,11 @@ page 20406 "Qlty. Inspection" TempItemTrackingSetup: Record "Item Tracking Setup" temporary; begin IsOpen := Rec.Status = Rec.Status::Open; - CanReopen := not Rec.HasMoreRecentReinspection(); + StatusStyleExpr := Rec.GetStatusStyleExpression(); + + CanReopen := (Rec.Status <> Rec.Status::Open) and not Rec.HasMoreRecentReinspection(); CanFinish := Rec.Status <> Rec.Status::Finished; - if Rec.Status = Rec.Status::Open then + if IsOpen then if QltyPermissionMgmt.CanChangeItemTracking() then begin TempItemTrackingSetup."Lot No. Required" := true; TempItemTrackingSetup."Serial No. Required" := true; diff --git a/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionCreate.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionCreate.Codeunit.al index d500bc2660..17b0f93181 100644 --- a/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionCreate.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionCreate.Codeunit.al @@ -709,7 +709,6 @@ codeunit 20404 "Qlty. Inspection - Create" internal procedure CreateReinspection(FromThisQltyInspectionHeader: Record "Qlty. Inspection Header"; var CreatedReinspectionQltyInspectionHeader: Record "Qlty. Inspection Header") var PrecedingQltyInspectionHeader: Record "Qlty. Inspection Header"; - QltyNotificationMgmt: Codeunit "Qlty. Notification Mgmt."; IsHandled: Boolean; begin QltyManagementSetup.Get(); @@ -733,9 +732,6 @@ codeunit 20404 "Qlty. Inspection - Create" LastCreatedQltyInspectionHeader := CreatedReinspectionQltyInspectionHeader; - if GuiAllowed() then - QltyNotificationMgmt.NotifyInspectionCreated(CreatedReinspectionQltyInspectionHeader); - OnAfterCreateReinspection(FromThisQltyInspectionHeader, CreatedReinspectionQltyInspectionHeader); end; @@ -853,8 +849,11 @@ codeunit 20404 "Qlty. Inspection - Create" QltyNotificationMgmt: Codeunit "Qlty. Notification Mgmt."; InspectionNo: Code[20]; PipeSeparatedFilter: Text; + FilterExceedsMaxLength: Boolean; + MaxSafeFilterLength: Integer; begin QltyManagementSetup.Get(); + MaxSafeFilterLength := 1024; if GuiAllowed() then begin foreach InspectionNo in CreatedQltyInspectionIds do @@ -862,8 +861,17 @@ codeunit 20404 "Qlty. Inspection - Create" if StrLen(PipeSeparatedFilter) > 1 then PipeSeparatedFilter += '|'; PipeSeparatedFilter += InspectionNo; + if StrLen(PipeSeparatedFilter) > MaxSafeFilterLength then begin + FilterExceedsMaxLength := true; + break; + end; end; + if FilterExceedsMaxLength then begin + QltyNotificationMgmt.NotifyMultipleInspectionsCreatedByCount(CreatedQltyInspectionIds.Count()); + exit; + end; + CreatedQltyInspectionHeader.SetFilter("No.", PipeSeparatedFilter); if CreatedQltyInspectionIds.Count() = 1 then begin CreatedQltyInspectionHeader.SetCurrentKey("No.", "Re-inspection No."); diff --git a/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionHeader.Table.al b/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionHeader.Table.al index cb1b0a2320..fdeef325d3 100644 --- a/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionHeader.Table.al +++ b/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionHeader.Table.al @@ -34,7 +34,8 @@ table 20405 "Qlty. Inspection Header" DrillDownPageId = "Qlty. Inspection List"; LookupPageId = "Qlty. Inspection List"; DataClassification = CustomerContent; - Permissions = tabledata "Qlty. Inspection Line" = d; + Permissions = tabledata "Qlty. Inspection Line" = d, + tabledata "Qlty. I. Result Condit. Conf." = d; fields { @@ -722,7 +723,7 @@ table 20405 "Qlty. Inspection Header" end; /// - /// This will upresult the result on the test based on the results from the line. + /// This will update the result on the inspection based on the results from the line. /// procedure UpdateResultFromLines() var @@ -990,6 +991,16 @@ table 20405 "Qlty. Inspection Header" procedure CreateReinspection() var NewlyCreatedReQltyInspectionHeader: Record "Qlty. Inspection Header"; + begin + CreateReinspection(NewlyCreatedReQltyInspectionHeader); + end; + + /// + /// Creates a Re-inspection and returns the created record. + /// + /// The newly created re-inspection header. + procedure CreateReinspection(var CreatedReinspectionHeader: Record "Qlty. Inspection Header") + var QltyInspectionCreate: Codeunit "Qlty. Inspection - Create"; Proceed: Boolean; begin @@ -1006,7 +1017,7 @@ table 20405 "Qlty. Inspection Header" else Proceed := true; if Proceed then - QltyInspectionCreate.CreateReinspection(Rec, NewlyCreatedReQltyInspectionHeader); + QltyInspectionCreate.CreateReinspection(Rec, CreatedReinspectionHeader); end; /// @@ -1574,6 +1585,18 @@ table 20405 "Qlty. Inspection Header" exit((Rec."Re-inspection No." = 0) ? Rec."No." : StrSubstNo(InspectionLbl, Rec."No.", Rec."Re-inspection No.")); end; + procedure GetStatusStyleExpression(): Text + begin + case Rec.Status of + Rec.Status::Open: + exit('Favorable'); + Rec.Status::Finished: + exit('Strong'); + else + exit('None'); + end; + end; + local procedure VerifyPassAndFailQuantities() var DifferenceInPassFailQuantity: Decimal; @@ -1584,6 +1607,21 @@ table 20405 "Qlty. Inspection Header" end; end; + /// + /// Gets the preferred result style to use. + /// + internal procedure GetResultStyle(): Text + var + QltyInspectionResult: Record "Qlty. Inspection Result"; + begin + if Rec."Result Code" = '' then + exit(''); + + QltyInspectionResult.SetLoadFields("Override Style", "Result Category"); + if QltyInspectionResult.Get(Rec."Result Code") then + exit(QltyInspectionResult.GetResultStyle()); + end; + #region Most Recent Picture Management /// /// This will use the camera to take a picture and add it to the Inspection document. diff --git a/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionLine.Table.al b/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionLine.Table.al index 3b47134228..cef37cd9b6 100644 --- a/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionLine.Table.al +++ b/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionLine.Table.al @@ -22,6 +22,7 @@ table 20406 "Qlty. Inspection Line" LookupPageId = "Qlty. Inspection Lines"; DrillDownPageId = "Qlty. Inspection Lines"; DataClassification = CustomerContent; + Permissions = tabledata "Qlty. I. Result Condit. Conf." = d; fields { @@ -147,10 +148,10 @@ table 20406 "Qlty. Inspection Line" trigger OnValidate() var - QltyResult: Record "Qlty. Inspection Result"; + QltyInspectionResult: Record "Qlty. Inspection Result"; begin - if QltyResult.Get(Rec."Result Code") then begin - Rec."Evaluation Sequence" := QltyResult."Evaluation Sequence"; + if QltyInspectionResult.Get(Rec."Result Code") then begin + Rec."Evaluation Sequence" := QltyInspectionResult."Evaluation Sequence"; Rec.CalcFields("Result Description"); end; end; @@ -389,6 +390,10 @@ table 20406 "Qlty. Inspection Line" var QltyInspectionResult: Record "Qlty. Inspection Result"; begin + if Rec."Result Code" = '' then + exit(''); + + QltyInspectionResult.SetLoadFields("Override Style", "Result Category"); if QltyInspectionResult.Get(Rec."Result Code") then exit(QltyInspectionResult.GetResultStyle()); end; diff --git a/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionList.Page.al b/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionList.Page.al index 36789d010f..95bb3e7c60 100644 --- a/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionList.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Document/QltyInspectionList.Page.al @@ -32,6 +32,7 @@ page 20408 "Qlty. Inspection List" SourceTableView = sorting("No.", "Re-inspection No.") order(descending); UsageCategory = Lists; ApplicationArea = QualityManagement; + RefreshOnActivate = true; AboutTitle = 'About Quality Inspections'; AboutText = 'Review all quality inspections created by rules or manually. Track their progress through the inspection process and take action when needed.'; @@ -62,15 +63,18 @@ page 20408 "Qlty. Inspection List" { AboutTitle = 'Inspection status at a glance'; AboutText = '**Status** shows whether the inspection is still in progress or finished. Finished inspections are locked and can''t be changed.'; + StyleExpr = StatusStyleExpr; } field("Result Code"; Rec."Result Code") { Visible = false; + StyleExpr = ResultStyleExpr; } field("Result Description"; Rec."Result Description") { AboutTitle = 'Inspection results'; AboutText = '**Result** shows the outcome of the inspection. It''s automatically calculated from the test values on the lines and the result conditions in the quality test page.'; + StyleExpr = ResultStyleExpr; } field("Finished Date"; Rec."Finished Date") { @@ -190,7 +194,7 @@ page 20408 "Qlty. Inspection List" AccessByPermission = tabledata "Qlty. Inspection Header" = I; Caption = 'Create Inspection'; ToolTip = 'Specifies to create a new Quality Inspection.'; - Image = CreateForm; + Image = BulletList; Promoted = true; PromotedCategory = Process; PromotedIsBig = true; @@ -218,9 +222,15 @@ page 20408 "Qlty. Inspection List" Enabled = CanCreateReinspection; trigger OnAction() + var + CreatedReinspectionHeader: Record "Qlty. Inspection Header"; begin - Rec.CreateReinspection(); + Rec.CreateReinspection(CreatedReinspectionHeader); CurrPage.Update(false); + if not IsNullGuid(CreatedReinspectionHeader.SystemId) then begin + Commit(); + Page.Run(Page::"Qlty. Inspection", CreatedReinspectionHeader); + end; end; } action(TakePicture) @@ -460,7 +470,7 @@ page 20408 "Qlty. Inspection List" Caption = 'Non Conformance Report'; Enabled = RowActionsAreEnabled; ToolTip = 'Specifies the Non Conformance Report has a layout suitable for quality inspection templates that typically contain Non Conformance Report questions.'; - Image = PrintReport; + Image = Report; Promoted = true; PromotedIsBig = true; PromotedOnly = true; @@ -481,7 +491,7 @@ page 20408 "Qlty. Inspection List" Caption = 'Inspection Report'; Enabled = RowActionsAreEnabled; ToolTip = 'General purpose inspection report.'; - Image = PrintReport; + Image = Report; Promoted = true; PromotedIsBig = true; PromotedOnly = true; @@ -551,7 +561,7 @@ page 20408 "Qlty. Inspection List" Caption = 'Item Availability by'; Enabled = RowActionsAreEnabled; Image = ItemAvailability; - action(tItemAvailabilityByEvent) + action(ItemAvailabilityByEvent) { ApplicationArea = Suite; Caption = 'Event'; @@ -568,7 +578,7 @@ page 20408 "Qlty. Inspection List" AvailItemAvailabilityFormsMgt.ShowItemAvailabilityFromItem(Item, "Item Availability Type"::"Event"); end; } - action(Period) + action(ItemAvailabilityByPeriod) { ApplicationArea = Suite; Caption = 'Period'; @@ -580,7 +590,7 @@ page 20408 "Qlty. Inspection List" "Variant Filter" = field("Source Variant Code"); ToolTip = 'Show the projected quantity of the item over time according to time periods, such as day, week, or month.'; } - action(Variant) + action(ItemAvailabilityByVariant) { ApplicationArea = Planning; Caption = 'Variant'; @@ -592,7 +602,7 @@ page 20408 "Qlty. Inspection List" "Variant Filter" = field("Source Variant Code"); ToolTip = 'View the current and projected quantity of the item for each variant.'; } - action(Location) + action(ItemAvailabilityByLocation) { ApplicationArea = Suite; Caption = 'Location'; @@ -604,7 +614,7 @@ page 20408 "Qlty. Inspection List" "Variant Filter" = field("Source Variant Code"); ToolTip = 'View the actual and projected quantity of the item per location.'; } - action(Lot) + action(ItemAvailabilityByLot) { ApplicationArea = ItemTracking; Caption = 'Lot'; @@ -614,7 +624,7 @@ page 20408 "Qlty. Inspection List" RunPageLink = "No." = field("Source Item No."); ToolTip = 'View the current and projected quantity of the item for each lot.'; } - action(BinContents) + action(ItemAvailabilityByBinContents) { ApplicationArea = Warehouse; Caption = 'Bin Contents'; @@ -689,19 +699,19 @@ page 20408 "Qlty. Inspection List" var QltyPermissionMgmt: Codeunit "Qlty. Permission Mgmt."; QltyMiscHelpers: Codeunit "Qlty. Misc Helpers"; + ResultStyleExpr: Text; CanAssignToSelf: Boolean; CanCreateReinspection: Boolean; CanUnassign: Boolean; CanFinish: Boolean; CanReopen: Boolean; RowActionsAreEnabled: Boolean; + StatusStyleExpr: Text; - trigger OnOpenPage() + trigger OnAfterGetRecord() begin - RowActionsAreEnabled := not IsNullGuid(Rec.SystemId); - CanReopen := RowActionsAreEnabled and not Rec.HasMoreRecentReinspection(); - CanFinish := RowActionsAreEnabled and (Rec.Status <> Rec.Status::Finished); - CanCreateReinspection := RowActionsAreEnabled; + ResultStyleExpr := Rec.GetResultStyle(); + StatusStyleExpr := Rec.GetStatusStyleExpression(); end; trigger OnAfterGetCurrRecord() @@ -709,8 +719,10 @@ page 20408 "Qlty. Inspection List" CanAssignToSelf := false; CanUnassign := false; RowActionsAreEnabled := not IsNullGuid(Rec.SystemId); - CanReopen := RowActionsAreEnabled and not Rec.HasMoreRecentReinspection(); + CanCreateReinspection := RowActionsAreEnabled; + CanReopen := RowActionsAreEnabled and (Rec.Status <> Rec.Status::Open) and not Rec.HasMoreRecentReinspection(); CanFinish := RowActionsAreEnabled and (Rec.Status <> Rec.Status::Finished); + StatusStyleExpr := Rec.GetStatusStyleExpression(); if (Rec."Assigned User ID" = '') or ((Rec."Assigned User ID" <> UserId()) and QltyPermissionMgmt.CanChangeOtherInspections()) then CanAssignToSelf := RowActionsAreEnabled; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Assembly/QltyAssemblyIntegration.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Integration/Assembly/QltyAssemblyIntegration.Codeunit.al index a5f042e93d..d0e09fcd50 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Assembly/QltyAssemblyIntegration.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Assembly/QltyAssemblyIntegration.Codeunit.al @@ -12,6 +12,7 @@ using Microsoft.Inventory.Tracking; using Microsoft.Projects.Resources.Journal; using Microsoft.QualityManagement.Configuration.GenerationRule; using Microsoft.QualityManagement.Document; +using Microsoft.QualityManagement.Utilities; using Microsoft.Warehouse.Journal; /// @@ -27,6 +28,7 @@ codeunit 20412 "Qlty. Assembly Integration" TempSpecTrackingSpecification: Record "Tracking Specification" temporary; TempQltyInspectionGenRule: Record "Qlty. Inspection Gen. Rule" temporary; QltyInspectionCreate: Codeunit "Qlty. Inspection - Create"; + QltyBatchNotifHelper: Codeunit "Qlty. Batch Notif. Helper"; MgtItemTrackingDocManagement: Codeunit "Item Tracking Doc. Management"; UnusedVariant1: Variant; UnusedVariant2: Variant; @@ -43,13 +45,19 @@ codeunit 20412 "Qlty. Assembly Integration" if IsHandled then exit; + QltyBatchNotifHelper.BeginBatch(); + QltyBatchNotifHelper.ConfigureForBatch(QltyInspectionCreate); if not TempSpecTrackingSpecification.IsEmpty() then repeat + Clear(QltyInspectionHeader); HasInspection := QltyInspectionCreate.CreateInspectionWithMultiVariants(PostedAssemblyHeader, TempSpecTrackingSpecification, AssemblyHeader, UnusedVariant1, false, QltyInspectionGenRule); if HasInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); - QltyInspectionHeader."Source Quantity (Base)" := TempSpecTrackingSpecification."Quantity (Base)"; - QltyInspectionHeader.Modify(false); + if QltyInspectionHeader."No." <> '' then begin + QltyInspectionHeader."Source Quantity (Base)" := TempSpecTrackingSpecification."Quantity (Base)"; + QltyInspectionHeader.Modify(false); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); + end; end; OnAfterAttemptCreateInspectionFromPostedAssembly(AssemblyHeader, PostedAssemblyHeader, TempSpecTrackingSpecification, QltyInspectionHeader); until TempSpecTrackingSpecification.Next(-1) = 0 @@ -59,10 +67,13 @@ codeunit 20412 "Qlty. Assembly Integration" if IsHandled then exit; HasInspection := QltyInspectionCreate.CreateInspectionWithMultiVariants(PostedAssemblyHeader, AssemblyHeader, UnusedVariant1, UnusedVariant2, false, TempQltyInspectionGenRule); - if HasInspection then + if HasInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); + end; OnAfterAttemptCreateInspectionFromPostedAssembly(AssemblyHeader, PostedAssemblyHeader, TempSpecTrackingSpecification, QltyInspectionHeader); end; + QltyBatchNotifHelper.EndBatch(); end; /// diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemCard.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemCard.PageExt.al index ddcb5f1c68..9968831c50 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemCard.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemCard.PageExt.al @@ -18,7 +18,7 @@ pageextension 20430 "Qlty. Item Card" extends "Item Card" ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; Caption = 'Quality Inspections'; - Image = TaskQualityMeasure; + Image = CheckList; ToolTip = 'View quality inspections filtered by the selected item.'; RunObject = Page "Qlty. Inspection List"; RunPageLink = "Source Item No." = field("No."); diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemList.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemList.PageExt.al index c995f9c26a..f20c239e4f 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemList.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemList.PageExt.al @@ -18,7 +18,7 @@ pageextension 20431 "Qlty. Item List" extends "Item List" ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; Caption = 'Quality Inspections'; - Image = TaskQualityMeasure; + Image = CheckList; ToolTip = 'View quality inspections filtered by the selected item.'; RunObject = Page "Qlty. Inspection List"; RunPageLink = "Source Item No." = field("No."); diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemVariantCard.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemVariantCard.PageExt.al index 1115523a83..0f6093633d 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemVariantCard.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemVariantCard.PageExt.al @@ -18,7 +18,7 @@ pageextension 20433 "Qlty. Item Variant Card" extends "Item Variant Card" ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; Caption = 'Quality Inspections'; - Image = TaskQualityMeasure; + Image = CheckList; ToolTip = 'View quality inspections filtered by the selected item and variant.'; RunObject = Page "Qlty. Inspection List"; RunPageLink = "Source Item No." = field("Item No."), diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemVariants.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemVariants.PageExt.al index db6ff7ac56..8e06720204 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemVariants.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Item/QltyItemVariants.PageExt.al @@ -18,7 +18,7 @@ pageextension 20432 "Qlty. Item Variants" extends "Item Variants" ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; Caption = 'Quality Inspections'; - Image = TaskQualityMeasure; + Image = CheckList; ToolTip = 'View quality inspections filtered by the selected item and variant.'; RunObject = Page "Qlty. Inspection List"; RunPageLink = "Source Item No." = field("Item No."), diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTracing.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTracing.PageExt.al index 22e75df47f..509977872a 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTracing.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTracing.PageExt.al @@ -18,7 +18,7 @@ pageextension 20428 "Qlty. Item Tracing" extends "Item Tracing" ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; Caption = 'Quality Inspections'; - Image = TaskQualityMeasure; + Image = CheckList; ToolTip = 'View quality inspections filtered by the selected item, variant, location, and tracking details.'; RunObject = Page "Qlty. Inspection List"; RunPageLink = "Source Item No." = field("Item No."), diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTrackingEntries.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTrackingEntries.PageExt.al index 32cafe424c..eb673dc29d 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTrackingEntries.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTrackingEntries.PageExt.al @@ -18,7 +18,7 @@ pageextension 20429 "Qlty. Item Tracking Entries" extends "Item Tracking Entries ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; Caption = 'Quality Inspections'; - Image = TaskQualityMeasure; + Image = CheckList; ToolTip = 'View quality inspections filtered by the selected item, variant, location, and tracking details.'; RunObject = Page "Qlty. Inspection List"; RunPageLink = "Source Item No." = field("Item No."), diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTrackingLines.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTrackingLines.PageExt.al index a35d0343b5..f33ec742e0 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTrackingLines.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Inventory/Tracking/QltyItemTrackingLines.PageExt.al @@ -21,7 +21,7 @@ pageextension 20418 "Qlty. Item Tracking Lines" extends "Item Tracking Lines" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = I; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspections'; ToolTip = 'Creates multiple quality inspections for the selected item tracking lines.'; AboutTitle = 'Create Quality Inspections for selected lines'; @@ -40,7 +40,7 @@ pageextension 20418 "Qlty. Item Tracking Lines" extends "Item Tracking Lines" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item with tracking specification'; ToolTip = 'Shows Quality Inspections for Item with tracking specification'; AboutTitle = 'Show Quality Inspections'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Document/QltyProdOrderRouting.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Document/QltyProdOrderRouting.PageExt.al index 38e7267d6b..aec60b34ef 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Document/QltyProdOrderRouting.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Document/QltyProdOrderRouting.PageExt.al @@ -20,7 +20,7 @@ pageextension 20400 "Qlty. Prod. Order Routing" extends "Prod. Order Routing" action(Qlty_CreateQualityInspection) { ApplicationArea = QualityManagement; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspection'; ToolTip = 'Specifies to create a new quality inspection.'; @@ -36,7 +36,7 @@ pageextension 20400 "Qlty. Prod. Order Routing" extends "Prod. Order Routing" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections'; ToolTip = 'Shows existing Quality Inspections.'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Journal/QltyConsumptionJournal.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Journal/QltyConsumptionJournal.PageExt.al index 79ea00ec52..3ff13f9501 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Journal/QltyConsumptionJournal.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Journal/QltyConsumptionJournal.PageExt.al @@ -21,7 +21,7 @@ pageextension 20408 "Qlty. Consumption Journal" extends "Consumption Journal" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = I; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspection'; ToolTip = 'Creates a quality inspection for this consumption journal line.'; AboutTitle = 'Create Quality Inspection'; @@ -39,7 +39,7 @@ pageextension 20408 "Qlty. Consumption Journal" extends "Consumption Journal" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item and Document'; ToolTip = 'Shows quality inspections for this item and document.'; AboutTitle = 'Show Quality Inspections'; @@ -57,7 +57,7 @@ pageextension 20408 "Qlty. Consumption Journal" extends "Consumption Journal" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item'; ToolTip = 'Shows Quality Inspections for Item'; AboutTitle = 'Show Quality Inspections'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Journal/QltyOutputJournal.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Journal/QltyOutputJournal.PageExt.al index e3d42cb9b0..27520485fc 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Journal/QltyOutputJournal.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Journal/QltyOutputJournal.PageExt.al @@ -21,7 +21,7 @@ pageextension 20401 "Qlty. Output Journal" extends "Output Journal" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = I; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspection'; ToolTip = 'Creates a quality inspection for this output journal line.'; AboutTitle = 'Create Quality Inspection'; @@ -39,7 +39,7 @@ pageextension 20401 "Qlty. Output Journal" extends "Output Journal" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item and Document'; ToolTip = 'Shows quality inspections for this item and document.'; AboutTitle = 'Show Quality Inspections'; @@ -57,7 +57,7 @@ pageextension 20401 "Qlty. Output Journal" extends "Output Journal" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item'; ToolTip = 'Shows Quality Inspections for Item'; AboutTitle = 'Show Quality Inspections'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/QltyManufacturIntegration.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/QltyManufacturIntegration.Codeunit.al index a5ba82153e..fbd26b1822 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/QltyManufacturIntegration.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/QltyManufacturIntegration.Codeunit.al @@ -13,6 +13,7 @@ using Microsoft.QualityManagement.Configuration.GenerationRule; using Microsoft.QualityManagement.Configuration.SourceConfiguration; using Microsoft.QualityManagement.Document; using Microsoft.QualityManagement.Setup; +using Microsoft.QualityManagement.Utilities; /// /// Used to integrate with manufacturing related events. @@ -347,6 +348,7 @@ codeunit 20407 "Qlty. Manufactur. Integration" TempTrackingSpecification: Record "Tracking Specification" temporary; QltyInspectionCreate: Codeunit "Qlty. Inspection - Create"; ProdOrderLineReserve: Codeunit "Prod. Order Line-Reserve"; + QltyBatchNotifHelper: Codeunit "Qlty. Batch Notif. Helper"; ListOfInspectionIds: List of [RecordId]; HasReservationEntries: Boolean; IsHandled: Boolean; @@ -360,6 +362,8 @@ codeunit 20407 "Qlty. Manufactur. Integration" if IsHandled then exit; + QltyBatchNotifHelper.BeginBatch(); + QltyBatchNotifHelper.ConfigureForBatch(QltyInspectionCreate); ProdOrderLine.SetRange(Status, ProductionOrder.Status); ProdOrderLine.SetRange("Prod. Order No.", ProductionOrder."No."); if ProdOrderLine.FindSet() then begin @@ -386,6 +390,7 @@ codeunit 20407 "Qlty. Manufactur. Integration" if MadeInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); ListOfInspectionIds.Add(QltyInspectionHeader.RecordId()); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); CreatedAtLeastOneInspectionForRoutingLine := true; end; until ReservationEntry.Next() = 0; @@ -395,6 +400,7 @@ codeunit 20407 "Qlty. Manufactur. Integration" if MadeInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); ListOfInspectionIds.Add(QltyInspectionHeader.RecordId()); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); CreatedAtLeastOneInspectionForRoutingLine := true; end; end; @@ -415,6 +421,7 @@ codeunit 20407 "Qlty. Manufactur. Integration" if MadeInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); ListOfInspectionIds.Add(QltyInspectionHeader.RecordId()); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); CreatedAtLeastOneInspectionForOrderLine := true; end; @@ -424,6 +431,7 @@ codeunit 20407 "Qlty. Manufactur. Integration" if MadeInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); ListOfInspectionIds.Add(QltyInspectionHeader.RecordId()); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); CreatedAtLeastOneInspectionForOrderLine := true; end; end; @@ -434,9 +442,11 @@ codeunit 20407 "Qlty. Manufactur. Integration" if MadeInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); ListOfInspectionIds.Add(QltyInspectionHeader.RecordId()); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); CreatedInspectionForProdOrder := MadeInspection; end; end; + QltyBatchNotifHelper.EndBatch(); OnAfterProductionAttemptCreateReleaseAutomaticInspection(ProductionOrder, CreatedAtLeastOneInspectionForRoutingLine, CreatedAtLeastOneInspectionForOrderLine, CreatedInspectionForProdOrder, ListOfInspectionIds); end; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Routing/QltyRoutingLineLookup.Page.al b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Routing/QltyRoutingLineLookup.Page.al index fa40461ea6..f2991ac26c 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Routing/QltyRoutingLineLookup.Page.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Manufacturing/Routing/QltyRoutingLineLookup.Page.al @@ -1,3 +1,4 @@ +#if not CLEAN28 // ------------------------------------------------------------------------------------------------ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. See License.txt in the project root for license information. @@ -13,6 +14,9 @@ page 20463 "Qlty. Routing Line Lookup" SourceTable = "Routing Line"; UsageCategory = None; ApplicationArea = Manufacturing; + ObsoleteReason = 'Unused and replaced with "Routing Line List" page.'; + ObsoleteState = Pending; + ObsoleteTag = '28.0'; layout { @@ -20,7 +24,6 @@ page 20463 "Qlty. Routing Line Lookup" { repeater(General) { -#pragma warning disable AA0218 field("Operation No."; Rec."Operation No.") { } @@ -119,5 +122,5 @@ page 20463 "Qlty. Routing Line Lookup" } } } -#pragma warning restore AA0218 } +#endif \ No newline at end of file diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Purchases/Document/QltyPurchRetOrderSubf.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Purchases/Document/QltyPurchRetOrderSubf.PageExt.al index f6299ce1cc..ff0f1d623f 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Purchases/Document/QltyPurchRetOrderSubf.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Purchases/Document/QltyPurchRetOrderSubf.PageExt.al @@ -21,7 +21,7 @@ pageextension 20407 "Qlty. Purch. Ret. Order Subf." extends "Purchase Return Ord { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = I; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspection'; ToolTip = 'Creates a quality inspection for this purchase return order line.'; AboutTitle = 'Create Quality Inspection'; @@ -39,7 +39,7 @@ pageextension 20407 "Qlty. Purch. Ret. Order Subf." extends "Purchase Return Ord { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item and Document'; ToolTip = 'Shows quality inspections for this item and document.'; AboutTitle = 'Show Quality Inspections'; @@ -57,7 +57,7 @@ pageextension 20407 "Qlty. Purch. Ret. Order Subf." extends "Purchase Return Ord { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item'; ToolTip = 'Shows Quality Inspections for Item'; AboutTitle = 'Show Quality Inspections'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Purchases/Document/QltyPurchaseOrderSubform.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Purchases/Document/QltyPurchaseOrderSubform.PageExt.al index 57c27ad84c..8b18f21b44 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Purchases/Document/QltyPurchaseOrderSubform.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Purchases/Document/QltyPurchaseOrderSubform.PageExt.al @@ -21,7 +21,7 @@ pageextension 20402 "Qlty. Purchase Order Subform" extends "Purchase Order Subfo { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = I; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspection'; ToolTip = 'Creates a quality inspection for this purchase order line.'; AboutTitle = 'Create Quality Inspection'; @@ -39,7 +39,7 @@ pageextension 20402 "Qlty. Purchase Order Subform" extends "Purchase Order Subfo { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item and Document'; ToolTip = 'Shows quality inspections for this item and document.'; AboutTitle = 'Show Quality Inspections'; @@ -57,7 +57,7 @@ pageextension 20402 "Qlty. Purchase Order Subform" extends "Purchase Order Subfo { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item'; ToolTip = 'Shows Quality Inspections for Item'; AboutTitle = 'Show Quality Inspections'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Receiving/QltyReceivingIntegration.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Integration/Receiving/QltyReceivingIntegration.Codeunit.al index 6442e315e8..4fb106bf37 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Receiving/QltyReceivingIntegration.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Receiving/QltyReceivingIntegration.Codeunit.al @@ -16,6 +16,7 @@ using Microsoft.QualityManagement.Configuration.GenerationRule; using Microsoft.QualityManagement.Document; using Microsoft.QualityManagement.Integration.Warehouse; using Microsoft.QualityManagement.Setup; +using Microsoft.QualityManagement.Utilities; using Microsoft.Sales.Document; using Microsoft.Sales.History; using Microsoft.Sales.Posting; @@ -28,6 +29,7 @@ codeunit 20411 "Qlty. Receiving Integration" var QltyManagementSetup: Record "Qlty. Management Setup"; ApplicableReceivingQltyInspectionGenRule: Record "Qlty. Inspection Gen. Rule"; + QltyBatchNotifHelper: Codeunit "Qlty. Batch Notif. Helper"; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purch.-Post", 'OnAfterPurchRcptLineInsert', '', true, true)] local procedure HandleOnAfterPurchRcptLineInsert(PurchaseLine: Record "Purchase Line"; var PurchRcptLine: Record "Purch. Rcpt. Line"; ItemLedgShptEntryNo: Integer; WhseShip: Boolean; WhseReceive: Boolean; CommitIsSupressed: Boolean; PurchInvHeader: Record "Purch. Inv. Header"; var TempTrackingSpecification: Record "Tracking Specification" temporary; PurchRcptHeader: Record "Purch. Rcpt. Header"; TempWhseRcptHeader: Record "Warehouse Receipt Header"; xPurchLine: Record "Purchase Line"; var TempPurchLineGlobal: Record "Purchase Line" temporary) @@ -59,6 +61,7 @@ codeunit 20411 "Qlty. Receiving Integration" TempTrackingSpecification.SetRange("Source Ref. No.", PurchaseLine."Line No."); TempTrackingSpecification.SetRange("Source Type", Database::"Purchase Line"); + QltyBatchNotifHelper.BeginBatch(); ExpectedCountOfInspections := TempTrackingSpecification.Count(); if ExpectedCountOfInspections = 0 then begin ExpectedCountOfInspections := 1; @@ -77,6 +80,7 @@ codeunit 20411 "Qlty. Receiving Integration" TempSingleBufferTrackingSpecification.SetRecFilter(); AttemptCreateInspectionWithPurchaseLineAndTracking(PurchaseLine, PurchaseHeader, TempTrackingSpecification); until TempTrackingSpecification.Next() = 0; + QltyBatchNotifHelper.EndBatch(); TempTrackingSpecification.SetRange("Qty. to Invoice (Base)"); TempTrackingSpecification.SetRange("Source ID"); @@ -98,8 +102,11 @@ codeunit 20411 "Qlty. Receiving Integration" ApplicableReceivingQltyInspectionGenRule.Reset(); ApplicableReceivingQltyInspectionGenRule.SetRange("Warehouse Receipt Trigger", ApplicableReceivingQltyInspectionGenRule."Warehouse Receipt Trigger"::OnWarehouseReceiptPost); ApplicableReceivingQltyInspectionGenRule.SetFilter("Activation Trigger", '%1|%2', ApplicableReceivingQltyInspectionGenRule."Activation Trigger"::"Manual or Automatic", ApplicableReceivingQltyInspectionGenRule."Activation Trigger"::"Automatic only"); - if not ApplicableReceivingQltyInspectionGenRule.IsEmpty() then + if not ApplicableReceivingQltyInspectionGenRule.IsEmpty() then begin + QltyBatchNotifHelper.BeginBatch(); AttemptCreateInspectionWithWhseJournalLine(WarehouseJournalLine, PostedWhseReceiptHeader); + QltyBatchNotifHelper.EndBatch(); + end; end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Purchases Warehouse Mgt.", 'OnAfterCreateRcptLineFromPurchLine', '', true, true)] @@ -118,7 +125,9 @@ codeunit 20411 "Qlty. Receiving Integration" ApplicableReceivingQltyInspectionGenRule.SetFilter("Activation Trigger", '%1|%2', ApplicableReceivingQltyInspectionGenRule."Activation Trigger"::"Manual or Automatic", ApplicableReceivingQltyInspectionGenRule."Activation Trigger"::"Automatic only"); if not ApplicableReceivingQltyInspectionGenRule.IsEmpty() then begin OptionalSource := PurchaseLine; + QltyBatchNotifHelper.BeginBatch(); AttemptCreateInspectionWithReceiptLine(WarehouseReceiptLine, WarehouseReceiptHeader, OptionalSource); + QltyBatchNotifHelper.EndBatch(); end; end; @@ -153,19 +162,24 @@ codeunit 20411 "Qlty. Receiving Integration" if IsHandled then exit; + QltyBatchNotifHelper.BeginBatch(); + QltyBatchNotifHelper.ConfigureForBatch(QltyInspectionCreate); TempTrackingSpecification.Reset(); if TempTrackingSpecification.FindSet() then repeat if QltyInspectionCreate.CreateInspectionWithMultiVariants(SalesLine, TempTrackingSpecification, DummyVariant, DummyVariant, false, QltyInspectionGenRule) then begin HasInspection := true; QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); end; until TempTrackingSpecification.Next() = 0 else if QltyInspectionCreate.CreateInspectionWithMultiVariants(SalesLine, DummyVariant, DummyVariant, DummyVariant, false, QltyInspectionGenRule) then begin HasInspection := true; QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); end; + QltyBatchNotifHelper.EndBatch(); end; OnAfterSalesReturnCreateInspectionWithSalesLine(SalesHeader, SalesLine, TempItemLedgEntryNotInvoiced, TempTrackingSpecification, HasInspection, QltyInspectionHeader); @@ -185,8 +199,11 @@ codeunit 20411 "Qlty. Receiving Integration" ApplicableReceivingQltyInspectionGenRule.Reset(); ApplicableReceivingQltyInspectionGenRule.SetRange("Transfer Order Trigger", ApplicableReceivingQltyInspectionGenRule."Transfer Order Trigger"::OnTransferOrderPostReceive); ApplicableReceivingQltyInspectionGenRule.SetFilter("Activation Trigger", '%1|%2', ApplicableReceivingQltyInspectionGenRule."Activation Trigger"::"Manual or Automatic", ApplicableReceivingQltyInspectionGenRule."Activation Trigger"::"Automatic only"); - if not ApplicableReceivingQltyInspectionGenRule.IsEmpty() then + if not ApplicableReceivingQltyInspectionGenRule.IsEmpty() then begin + QltyBatchNotifHelper.BeginBatch(); AttemptCreateInspectionWithReceiveTransferLine(TransLine, UnusedTransTransferReceiptHeader, DirectTransHeader); + QltyBatchNotifHelper.EndBatch(); + end; end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"TransferOrder-Post Receipt", 'OnAfterInsertTransRcptLine', '', true, true)] @@ -203,8 +220,11 @@ codeunit 20411 "Qlty. Receiving Integration" ApplicableReceivingQltyInspectionGenRule.Reset(); ApplicableReceivingQltyInspectionGenRule.SetRange("Transfer Order Trigger", ApplicableReceivingQltyInspectionGenRule."Transfer Order Trigger"::OnTransferOrderPostReceive); ApplicableReceivingQltyInspectionGenRule.SetFilter("Activation Trigger", '%1|%2', ApplicableReceivingQltyInspectionGenRule."Activation Trigger"::"Manual or Automatic", ApplicableReceivingQltyInspectionGenRule."Activation Trigger"::"Automatic only"); - if not ApplicableReceivingQltyInspectionGenRule.IsEmpty() then + if not ApplicableReceivingQltyInspectionGenRule.IsEmpty() then begin + QltyBatchNotifHelper.BeginBatch(); AttemptCreateInspectionWithReceiveTransferLine(TransLine, TransferReceiptHeader, UnusedDirectTransHeader); + QltyBatchNotifHelper.EndBatch(); + end; end; [EventSubscriber(ObjectType::Codeunit, Codeunit::"Release Purchase Document", 'OnAfterReleasePurchaseDoc', '', true, true)] @@ -227,6 +247,7 @@ codeunit 20411 "Qlty. Receiving Integration" if ApplicableReceivingQltyInspectionGenRule.IsEmpty() then exit; + QltyBatchNotifHelper.BeginBatch(); PurchaseLine.SetRange("Document Type", PurchaseHeader."Document Type"); PurchaseLine.SetRange("Document No.", PurchaseHeader."No."); PurchaseLine.SetRange(Type, PurchaseLine.Type::Item); @@ -255,6 +276,7 @@ codeunit 20411 "Qlty. Receiving Integration" AttemptCreateInspectionWithPurchaseLineAndTracking(PurchaseLine, PurchaseHeader, TempTrackingSpecification); end; until PurchaseLine.Next() = 0; + QltyBatchNotifHelper.EndBatch(); end; local procedure AttemptCreateInspectionWithReceiptLine(var WarehouseReceiptLine: Record "Warehouse Receipt Line"; var WarehouseReceiptHeader: Record "Warehouse Receipt Header"; var OptionalSourceLineVariant: Variant) @@ -272,6 +294,8 @@ codeunit 20411 "Qlty. Receiving Integration" if IsHandled then exit; + QltyBatchNotifHelper.ConfigureForBatch(QltyInspectionCreate); + QltyWarehouseIntegration.CollectSourceItemTracking(OptionalSourceLineVariant, TempTrackingSpecification); TempTrackingSpecification.Reset(); @@ -282,6 +306,7 @@ codeunit 20411 "Qlty. Receiving Integration" if QltyInspectionCreate.CreateInspectionWithMultiVariants(WarehouseReceiptLine, OptionalSourceLineVariant, WarehouseReceiptHeader, TempTrackingSpecification, false, TempQltyInspectionGenRule) then begin HasInspection := true; QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); end; until TempTrackingSpecification.Next() = 0 else begin @@ -289,6 +314,7 @@ codeunit 20411 "Qlty. Receiving Integration" if QltyInspectionCreate.CreateInspectionWithMultiVariants(WarehouseReceiptLine, OptionalSourceLineVariant, WarehouseReceiptHeader, DummyVariant, false, TempQltyInspectionGenRule) then begin HasInspection := true; QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); end; end; @@ -311,6 +337,8 @@ codeunit 20411 "Qlty. Receiving Integration" if IsHandled then exit; + QltyBatchNotifHelper.ConfigureForBatch(QltyInspectionCreate); + if QltyWarehouseIntegration.GetOptionalSourceVariantForWarehouseJournalLine(WarehouseJournalLine, OptionalSourceRecordVariant) then QltyWarehouseIntegration.CollectSourceItemTracking(OptionalSourceRecordVariant, TempTrackingSpecification); @@ -322,6 +350,7 @@ codeunit 20411 "Qlty. Receiving Integration" if QltyInspectionCreate.CreateInspectionWithMultiVariants(WarehouseJournalLine, OptionalSourceRecordVariant, PostedWhseReceiptHeader, TempTrackingSpecification, false, TempQltyInspectionGenRule) then begin HasInspection := true; QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); end; until TempTrackingSpecification.Next() = 0 else begin @@ -329,6 +358,7 @@ codeunit 20411 "Qlty. Receiving Integration" if QltyInspectionCreate.CreateInspectionWithMultiVariants(WarehouseJournalLine, OptionalSourceRecordVariant, PostedWhseReceiptHeader, DummyVariant, false, TempQltyInspectionGenRule) then begin HasInspection := true; QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); end; end; @@ -348,10 +378,14 @@ codeunit 20411 "Qlty. Receiving Integration" if IsHandled then exit; + QltyBatchNotifHelper.ConfigureForBatch(QltyInspectionCreate); + TempQltyInspectionGenRule.CopyFilters(ApplicableReceivingQltyInspectionGenRule); HasInspection := QltyInspectionCreate.CreateInspectionWithMultiVariants(PurchaseLine, PurchaseHeader, TempTrackingSpecification, DummyVariant, false, TempQltyInspectionGenRule); - if HasInspection then + if HasInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); + end; OnAfterPurchaseAttemptCreateInspectionWithPurchaseLine(HasInspection, QltyInspectionHeader, PurchaseLine, PurchaseHeader, TempTrackingSpecification); end; @@ -371,6 +405,9 @@ codeunit 20411 "Qlty. Receiving Integration" OnBeforeAttemptCreateInspectionWithInboundTransferLine(TransTransferLine, OptionalTransferReceiptHeader, OptionalDirectTransHeader, TempTrackingSpecification, QltyInspectionHeader, HasInspection, IsHandled); if IsHandled then exit; + + QltyBatchNotifHelper.ConfigureForBatch(QltyInspectionCreate); + CurrentVariant := TransTransferLine; QltyWarehouseIntegration.CollectSourceItemTracking(CurrentVariant, TempTrackingSpecification); TempTrackingSpecification.Reset(); @@ -384,8 +421,10 @@ codeunit 20411 "Qlty. Receiving Integration" if OptionalDirectTransHeader."No." <> '' then HasInspection := QltyInspectionCreate.CreateInspectionWithMultiVariants(TransTransferLine, OptionalDirectTransHeader, TempTrackingSpecification, OptionalTransferReceiptHeader, false, TempQltyInspectionGenRule); - if HasInspection then + if HasInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); + end; until TempTrackingSpecification.Next() = 0 else begin TempQltyInspectionGenRule.CopyFilters(ApplicableReceivingQltyInspectionGenRule); @@ -395,8 +434,10 @@ codeunit 20411 "Qlty. Receiving Integration" if OptionalDirectTransHeader."No." <> '' then HasInspection := QltyInspectionCreate.CreateInspectionWithMultiVariants(TransTransferLine, OptionalDirectTransHeader, OptionalTransferReceiptHeader, TempTrackingSpecification, false, TempQltyInspectionGenRule); - if HasInspection then + if HasInspection then begin QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); + end; end; OnAfterTransferAttemptCreateInspectionWithInboundTransferLine(TransTransferLine, OptionalTransferReceiptHeader, OptionalDirectTransHeader, TempTrackingSpecification, QltyInspectionHeader, HasInspection); end; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Sales/Document/QltySalesOrderSubform.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Sales/Document/QltySalesOrderSubform.PageExt.al index 8261c3c4e7..f6b36da08e 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Sales/Document/QltySalesOrderSubform.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Sales/Document/QltySalesOrderSubform.PageExt.al @@ -21,7 +21,7 @@ pageextension 20405 "Qlty. Sales Order Subform" extends "Sales Order Subform" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = I; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspection'; ToolTip = 'Creates a quality inspection for this sales order line.'; AboutTitle = 'Create Quality Inspection'; @@ -39,7 +39,7 @@ pageextension 20405 "Qlty. Sales Order Subform" extends "Sales Order Subform" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item and Document'; ToolTip = 'Shows quality inspections for this item and document.'; AboutTitle = 'Show Quality Inspections'; @@ -57,7 +57,7 @@ pageextension 20405 "Qlty. Sales Order Subform" extends "Sales Order Subform" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item'; ToolTip = 'Shows Quality Inspections for Item'; AboutTitle = 'Show Quality Inspections'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Sales/Document/QltySalesReturnOrderSubf.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Sales/Document/QltySalesReturnOrderSubf.PageExt.al index 1a279274bc..6554513499 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Sales/Document/QltySalesReturnOrderSubf.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Sales/Document/QltySalesReturnOrderSubf.PageExt.al @@ -21,7 +21,7 @@ pageextension 20406 "Qlty. Sales Return Order Subf." extends "Sales Return Order { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = I; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspection'; ToolTip = 'Creates a quality inspection for this sales return order line.'; AboutTitle = 'Create Quality Inspection'; @@ -39,7 +39,7 @@ pageextension 20406 "Qlty. Sales Return Order Subf." extends "Sales Return Order { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item and Document'; ToolTip = 'Shows quality inspections for this item and document.'; AboutTitle = 'Show Quality Inspections'; @@ -57,7 +57,7 @@ pageextension 20406 "Qlty. Sales Return Order Subf." extends "Sales Return Order { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item'; ToolTip = 'Shows Quality Inspections for Item'; AboutTitle = 'Show Quality Inspections'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Warehouse/Ledger/QltyWarehouseEntries.PageExt.al b/src/Apps/W1/Quality Management/app/src/Integration/Warehouse/Ledger/QltyWarehouseEntries.PageExt.al index 8022315db3..5a6ee45919 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Warehouse/Ledger/QltyWarehouseEntries.PageExt.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Warehouse/Ledger/QltyWarehouseEntries.PageExt.al @@ -21,7 +21,7 @@ pageextension 20427 "Qlty. Warehouse Entries" extends "Warehouse Entries" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = I; - Image = CreateForm; + Image = BulletList; Caption = 'Create Quality Inspection'; ToolTip = 'Creates a quality inspection for this warehouse entry.'; AboutTitle = 'Create Quality Inspection'; @@ -38,7 +38,7 @@ pageextension 20427 "Qlty. Warehouse Entries" extends "Warehouse Entries" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item and Document'; ToolTip = 'Shows quality inspections for this item and document.'; AboutTitle = 'Show Quality Inspections'; @@ -55,7 +55,7 @@ pageextension 20427 "Qlty. Warehouse Entries" extends "Warehouse Entries" { ApplicationArea = QualityManagement; AccessByPermission = tabledata "Qlty. Inspection Header" = R; - Image = TaskQualityMeasure; + Image = CheckList; Caption = 'Show Quality Inspections for Item'; ToolTip = 'Shows Quality Inspections for Item'; AboutTitle = 'Show Quality Inspections'; diff --git a/src/Apps/W1/Quality Management/app/src/Integration/Warehouse/QltyWarehouseIntegration.Codeunit.al b/src/Apps/W1/Quality Management/app/src/Integration/Warehouse/QltyWarehouseIntegration.Codeunit.al index 0ca1151dc0..df7071bab4 100644 --- a/src/Apps/W1/Quality Management/app/src/Integration/Warehouse/QltyWarehouseIntegration.Codeunit.al +++ b/src/Apps/W1/Quality Management/app/src/Integration/Warehouse/QltyWarehouseIntegration.Codeunit.al @@ -40,6 +40,7 @@ codeunit 20438 "Qlty. Warehouse Integration" QltyInspectionHeader: Record "Qlty. Inspection Header"; TempTrackingSpecification: Record "Tracking Specification" temporary; QltyInspectionCreate: Codeunit "Qlty. Inspection - Create"; + QltyBatchNotifHelper: Codeunit "Qlty. Batch Notif. Helper"; DoNotSendSourceVariant: Variant; IsHandled: Boolean; HasInspection: Boolean; @@ -60,19 +61,25 @@ codeunit 20438 "Qlty. Warehouse Integration" if GetOptionalSourceVariantForWarehouseJournalLine(WarehouseJournalLine, DoNotSendSourceVariant) then CollectSourceItemTracking(DoNotSendSourceVariant, TempTrackingSpecification); + QltyBatchNotifHelper.BeginBatch(); + QltyBatchNotifHelper.ConfigureForBatch(QltyInspectionCreate); TempTrackingSpecification.Reset(); if TempTrackingSpecification.FindSet() then repeat + Clear(QltyInspectionHeader); if QltyInspectionCreate.CreateInspectionWithMultiVariants(WarehouseEntry, WarehouseJournalLine, TempTrackingSpecification, DummyVariant, false, QltyInspectionGenRule) then begin HasInspection := true; QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); end; until TempTrackingSpecification.Next() = 0 else if QltyInspectionCreate.CreateInspectionWithMultiVariants(WarehouseEntry, WarehouseJournalLine, DummyVariant, DummyVariant, false, QltyInspectionGenRule) then begin HasInspection := true; QltyInspectionCreate.GetCreatedInspection(QltyInspectionHeader); + QltyBatchNotifHelper.TrackCreatedInspection(QltyInspectionHeader."No."); end; + QltyBatchNotifHelper.EndBatch(); OnAfterWarehouseAttemptCreateInspectionWithWhseJournalLine(HasInspection, QltyInspectionHeader, WarehouseEntry, WarehouseJournalLine, DoNotSendSourceVariant); end; diff --git a/src/Apps/W1/Quality Management/app/src/Permissions/D365BasicQltyMngmnt.PermissionSetExt.al b/src/Apps/W1/Quality Management/app/src/Permissions/AdministratorQltyMgmt.PermissionSetExt.al similarity index 77% rename from src/Apps/W1/Quality Management/app/src/Permissions/D365BasicQltyMngmnt.PermissionSetExt.al rename to src/Apps/W1/Quality Management/app/src/Permissions/AdministratorQltyMgmt.PermissionSetExt.al index 1aa3b8824f..bd8b0e3d67 100644 --- a/src/Apps/W1/Quality Management/app/src/Permissions/D365BasicQltyMngmnt.PermissionSetExt.al +++ b/src/Apps/W1/Quality Management/app/src/Permissions/AdministratorQltyMgmt.PermissionSetExt.al @@ -6,7 +6,7 @@ namespace Microsoft.QualityManagement.Permissions; using System.Security.AccessControl; -permissionsetextension 20400 "D365 BASIC - QltyMngmnt" extends "D365 BASIC" +permissionsetextension 20402 "Administrator - QltyMgmt" extends "Administrator" { - IncludedPermissionSets = "QltyMngmnt - Edit"; -} \ No newline at end of file + IncludedPermissionSets = "QltyMgmt - Admin"; +} diff --git a/src/Apps/W1/Quality Management/app/src/Permissions/AdministratorQltyMngmnt.PermissionSetExt.al b/src/Apps/W1/Quality Management/app/src/Permissions/D365BasicIsvQltyMgmt.PermissionSetExt.al similarity index 77% rename from src/Apps/W1/Quality Management/app/src/Permissions/AdministratorQltyMngmnt.PermissionSetExt.al rename to src/Apps/W1/Quality Management/app/src/Permissions/D365BasicIsvQltyMgmt.PermissionSetExt.al index 4b49d6c5da..c1248077fd 100644 --- a/src/Apps/W1/Quality Management/app/src/Permissions/AdministratorQltyMngmnt.PermissionSetExt.al +++ b/src/Apps/W1/Quality Management/app/src/Permissions/D365BasicIsvQltyMgmt.PermissionSetExt.al @@ -6,7 +6,7 @@ namespace Microsoft.QualityManagement.Permissions; using System.Security.AccessControl; -permissionsetextension 20402 "Administrator - QltyMngmnt" extends "Administrator" +permissionsetextension 20400 "D365 BASIC ISV - QltyMgmt" extends "D365 BASIC ISV" { - IncludedPermissionSets = "QltyMngmnt - Edit"; -} \ No newline at end of file + IncludedPermissionSets = "QltyMgmt - Admin"; +} diff --git a/src/Apps/W1/Quality Management/app/src/Permissions/D365BusFullAccessQltyMngmnt.PermissionSetExt.al b/src/Apps/W1/Quality Management/app/src/Permissions/D365BusFullAccessQltyMgmt.PermissionSetExt.al similarity index 75% rename from src/Apps/W1/Quality Management/app/src/Permissions/D365BusFullAccessQltyMngmnt.PermissionSetExt.al rename to src/Apps/W1/Quality Management/app/src/Permissions/D365BusFullAccessQltyMgmt.PermissionSetExt.al index b6f8d3a6b6..b90b3cc2c7 100644 --- a/src/Apps/W1/Quality Management/app/src/Permissions/D365BusFullAccessQltyMngmnt.PermissionSetExt.al +++ b/src/Apps/W1/Quality Management/app/src/Permissions/D365BusFullAccessQltyMgmt.PermissionSetExt.al @@ -6,7 +6,7 @@ namespace Microsoft.QualityManagement.Permissions; using System.Security.AccessControl; -permissionsetextension 20401 "D365 BUS FULL ACCESS - QltyMngmnt" extends "D365 BUS FULL ACCESS" +permissionsetextension 20401 "D365 BUS FULL ACCESS - QltyMgmt" extends "D365 BUS FULL ACCESS" { - IncludedPermissionSets = "QltyMngmnt - Edit"; -} \ No newline at end of file + IncludedPermissionSets = "QltyMgmt - Admin"; +} diff --git a/src/Apps/W1/Quality Management/app/src/Permissions/D365ReadQltyMngmnt.PermissionSetExt.al b/src/Apps/W1/Quality Management/app/src/Permissions/D365ReadQltyMgmt.PermissionSetExt.al similarity index 78% rename from src/Apps/W1/Quality Management/app/src/Permissions/D365ReadQltyMngmnt.PermissionSetExt.al rename to src/Apps/W1/Quality Management/app/src/Permissions/D365ReadQltyMgmt.PermissionSetExt.al index 6271133f8b..7d87a70494 100644 --- a/src/Apps/W1/Quality Management/app/src/Permissions/D365ReadQltyMngmnt.PermissionSetExt.al +++ b/src/Apps/W1/Quality Management/app/src/Permissions/D365ReadQltyMgmt.PermissionSetExt.al @@ -6,7 +6,7 @@ namespace Microsoft.QualityManagement.Permissions; using System.Security.AccessControl; -permissionsetextension 20403 "D365 READ - QltyMngmnt" extends "D365 READ" +permissionsetextension 20403 "D365 READ - QltyMgmt" extends "D365 READ" { - IncludedPermissionSets = "QltyMngmnt - Read"; -} \ No newline at end of file + IncludedPermissionSets = "QltyMgmt - Read"; +} diff --git a/src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntEdit.PermissionSet.al b/src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtAdmin.PermissionSet.al similarity index 92% rename from src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntEdit.PermissionSet.al rename to src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtAdmin.PermissionSet.al index 6a9d66f7a8..ed84243f63 100644 --- a/src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntEdit.PermissionSet.al +++ b/src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtAdmin.PermissionSet.al @@ -18,13 +18,13 @@ using Microsoft.QualityManagement.Workflow; /// /// Used for administering Quality Management and supervising Quality Inspections. /// -permissionset 20405 "QltyMngmnt - Edit" +permissionset 20405 "QltyMgmt - Admin" { - Caption = 'Quality Management - Full edit access'; + Caption = 'Quality Admin & Supervisor'; Access = Public; Assignable = true; - IncludedPermissionSets = "QltyMngmnt - Objects"; + IncludedPermissionSets = "QltyMgmt - Objects"; Permissions = tabledata "Qlty. Management Setup" = RIMD, diff --git a/src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntInspector.PermissionSet.al b/src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtInspector.PermissionSet.al similarity index 57% rename from src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntInspector.PermissionSet.al rename to src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtInspector.PermissionSet.al index 3fdf824ecc..28f5f07538 100644 --- a/src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntInspector.PermissionSet.al +++ b/src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtInspector.PermissionSet.al @@ -10,7 +10,6 @@ using Microsoft.QualityManagement.Configuration.SourceConfiguration; using Microsoft.QualityManagement.Configuration.Template; using Microsoft.QualityManagement.Configuration.Template.Test; using Microsoft.QualityManagement.Document; -using Microsoft.QualityManagement.Integration.Inventory.Transfer; using Microsoft.QualityManagement.RoleCenters; using Microsoft.QualityManagement.Setup; using Microsoft.QualityManagement.Workflow; @@ -18,29 +17,27 @@ using Microsoft.QualityManagement.Workflow; /// /// Used for working with Quality Inspections. /// -permissionset 20404 QltyMngmntInspector +permissionset 20404 "QltyMgmt - Inspector" { - Caption = 'Quality Management - Quality Inspector'; + Caption = 'Quality Inspector'; Access = Public; Assignable = true; - IncludedPermissionSets = "QltyMngmnt - Objects"; + IncludedPermissionSets = "QltyMgmt - Objects"; Permissions = - tabledata "Qlty. Workflow Config. Value" = RIMD, - tabledata "Qlty. Inspection Gen. Rule" = RIMd, + tabledata "Qlty. Workflow Config. Value" = Rim, + tabledata "Qlty. Inspection Gen. Rule" = R, tabledata "Qlty. I. Result Condit. Conf." = RIMd, - tabledata "Qlty. Inspection Result" = RIMd, - tabledata "Qlty. Inspection Template Hdr." = RIMd, - tabledata "Qlty. Inspection Template Line" = RIMd, - tabledata "Qlty. Test Lookup Value" = RIMd, - tabledata "Qlty. Management Setup" = RIMd, - tabledata "Qlty. Related Transfers Buffer" = RIMD, - tabledata "Qlty. Mgmt. Role Center Cue" = RIMd, - tabledata "Qlty. Inspect. Src. Fld. Conf." = RIMd, - tabledata "Qlty. Inspect. Source Config." = RIMd, + tabledata "Qlty. Inspection Result" = R, + tabledata "Qlty. Inspection Template Hdr." = R, + tabledata "Qlty. Inspection Template Line" = R, + tabledata "Qlty. Test Lookup Value" = R, + tabledata "Qlty. Management Setup" = R, + tabledata "Qlty. Mgmt. Role Center Cue" = Ri, + tabledata "Qlty. Inspect. Src. Fld. Conf." = R, + tabledata "Qlty. Inspect. Source Config." = R, tabledata "Qlty. Inspection Line" = RIMd, tabledata "Qlty. Inspection Header" = RIMd, - tabledata "Qlty. Test" = RIMd; + tabledata "Qlty. Test" = R; } - diff --git a/src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntObjects.PermissionSet.al b/src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtObjects.PermissionSet.al similarity index 97% rename from src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntObjects.PermissionSet.al rename to src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtObjects.PermissionSet.al index e50387bcf0..018518534f 100644 --- a/src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntObjects.PermissionSet.al +++ b/src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtObjects.PermissionSet.al @@ -28,7 +28,6 @@ using Microsoft.QualityManagement.Integration.Foundation.Navigate; using Microsoft.QualityManagement.Integration.Inventory; using Microsoft.QualityManagement.Integration.Inventory.Transfer; using Microsoft.QualityManagement.Integration.Manufacturing; -using Microsoft.QualityManagement.Integration.Manufacturing.Routing; using Microsoft.QualityManagement.Integration.Receiving; using Microsoft.QualityManagement.Integration.Utilities; using Microsoft.QualityManagement.Integration.Warehouse; @@ -41,11 +40,11 @@ using Microsoft.QualityManagement.Setup.SetupGuide; using Microsoft.QualityManagement.Utilities; using Microsoft.QualityManagement.Workflow; -permissionset 20406 "QltyMngmnt - Objects" +permissionset 20406 "QltyMgmt - Objects" { Caption = 'Quality Management - Objects'; Access = Internal; - Assignable = true; + Assignable = false; Permissions = // Codeunits @@ -88,6 +87,7 @@ permissionset 20406 "QltyMngmnt - Objects" codeunit "Qlty. Permission Mgmt." = X, codeunit "Qlty. Manufactur. Integration" = X, codeunit "Qlty. Assembly Integration" = X, + codeunit "Qlty. Batch Notif. Helper" = X, codeunit "Qlty. Demo Data Mgmt." = X, codeunit "Qlty. Receiving Integration" = X, codeunit "Qlty. Report Mgmt." = X, @@ -117,7 +117,6 @@ permissionset 20406 "QltyMngmnt - Objects" page "Qlty. Rec. Gen. Rule S. Guide" = X, page "Qlty. Related Transfer Orders" = X, page "Qlty. Report Selection - QM" = X, - page "Qlty. Routing Line Lookup" = X, page "Qlty. Inspection Template List" = X, page "Qlty. Inspection Template Subf" = X, page "Qlty. Inspection Template" = X, diff --git a/src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntRead.PermissionSet.al b/src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtRead.PermissionSet.al similarity index 92% rename from src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntRead.PermissionSet.al rename to src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtRead.PermissionSet.al index ba280e8fe4..4dc732e53d 100644 --- a/src/Apps/W1/Quality Management/app/src/Permissions/QltyMngmntRead.PermissionSet.al +++ b/src/Apps/W1/Quality Management/app/src/Permissions/QltyMgmtRead.PermissionSet.al @@ -18,13 +18,13 @@ using Microsoft.QualityManagement.Workflow; /// /// Used for full read-only access to Quality Management. /// -permissionset 20401 "QltyMngmnt - Read" +permissionset 20401 "QltyMgmt - Read" { - Caption = 'Quality Management - Read access'; + Caption = 'Quality Auditor'; Access = Public; Assignable = true; - IncludedPermissionSets = "QltyMngmnt - Objects"; + IncludedPermissionSets = "QltyMgmt - Objects"; Permissions = tabledata "Qlty. Management Setup" = R, diff --git a/src/Apps/W1/Quality Management/app/src/Reports/QltyCertificateOfAnalysis.docx b/src/Apps/W1/Quality Management/app/src/Reports/QltyCertificateOfAnalysis.docx deleted file mode 100644 index a977f4134a..0000000000 Binary files a/src/Apps/W1/Quality Management/app/src/Reports/QltyCertificateOfAnalysis.docx and /dev/null differ diff --git a/src/Apps/W1/Quality Management/app/src/Reports/QltyCertificateOfAnalysisDefault.rdl b/src/Apps/W1/Quality Management/app/src/Reports/QltyCertificateOfAnalysisDefault.rdl deleted file mode 100644 index 9256a0161a..0000000000 --- a/src/Apps/W1/Quality Management/app/src/Reports/QltyCertificateOfAnalysisDefault.rdl +++ /dev/null @@ -1,2694 +0,0 @@ - - - 0 - - - - SQL - - - None - 38e49080-d232-4b28-abf1-9b23c82796fb - - - - - - - - - - - 1.59596in - - - 1.08248in - - - 1.46927in - - - 1.34029in - - - 1.25976in - - - - - 0.03125in - - - - - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.78802in - - - - - - - true - - - - - Test Document No. - - - - - - - Textbox21 - 0.03819in - 0in - 0.20152in - 1.33634in - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =Fields!QltyInspection_No.Value - - - - - - - QltyInspection_No2 - 0.03819in - 1.55221in - 0.20152in - 3.44494in - 1 - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!QltyInspection_Source_Item_No_.Value & " " & Fields!QltyInspection_Source_Variant_Code.Value & " "' Fields!QltyInspection_Source_Item_Description.Value & " "' Fields!QltyInspection_Source_Item_Description2.Value - - - - - - - QltyInspection_Source_Item_No_ - 0.2536in - 1.55221in - 0.20152in - 3.44494in - 2 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =Fields!QltyInspection_Source_Lot_No_.Value & " " & Fields!QltyInspection_Source_Serial_No_.Value - - - - - - - QltyInspection_Source_Lot_No_ - 0.45511in - 1.55221in - 0.21541in - 3.44494in - 3 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Product - - - - - - - Textbox21 - 0.2536in - 0in - 0.20152in - 1.33634in - 4 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Item Tracking - - - - - - - Textbox21 - 0.469in - 0in - 0.20152in - 1.33634in - 5 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - 0.79691in - - - - - - - true - - - - - Reinspection sequence - - - - =Fields!QltyInspection_ReinspectionNo.Value - - - - ( - - - - =First(Fields!QltyInspection_Status.Value) - - - - , Result " - - - - =First(Fields!QltyInspection_Result_Description.Value) - - - - ") - - - - - - - Textbox21 - 0.14583in - 0.025in - 0.21528in - 6.69393in - - - - Black - - 1pt - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Completed by: - - - - =First(Fields!QltyInspection_Finished_By_User_ID.Value) - - - - - - - - - - - Textbox21 - 0.36111in - 0.41181in - 0.20139in - 6.30712in - 1 - - - - Black - - 1pt - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - On: - - - - - - - - =First(Fields!QltyInspection_Finished_Date.Value) - - - - - - - Textbox21 - 0.5625in - 0.41181in - 0.20139in - 6.30712in - 2 - - - - Black - - 1pt - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - 0.19248in - - - - - true - - - - - Metric - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - Measurement - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - ="Result" - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - =Fields!PromptedResultCaption_1.Value & " Condition" - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - =Fields!PromptedResultCaption_2.Value & " Condition" - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - - - true - - - - - 4 - - - - - - - - - true - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.28756in - - - - - - - true - true - - - - - =Fields!Field_Description.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - 0.24306in - - - - - true - true - - - - - - - - - =Fields!Field_Description.Value - - - - - - Middle - 4pt - 4pt - 4pt - 4pt - - - - - - - - true - true - - - - - =Fields!Test_Value.Value - - - - - - Middle - 4pt - 4pt - 4pt - 4pt - - - - - - - - true - - - - - =Fields!Test_ResultDescription.Value - - - - - - Middle - 4pt - 4pt - 4pt - 4pt - - - - - - - - true - true - - - - - =Fields!PromptedResultConditionDescription_1.Value - - - - - - Middle - 4pt - 4pt - 4pt - 4pt - - - - - - - - true - true - - - - - =Fields!PromptedResultConditionDescription_2.Value - - - - - - Middle - 4pt - 4pt - 4pt - 4pt - - - - - - - - 0.23633in - - - - - true - true - - - - - - - - - - - 4pt - 4pt - 4pt - 4pt - - - - - - - - true - true - - - - - - - - - - - 4pt - 4pt - 4pt - 4pt - - - - - - - - true - true - - - - - - - - - - - 4pt - 4pt - 4pt - 4pt - - - - - - - - true - true - - - - - - - - - - - 4pt - 4pt - 4pt - 4pt - - - - - - - - true - true - - - - - - - - - - - 4pt - 4pt - 4pt - 4pt - - - - - - - - 2.42299in - - - - - - - - - true - true - - - - - =First(Fields!QltyInspection_Finished_By_Title.Value) - - - - Signature - - - - - - - Textbox2 - 1.33128in - 0.16736in - 0.31394in - 2.96803in - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Finished_Date.Value) - - - - - - - QltyInspection_Finished_Date1 - 1.71466in - 0.16736in - 0.25in - 2.96803in - 1 - - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - Date - - - - - - - Textbox2 - 1.97855in - 0.16736in - 0.25838in - 2.96803in - 2 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - Date - - - - - - - Textbox2 - 1.97855in - 3.58754in - 0.25838in - 2.96803in - 3 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Finished_Date.Value) - - - - - - - QltyInspection_Finished_Date1 - 1.71466in - 3.58754in - 0.25in - 2.96803in - 4 - - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =First(Fields!QltyInspection_Director_Title.Value) - - - - Signature - - - - - - - Textbox2 - 1.33128in - 3.58754in - 0.31394in - 2.96803in - 5 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =First(Fields!QltyInspection_Finished_By_Title.Value) - - - - Name - - - - - - - Textbox2 - 0.83722in - 0.16736in - 0.31394in - 2.96803in - 6 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =First(Fields!QltyInspection_Director_Title.Value) - - - - Name - - - - - - - Textbox2 - 0.83722in - 3.58754in - 0.31394in - 2.96803in - 7 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Director_Name.Value) - - - - - - - QltyInspection_Finished_Date1 - 0.57333in - 3.58754in - 0.25in - 2.96803in - 8 - - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - 6.15439cm - 17.06608cm - - - - - - true - - - - - 5 - - - - - - - - - - - - - - - - - - =CBool(Fields!PromptedResultVisible_1.Value)=CBool(False) - - - - - =CBool(Fields!PromptedResultVisible_2.Value)=CBool(False) - - - - - - - - - - =Fields!QltyInspection_No.Value - - - Between - - - - - =Fields!QltyInspection_No.Value - - - - 0.03125in - - - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - After - - - - 0.0625in - - - true - - - - - - - After - - - - - =Fields!QltyInspection_ReinspectionNo.Value - - - - - =Fields!QltyInspection_ReinspectionNo.Value - - - - 0.03125in - - - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - - - - - =not Fields!Field_IsLabel.Value - - - - - =Fields!Field_IsLabel.Value - - - - - - - - - - - - =Fields!QltyInspection_No.Value - - - - - =Fields!QltyInspectionTemplate_Description.Value - - - - 0.03125in - - - true - true - - - - - =Fields!QltyInspectionTemplate_Description.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - - - - - true - DataSet_Result - 0in - 0in - 5.02985in - 6.84151in - - - Segoe UI - 8pt - - - - 5.02985in - - - - - - - Textbox36 - 0.21181in - 0.09375in - 0.34828in - 3.24408in - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Page - - - - =Globals!PageNumber - - - - of - - - - =Globals!OverallPageNumber - - - - - - - - - - - - - - - - - - =Fields!CompanyInformation_All.Value - - - - - - - - - - - - - - - - - - =Fields!COAContact_All.Value - - - - - - - QltyInspection_No2 - 0.21181in - 3.50768in - 1.90625in - 3.33383in - 1 - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.75in - 0.75in - 0.75in - 0.75in - - - 2pt - 2pt - 2pt - 2pt - - - 5 - - - - - - - - - - 0.09722in - - - - - true - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - 5 - - - - - - - - - - 1.06435in - - - - - - - true - - - - - Test Description - - - - - - - Textbox21 - 0.23112in - 0.04375in - 0.22235in - 1.36008in - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Description.Value) - - - - - - - QltyInspection_No2 - 0.22773in - 1.59596in - 0.22235in - 3.22553in - 1 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Product - - - - - - - Textbox21 - 0.45347in - 0.04375in - 0.22773in - 1.36008in - 2 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!QltyInspection_Source_Item_No_.Value & " " & Fields!QltyInspection_Source_Variant_Code.Value & " "' Fields!QltyInspection_Source_Item_Description.Value & " "' Fields!QltyInspection_Source_Item_Description2.Value - - - - - - - QltyInspection_Source_Item_No_ - 0.45008in - 1.59596in - 0.22773in - 3.22553in - 3 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Item Tracking - - - - - - - Textbox21 - 0.6812in - 0.04375in - 0.21044in - 1.36008in - 4 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =Fields!QltyInspection_Source_Lot_No_.Value & " " & Fields!QltyInspection_Source_Serial_No_.Value - - - - - - - QltyInspection_Source_Lot_No_ - 0.6778in - 1.59596in - 0.21044in - 3.22553in - 5 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Test Document No. - - - - - - - Textbox21 - 0.0034in - 0.04375in - 0.22773in - 1.36008in - 6 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!QltyInspection_No.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - 1.26104in - - - - - - - true - - - - - Reinspection sequence - - - - =Fields!QltyInspection_ReinspectionNo.Value - - - - ( - - - - =First(Fields!QltyInspection_Status.Value) - - - - , Result " - - - - =First(Fields!QltyInspection_Result_Description.Value) - - - - ") - - - - - - - Textbox21 - 0.25in - 0.28949in - 0.25in - 6.0287in - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Completed by: - - - - =First(Fields!QltyInspection_Finished_By_User_ID.Value) - - - - - - - - - - - Textbox21 - 0.5in - 0.54911in - 0.25in - 4.99825in - 1 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Field - - - - - - - Textbox21 - 1in - 0.54911in - 0.20548in - 1.10075in - 2 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Test Value - - - - - - - Textbox21 - 1in - 1.64986in - 0.20548in - 2.0295in - 3 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Last Modified By - - - - - - - Textbox21 - 1in - 4.57251in - 0.20548in - 1.51315in - 4 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - On: - - - - =First(Fields!QltyInspection_Finished_Date.Value) - - - - - - - Textbox21 - 0.75in - 0.54911in - 0.25in - 4.99825in - 5 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - 0.03125in - - - - - true - - - - - 4 - - - - - - - - - true - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.25in - - - - - true - true - - - - - =Fields!Field_Description.Value - - - - - - - Textbox8 - - - 2pt - 2pt - 2pt - 2pt - - - 5 - - - - - - - - - - 0.39418in - - - - - true - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - =Fields!Field_Description.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - =Fields!Test_Value.Value - - - - - - 4pt - 4pt - 4pt - 2pt - - - - - - - - true - true - - - - - =Fields!Field_ModifiedByUserName.Value - - - - - - - - - =Fields!Field_ModifiedDateTime.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - 2 - - - - - - - 0.25in - - - - - true - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - =Fields!Field_LineCommentary.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - 3 - - - - - - - - 1.0381in - - - - - true - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - =Fields!Field_Description.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - - - true - true - - - - - =Fields!Field_IfPersonName.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!Field_IfPersonTitle.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!Field_IfPersonEmail.Value - - - - - - - Field_IfPersonEmail - 0.77778in - 0.01389in - 0.25in - 1.42367in - 2 - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!Field_IfPersonPhone.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - - - - - - true - true - - - - - =Fields!Field_ModifiedByUserName.Value - - - - - - - - - =Fields!Field_ModifiedDateTime.Value - - - - - - 4pt - 4pt - 4pt - 2pt - - - 2 - - - - - - - - - - - - - - - =CBool(Fields!PromptedResultVisible_1.Value)=CBool(False) - - - - - =CBool(Fields!PromptedResultVisible_2.Value)=CBool(False) - - - - - - - - - - =Fields!QltyInspection_No.Value - - - Between - - - - - =Fields!QltyInspection_No.Value - - - - 0.03125in - - - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - After - - - - 0.03125in - - - true - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - After - - - - 0.0625in - - - true - - - - - - - After - - - - - =Fields!QltyInspection_ReinspectionNo.Value - - - - - =Fields!QltyInspection_ReinspectionNo.Value - - - - 0.03125in - - - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - After - - - - - - - =Not Fields!Field_IsLabel.Value - - - - - =Fields!Field_IsLabel.Value Or Fields!Field_IsPersonField.Value - - - - - =not Len(Fields!Field_LineCommentary.Value)>0 - - - - - =Not Fields!Field_IsPersonField.Value - - - - - - - - - - - - - DataSet_Result - 0in - 0in - 4.5285in - 6.41194in - - - - - - 5.43202in - - - - =Globals!PageNumber - - - - of - - - - =Globals!TotalPages - - - - - - - - - - - - - - - - - - =Fields!CompanyInformation_All.Value - - - - - - - - - - - - - - - - - - =Fields!COAContact_All.Value - - - - - - - 0.13889in - 3.78433in - 1.16667in - 2.62761in - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - Quality Inspection Report - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.75in - 0.75in - 0.75in - 0.75in - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 1.17691in - - - - - - - true - - - - - Test Document No. - - - - - - - Textbox21 - 0.30806in - 0.04826in - 0.22929in - 1.30647in - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =Fields!QltyInspection_No.Value - - - - - - - QltyInspection_No2 - 0.30806in - 1.57763in - 0.22929in - 2.41623in - 1 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Item - - - - - - - Textbox21 - 0.53736in - 0.04826in - 0.23467in - 1.30647in - 2 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!QltyInspection_Source_Item_No_.Value & " " & Fields!QltyInspection_Source_Variant_Code.Value & " "' Fields!QltyInspection_Source_Item_Description.Value & " "' Fields!QltyInspection_Source_Item_Description2.Value - - - - - - - QltyInspection_Source_Item_No_ - 0.53736in - 1.57763in - 0.23467in - 2.41623in - 3 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Item Tracking - - - - - - - Textbox21 - 0.78592in - 0.04826in - 0.21738in - 1.30647in - 4 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =Fields!QltyInspection_Source_Lot_No_.Value & " " & Fields!QltyInspection_Source_Serial_No_.Value - - - - - - - QltyInspection_Source_Lot_No_ - 0.78592in - 1.57763in - 0.21738in - 2.41623in - 5 - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - 0.28577in - - - - - - - true - - - - - Test ( - - - - =First(Fields!QltyInspection_Status.Value) - - - - , Result " - - - - =First(Fields!QltyInspection_Result_Description.Value) - - - - ") - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - 0.30491in - - - - - - - true - - - - - Field - - - - - - - Textbox21 - 0.07024in - 0.29102in - 0.22244in - 1.02991in - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Test Value - - - - - - - Textbox21 - 0.07024in - 1.32092in - 0.23467in - 1.77118in - 1 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - Entered By - - - - - - - Textbox21 - 0.07024in - 4.49387in - 0.23467in - 1.31284in - 2 - - - Top - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - 0.25in - - - - - true - true - - - - - =Fields!Field_Description.Value - - - - - - - Textbox8 - - - 2pt - 2pt - 2pt - 2pt - - - 5 - - - - - - - - - - 0.41207in - - - - - true - true - - - - - - - - - - - Top - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - =Fields!Field_Description.Value - - - - - - Top - 4pt - 4pt - 4pt - 2pt - - - - - - - - true - true - - - - - =Fields!Test_Value.Value - - - - - - Top - 4pt - 4pt - 4pt - 2pt - - - - - - - - true - true - - - - - =Fields!Field_ModifiedByUserName.Value - - - - - - - - - =Fields!Field_ModifiedDateTime.Value - - - - - - Top - 4pt - 4pt - 4pt - 2pt - - - 2 - - - - - - - 0.25in - - - - - true - true - - - - - - - - - - - Top - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - - - - - - - Top - 4pt - 4pt - 4pt - 2pt - - - - - - - - true - true - true - - - - - =Fields!Field_LineCommentary.Value - - - - - - Top - 4pt - 4pt - 4pt - 2pt - - - 3 - - - - - - - - 1.03992in - - - - - true - true - - - - - - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - true - true - - - - - =Fields!Field_Description.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - - - true - true - - - - - =Fields!Field_IfPersonName.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!Field_IfPersonTitle.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - Name: - - - - - - - Textbox29 - 0in - 0in - 0.25in - 1in - 2 - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - Title: - - - - - - - Textbox29 - 0.25in - 0in - 0.25in - 1in - 3 - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!Field_IfPersonEmail.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =Fields!Field_IfPersonPhone.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - - - - - - true - true - - - - - =Fields!Field_ModifiedByUserName.Value - - - - - - - - - =Fields!Field_ModifiedDateTime.Value - - - - - - 2pt - 2pt - 2pt - 2pt - - - 2 - - - - - - - 2.83085in - - - - - - - true - - - - - =First(Fields!QltyInspection_Finished_By_UserName.Value) - - - - Signature - - - - - - - QltyInspection_Finished_By_User_ID1 - 1.22543in - 0.53764in - 0.25in - 2.58069in - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - =First(Fields!QltyInspection_Finished_By_Title.Value) - - - - Name - - - - - - - Textbox2 - 0.58075in - 0.53764in - 0.25in - 2.58069in - 1 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Director_Title.Value) - - - - Name - - - - - - - QltyInspection_Finished_Date1 - 0.58075in - 3.57318in - 0.25in - 2.58069in - 2 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Director_Title.Value) - - - - Signature - - - - - - - QltyInspection_Finished_Date1 - 1.22543in - 3.57318in - 0.25in - 2.58069in - 3 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Finished_Date.Value) - - - - - - - QltyInspection_Finished_Date1 - 1.54488in - 3.57318in - 0.25in - 2.58069in - 4 - - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - Date - - - - - - - Textbox2 - 1.80877in - 3.57318in - 0.22769in - 2.58069in - 5 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - Date - - - - - - - Textbox2 - 1.80877in - 0.53764in - 0.22769in - 2.58069in - 6 - - - - Black - - 1pt - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Finished_Date.Value) - - - - - - - QltyInspection_Finished_Date1 - 1.54488in - 0.53764in - 0.25in - 2.58069in - 7 - - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - =First(Fields!QltyInspection_Director_Name.Value) - - - - - - - QltyInspection_Finished_Date1 - 0.33075in - 3.57318in - 0.25in - 2.58069in - 8 - - - Bottom - 2pt - 2pt - 2pt - 2pt - - - - true - - - - - 5 - - - - - - - - - - - - - - - - - - =CBool(Fields!PromptedResultVisible_1.Value)=CBool(False) - - - - - =CBool(Fields!PromptedResultVisible_2.Value)=CBool(False) - - - - - - - - - - =Fields!QltyInspection_No.Value - - - Between - - - - - =Fields!QltyInspection_No.Value - - - - 0.03125in - - - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.0625in - - - true - - - - - - - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - - LightGrey - - 1pt - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - - =Fields!QltyInspection_ReinspectionNo.Value - - - - - =Fields!QltyInspection_ReinspectionNo.Value - - - - 0.03125in - - - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - - - - - =Not Fields!Field_IsLabel.Value - - - - - - =not Len(Fields!Field_LineCommentary.Value)>0 - - - - - =not Fields!Field_IsPersonField.Value - - - - - - - - - - - - =Fields!QltyInspectionTemplate_Description.Value - - - - - =Fields!QltyInspectionTemplate_Description.Value - - - - 0.03125in - - - true - true - - - - - =Fields!QltyInspectionTemplate_Description.Value - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.03125in - - - true - true - - - - - - - - - - - White - 2pt - 2pt - 2pt - 2pt - - - - - - - - - - - - - - - - true - DataSet_Result - 0in - 0in - 6.58168in - 6.80421in - - - - - - 6.58168in - - - - - - - Textbox36 - 0.20753in - 0.20451in - 0.34828in - 3.07007in - - - 2pt - 2pt - 2pt - 2pt - - - - true - true - - - - - Page - - - - =Globals!PageNumber - - - - of - - - - =Globals!TotalPages - - - - - - - - - - - - - - - - - - =Fields!CompanyInformation_All.Value - - - - - - - - - - - - - - - - - - =Fields!COAContact_All.Value - - - - - - - CompanyInformation_Row2 - 0.20753in - 4.03141in - 1.57719in - 2.7728in - 1 - - - 2pt - 2pt - 2pt - 2pt - - - - - - - - 0.75in - 0.75in - 0.75in - 0.75in - - - - - - - Textbox1 - - - 12 - - - - - - - - - - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - - - - Textbox2 - - - - - - - - true - true - - - - - - - - - - - - Textbox3 - - - - - - - - true - true - - - - - - - - - - - - Textbox4 - - - - - - - - true - true - - - - - - - - - - - - Textbox5 - - - - - - - - true - true - - - - - - - - - - - - Textbox6 - - - - - - - - true - true - - - - - - - - - - - - - - - - true - true - - - - - - - - - - - - Textbox8 - - - - - - - - true - true - - - - - - - - - - - - Textbox9 - - - - - - - - true - true - - - - - - - - - - - - Textbox10 - - - - - - - - true - true - - - - - - - - - - - - Textbox11 - - - - - - - - true - true - - - - - - - - - - - - Textbox12 - - - - - - - - true - true - - - - - - - - - - - - Textbox13 - - - - - - - - 0.35278cm - - - - - true - true - - - - - =Parameters!No_ItemCaption.Value - - - - - - - Textbox14 - - - 2 - - - - - - - true - true - - - - - - - - - - - - Textbox15 - - - - - - - - true - true - - - - - =Fields!No_Item.Value - - - - - - - Textbox16 - - - Bottom - 5pt - 5pt - - - 5 - - - - - - - - - - true - true - - - - - - - - - - - - Textbox17 - - - - - - - - true - true - - - - - - - - - - - - Textbox18 - - - - - - - - true - true - - - - - - - - - - - - Textbox19 - - - - - - - - true - true - - - - - - - - - - - - Textbox20 - - - - - - - - 0.4064cm - - - - - true - true - - - - - =Parameters!Description_ItemCaption.Value - - - - - - - Textbox21 - - - 3 - - - - - - - - true - true - - - - - =Fields!Description_Item.Value - - - - - - - Textbox22 - - - 6 - - - - - - - - - - - true - true - - - - - - - - - - - - Textbox23 - - - - - - - - true - true - - - - - - - - - - - - Textbox24 - - - - - - - - true - true - - - - - - - - - - - - Textbox25 - - - - - - - - 0.35278cm - - - - - true - true - - - - - =Parameters!ProductionBOMNo_ItemCaption.Value - - - - - - - Textbox26 - - - 3 - - - - - - - - true - true - - - - - =Fields!ProductionBOMNo_Item.Value - - - - - - - Textbox27 - - - 2 - - - - - - - true - true - - - - - =Fields!PBOMVersionCode1.Value - - - - - - - Textbox28 - - - 2 - - - - - - - true - true - - - - - - - - - - - - Textbox29 - - - - - - - - true - true - - - - - - - - - - - - Textbox30 - - - - - - - - true - true - - - - - - - - - - - - Textbox32 - - - - - - - - true - true - - - - - - - - - - - - Textbox34 - - - - - - - - true - true - - - - - - - - - - - - Textbox35 - - - - - - - - 0.35278cm - - - - - true - true - - - - - =Parameters!LotSize_ItemCaption.Value - - - - - - - Textbox36 - - - 3 - - - - - - - - true - true - - - - - =Fields!LotSize_Item.Value - - - - - - - Textbox37 - - - 2 - - - - - - - true - true - - - - - =Fields!BaseUnitOfMeasure_Item.Value - - - - - - 2 - - - - - - - true - true - - - - - - - - - - - - - - - - true - true - - - - - - - - - - - - Textbox40 - - - - - - - - true - true - - - - - - - - - - - - Textbox41 - - - - - - - - true - true - - - - - - - - - - - - Textbox42 - - - - - - - - true - true - - - - - - - - - - - - Textbox43 - - - - - - - - 0.35278cm - - - - - true - true - - - - - =Parameters!RoutingNo_ItemCaption.Value - - - - - - - Textbox44 - - - 3 - - - - - - - - true - true - - - - - =Fields!RoutingNo_Item.Value - - - - - - - Textbox45 - - - 2 - - - - - - - true - true - - - - - =Fields!RtngVersionCode.Value - - - - - - - Textbox46 - - - 2 - - - - - - - true - true - - - - - - - - - - - - Textbox47 - - - - - - - - true - true - - - - - - - - - - - - Textbox48 - - - - - - - - true - true - - - - - - - - - - - - Textbox49 - - - - - - - - true - true - - - - - - - - - - - - Textbox50 - - - - - - - - true - true - - - - - - - - - - - - Textbox51 - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - - - - Textbox52 - - - 12 - - - - - - - - - - - - - - - - - 0.35278cm - - - - - true - true - - - - - =Parameters!OperationNo_RtngLineCaption.Value - - - - - - - Textbox53 - - - 2 - - - - - - - true - true - - - - - =Parameters!Type_RtngLineCaption.Value - - - - - - - Textbox54 - - - - - - - - true - true - - - - - =Parameters!No_RtngLineCaption.Value - - - - - - - Textbox55 - - - - - - - - true - true - - - - - =Parameters!Description_ItemCaption.Value - - - - - - - Textbox56 - - - 2 - - - - - - - true - true - - - - - =Parameters!SetupTime_RtngLineCaption.Value - - - - - - - Textbox57 - - - - - - - - true - true - - - - - =Parameters!RunTime_RtngLineCaption.Value - - - - - - - Textbox58 - - - - - - - - true - true - - - - - =Fields!CostTimeCaption.Value - - - - - - - Textbox59 - - - - - - - - true - true - - - - - =Fields!UnitCostCaption.Value - - - - - - - Textbox60 - - - - - - - - true - true - - - - - =Fields!TotalCostCaption.Value - - - - - - - Textbox62 - - - 2 - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox63 - - - 12 - - - - - - - - - - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox64 - - - - - - Bottom - 5pt - - - 12 - - - - - - - - - - - - - - - - - 0.35278cm - - - - - true - true - - - - - =Fields!TypeCaption.Value - - - - - - - Textbox65 - - - - - - - - true - true - - - - - =Fields!NoCaption.Value - - - - - - - Textbox66 - - - - - - - - true - true - - - - - =Fields!DescriptionCaption.Value - - - - - - - Textbox67 - - - 2 - - - - - - - true - true - - - - - =Fields!QuantityCaption.Value - - - - - - - Textbox68 - - - - - - - - true - true - - - - - =Fields!BaseUnitOfMeasureCaption.Value - - - - - - - Textbox69 - - - 2 - - - - - - - true - true - - - - - =Fields!UnitCostCaption.Value - - - - - - - Textbox70 - - - - - - - - true - true - - - - - =Fields!TotalCost1Caption.Value - - - - - - - Textbox71 - - - - - - - - true - true - - - - - - - - - - - - Textbox72 - - - - - - - - true - true - - - - - - - - - - - - Textbox73 - - - - - - - - true - true - - - - - - - - - - - - Textbox74 - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox75 - - - 12 - - - - - - - - - - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox77 - - - - - - Bottom - 5pt - - - 12 - - - - - - - - - - - - - - - - - 0.35278cm - - - - - true - true - - - - - =Fields!OperationNo_RtngLine.Value - - - - - - - Textbox78 - - - 2 - - - - - - - true - true - - - - - =Fields!Type_RtngLine.Value - - - - - - - Textbox79 - - - - - - - - true - true - - - - - =Fields!No_RtngLine.Value - - - - - - - Textbox80 - - - - - - - - true - true - - - - - =Fields!Description_RtngLine.Value - - - - - - - Textbox81 - - - 2 - - - - - - - true - true - - - - - =Fields!SetupTime_RtngLine.Value - - - - - - - Textbox82 - - - - - - - - true - true - - - - - =Fields!RunTime_RtngLine.Value - - - - - - - Textbox83 - - - - - - - - true - true - - - - - =Fields!CostTime.Value - - - - - - - Textbox84 - - - - - - - - true - true - - - - - =Fields!ProdUnitCost.Value - - - - - - - Textbox85 - - - - - - - - true - true - - - - - =Fields!ProdTotalCost.Value - - - - - - - Textbox86 - - - 2 - - - - - - - 0.35278cm - - - - - true - true - - - - - =Fields!ProdBOMLineLevelType.Value - - - - - - - Textbox87 - - - - - - - - true - true - - - - - =Fields!ProdBOMLineLevelNo.Value - - - - - - - Textbox88 - - - - - - - - true - true - - - - - =Fields!ProdBOMLineLevelDesc.Value - - - - - - - Textbox89 - - - 2 - - - - - - - true - true - - - - - =Fields!ProdBOMLineLevelQuantity.Value - - - - - - - Textbox90 - - - - - - - - true - true - - - - - =Fields!CompItemBaseUOM.Value - - - - - - - Textbox91 - - - 2 - - - - - - - true - true - - - - - =Fields!CompItemUnitCost.Value - - - - - - - Textbox92 - - - - - - - - true - true - - - - - =Fields!CostTotal.Value - - - - - - - Textbox93 - - - - - - - - true - true - - - - - - - - - - - - Textbox94 - - - - - - - - true - true - - - - - - - - - - - - Textbox95 - - - - - - - - true - true - - - - - - - - - - - - Textbox96 - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox97 - - - 12 - - - - - - - - - - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox98 - - - 8 - - - - - - - - - - - - - true - true - - - - - - - - - - - - Textbox99 - - - - - - Top - 5pt - - - - - - - - true - true - - - - - - - - - - - - Textbox100 - - - 3 - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - - - - Textbox101 - - - - - - - - true - true - - - - - - - - - - - - Textbox102 - - - - - - - - true - true - - - - - - - - - - - - Textbox103 - - - - - - - - true - true - - - - - - - - - - - - Textbox104 - - - - - - - - true - true - - - - - - - - - - - - Textbox105 - - - - - - - - true - true - - - - - =Fields!TotalCost1Caption.Value - - - - - - - Textbox106 - - - 3 - - - - - - - - true - true - - - - - =Sum(Fields!CostTotal.Value) - - - - - - - Textbox107 - - - - - - - - true - true - - - - - - - - - Textbox108 - - - - - - - - true - true - - - - - - - - - - - - Textbox109 - - - - - - - - true - true - - - - - - - - - - - - Textbox110 - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox111 - - - - - - - - true - true - - - - - - - - - - - - Textbox112 - - - - - - - - true - true - - - - - - - - - - - - Textbox113 - - - - - - - - true - true - - - - - - - - - - - - Textbox114 - - - - - - - - true - true - - - - - - - - - - - - Textbox115 - - - - - - - - true - true - - - - - - - - - - - - - - - - true - true - - - - - - - - - - - - Textbox117 - - - - - - - - true - true - - - - - - - - - - - - Textbox118 - - - - - - - - true - true - - - - - - - - - - - - Textbox119 - - - - - - - - true - true - - - - - - - - - - - - Textbox120 - - - - - - - - true - true - - - - - - - - - - - - Textbox121 - - - - - - - - true - true - - - - - - - - - - - - Textbox122 - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox123 - - - 11 - - - - - - - - - - - - - - - - true - true - - - - - - - - - - - - Textbox124 - - - - - - Top - 5pt - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - Textbox125 - - - 7 - - - - - - - - - - - - true - true - - - - - =First(Fields!TotalCostCaption.Value) - - - - - - - Textbox126 - - - 4 - - - - - - - - - true - true - - - - - =Sum(Fields!ProdTotalCost.Value) - - - - - - - Textbox127 - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - Textbox128 - - - 7 - - - - - - - - - - - - true - true - - - - - - - - - Textbox129 - - - - - - - - true - true - - - - - - - - - Textbox130 - - - - - - - - true - true - - - - - - - - - Textbox131 - - - - - - - - true - true - - - - - - - - - Textbox132 - - - - - - - - true - true - - - - - - - - - - - - Textbox133 - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - - - - Textbox134 - - - 7 - - - - - - - - - - - - true - true - - - - - =Last(Fields!CostOfProductionCaption.Value) - - - - - - - Textbox135 - - - 4 - - - - - - - - - true - true - - - - - =Sum(Fields!FooterProdTotalCost.Value) - - - - - - - Textbox136 - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - - - - Textbox137 - - - 7 - - - - - - - - - - - - true - true - - - - - =Last(Fields!CostOfComponentsCaption.Value) - - - - - - - Textbox138 - - - 4 - - - - - - - - - true - true - - - - - =Sum(Fields!FooterCostTotal.Value) - - - - - - - Textbox139 - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - - - - Textbox140 - - - 7 - - - - - - - - - - - - true - true - - - - - =Last(Fields!SingleLevelMfgOverheadCostCaption.Value) - - - - - - - Textbox141 - - - 4 - - - - - - - - - true - true - - - - - =Sum(Fields!SingleLevelMfgOvhd.Value) - - - - - - - Textbox142 - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox143 - - - 7 - - - - - - - - - - - - true - true - - - - - - - - - Textbox144 - - - - - - - - true - true - - - - - - - - - - - - Textbox145 - - - - - - - - true - true - - - - - - - - - Textbox146 - - - - - - - - true - true - - - - - - - - - Textbox147 - - - - - - - - true - true - - - - - - - - - - - - Textbox148 - - - - - - - - 0.17638cm - - - - - true - true - - - - - - - - - - - - Textbox149 - - - 11 - - - - - - - - - - - - - - - - true - true - - - - - - - - - - - - Textbox150 - - - - - - Top - 5pt - - - - - - - - 0.35278cm - - - - - true - true - - - - - - - - - - - - Textbox151 - - - 7 - - - - - - - - - - - - true - true - - - - - - - - - Textbox152 - - - - - - - - true - true - - - - - =Last(Fields!UnitCostCaption.Value) - - - - - - - Textbox153 - - - 3 - - - - - - - - true - true - - - - - =Sum(Fields!UnitCost_Item.Value) - - - - - - - Textbox154 - - - - - - - - - - - - - - - - - - - - - - - - - - - - - =iif(Fields!ItemFilter.Value = "",true,false) - - After - true - - - - =iif(Fields!ItemFilter.Value = "",true,false) - - After - true - - - - - =Fields!No_Item.Value - - - End - - - - - After - true - - - After - true - - - After - true - - - After - true - - - After - true - - - After - true - - - - - =Fields!InRouting.Value - - - - - - =iif(Fields!InRouting.Value,false,true) - - After - true - - - - =iif(Fields!InRouting.Value,false,true) - - After - true - - - - =iif(Fields!InRouting.Value,false,true) - - After - - - - - =Fields!InBOM.Value - - - - - - =iif(Fields!InBOM.Value,false,true) - - After - true - - - - =iif(Fields!InBOM.Value,false,true) - - After - true - - - - =iif(Fields!InBOM.Value,false,true) - - After - - - - Detail - - - - - =iif(Fields!OperationNo_RtngLine.Value = "",true,false) - - - - - =iif(Fields!ShowLine.Value,false,true) - - - - Detail_Collection - Output - true - - - - =iif(Fields!TotalCost1Caption.Value = "",true,false) - - Before - - - - =iif(Fields!TotalCost1Caption.Value = "",true,false) - - Before - - - - =iif(Fields!TotalCost1Caption.Value = "",true,false) - - Before - true - - - - =iif(Fields!TotalCostCaption.Value = "",true,false) - - Before - true - - - - =iif(Fields!TotalCostCaption.Value = "",true,false) - - Before - - - - - - =iif(First(Fields!TotalCostCaption.Value) = "",true,false) - - Before - true - - - - =iif(First(Fields!TotalCostCaption.Value) = "",true,false) - - Before - true - - - - - Before - true - - - Before - true - - - Before - true - - - Before - true - - - Before - - - Before - true - - - - - - DataSet_Result - 8.52024cm - 18.15273cm - - - - - - - true - - - - - =Fields!PageNoCaption.Value - - - - - - - 0.3595cm - 12.57817cm - 11pt - 5.10758cm - 1 - - - - true - - - - - =Fields!CalculateDate.Value - - - - - - - true - - - - - =Fields!CompanyName.Value - - - - - - - true - - - - - =Fields!TodayFormatted.Value - - - - - - - 12.50762cm - 11pt - 5.64511cm - 4 - - - - true - true - - - - - =User!UserID - - - - - - - 0.75075cm - 14.41376cm - 11pt - 3.73896cm - 5 - - =iif(Fields!DetailedCalculationCaption.Value = "",true,false) - - NoOutput - - - - - - - - 29.7cm - 21cm - 1.76389cm - 1.05833cm - 1.05833cm - 1.48167cm - 1.27cm - + + + + + + Textbox1 + + + 12 + + + + + + + + + + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + + + + Textbox2 + + + + + + + + true + true + + + + + + + + + + + + Textbox3 + + + + + + + + true + true + + + + + + + + + + + + Textbox4 + + + + + + + + true + true + + + + + + + + + + + + Textbox5 + + + + + + + + true + true + + + + + + + + + + + + Textbox6 + + + + + + + + true + true + + + + + + + + + + + + + + + + true + true + + + + + + + + + + + + Textbox8 + + + + + + + + true + true + + + + + + + + + + + + Textbox9 + + + + + + + + true + true + + + + + + + + + + + + Textbox10 + + + + + + + + true + true + + + + + + + + + + + + Textbox11 + + + + + + + + true + true + + + + + + + + + + + + Textbox12 + + + + + + + + true + true + + + + + + + + + + + + Textbox13 + + + + + + + + 0.35278cm + + + + + true + true + + + + + =Parameters!No_ItemCaption.Value + + + + + + + Textbox14 + + + 2 + + + + + + + true + true + + + + + + + + + + + + Textbox15 + + + + + + + + true + true + + + + + =Fields!No_Item.Value + + + + + + + Textbox16 + + + Bottom + 5pt + 5pt + + + 5 + + + + + + + + + + true + true + + + + + + + + + + + + Textbox17 + + + + + + + + true + true + + + + + + + + + + + + Textbox18 + + + + + + + + true + true + + + + + + + + + + + + Textbox19 + + + + + + + + true + true + + + + + + + + + + + + Textbox20 + + + + + + + + 0.4064cm + + + + + true + true + + + + + =Parameters!Description_ItemCaption.Value + + + + + + + Textbox21 + + + 3 + + + + + + + + true + true + + + + + =Fields!Description_Item.Value + + + + + + + Textbox22 + + + 6 + + + + + + + + + + + true + true + + + + + + + + + + + + Textbox23 + + + + + + + + true + true + + + + + + + + + + + + Textbox24 + + + + + + + + true + true + + + + + + + + + + + + Textbox25 + + + + + + + + 0.35278cm + + + + + true + true + + + + + =Parameters!ProductionBOMNo_ItemCaption.Value + + + + + + + Textbox26 + + + 3 + + + + + + + + true + true + + + + + =Fields!ProductionBOMNo_Item.Value + + + + + + + Textbox27 + + + 2 + + + + + + + true + true + + + + + =Fields!PBOMVersionCode1.Value + + + + + + + Textbox28 + + + 2 + + + + + + + true + true + + + + + + + + + + + + Textbox29 + + + + + + + + true + true + + + + + + + + + + + + Textbox30 + + + + + + + + true + true + + + + + + + + + + + + Textbox32 + + + + + + + + true + true + + + + + + + + + + + + Textbox34 + + + + + + + + true + true + + + + + + + + + + + + Textbox35 + + + + + + + + 0.35278cm + + + + + true + true + + + + + =Parameters!LotSize_ItemCaption.Value + + + + + + + Textbox36 + + + 3 + + + + + + + + true + true + + + + + =Fields!LotSize_Item.Value + + + + + + + Textbox37 + + + 2 + + + + + + + true + true + + + + + =Fields!BaseUnitOfMeasure_Item.Value + + + + + + 2 + + + + + + + true + true + + + + + + + + + + + + + + + + true + true + + + + + + + + + + + + Textbox40 + + + + + + + + true + true + + + + + + + + + + + + Textbox41 + + + + + + + + true + true + + + + + + + + + + + + Textbox42 + + + + + + + + true + true + + + + + + + + + + + + Textbox43 + + + + + + + + 0.35278cm + + + + + true + true + + + + + =Parameters!RoutingNo_ItemCaption.Value + + + + + + + Textbox44 + + + 3 + + + + + + + + true + true + + + + + =Fields!RoutingNo_Item.Value + + + + + + + Textbox45 + + + 2 + + + + + + + true + true + + + + + =Fields!RtngVersionCode.Value + + + + + + + Textbox46 + + + 2 + + + + + + + true + true + + + + + + + + + + + + Textbox47 + + + + + + + + true + true + + + + + + + + + + + + Textbox48 + + + + + + + + true + true + + + + + + + + + + + + Textbox49 + + + + + + + + true + true + + + + + + + + + + + + Textbox50 + + + + + + + + true + true + + + + + + + + + + + + Textbox51 + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + + + + Textbox52 + + + 12 + + + + + + + + + + + + + + + + + 0.35278cm + + + + + true + true + + + + + =Parameters!OperationNo_RtngLineCaption.Value + + + + + + + Textbox53 + + + 2 + + + + + + + true + true + + + + + =Parameters!Type_RtngLineCaption.Value + + + + + + + Textbox54 + + + + + + + + true + true + + + + + =Parameters!No_RtngLineCaption.Value + + + + + + + Textbox55 + + + + + + + + true + true + + + + + =Parameters!Description_ItemCaption.Value + + + + + + + Textbox56 + + + 2 + + + + + + + true + true + + + + + =Parameters!SetupTime_RtngLineCaption.Value + + + + + + + Textbox57 + + + + + + + + true + true + + + + + =Parameters!RunTime_RtngLineCaption.Value + + + + + + + Textbox58 + + + + + + + + true + true + + + + + =Fields!CostTimeCaption.Value + + + + + + + Textbox59 + + + + + + + + true + true + + + + + =Fields!UnitCostCaption.Value + + + + + + + Textbox60 + + + + + + + + true + true + + + + + =Fields!TotalCostCaption.Value + + + + + + + Textbox62 + + + 2 + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox63 + + + 12 + + + + + + + + + + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox64 + + + + + + Bottom + 5pt + + + 12 + + + + + + + + + + + + + + + + + 0.35278cm + + + + + true + true + + + + + =Fields!TypeCaption.Value + + + + + + + Textbox65 + + + + + + + + true + true + + + + + =Fields!NoCaption.Value + + + + + + + Textbox66 + + + + + + + + true + true + + + + + =Fields!DescriptionCaption.Value + + + + + + + Textbox67 + + + 2 + + + + + + + true + true + + + + + =Fields!QuantityCaption.Value + + + + + + + Textbox68 + + + + + + + + true + true + + + + + =Fields!BaseUnitOfMeasureCaption.Value + + + + + + + Textbox69 + + + 2 + + + + + + + true + true + + + + + =Fields!UnitCostCaption.Value + + + + + + + Textbox70 + + + + + + + + true + true + + + + + =Fields!TotalCost1Caption.Value + + + + + + + Textbox71 + + + + + + + + true + true + + + + + + + + + + + + Textbox72 + + + + + + + + true + true + + + + + + + + + + + + Textbox73 + + + + + + + + true + true + + + + + + + + + + + + Textbox74 + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox75 + + + 12 + + + + + + + + + + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox77 + + + + + + Bottom + 5pt + + + 12 + + + + + + + + + + + + + + + + + 0.35278cm + + + + + true + true + + + + + =Fields!OperationNo_RtngLine.Value + + + + + + + Textbox78 + + + 2 + + + + + + + true + true + + + + + =Fields!Type_RtngLine.Value + + + + + + + Textbox79 + + + + + + + + true + true + + + + + =Fields!No_RtngLine.Value + + + + + + + Textbox80 + + + + + + + + true + true + + + + + =Fields!Description_RtngLine.Value + + + + + + + Textbox81 + + + 2 + + + + + + + true + true + + + + + =Fields!SetupTime_RtngLine.Value + + + + + + + Textbox82 + + + + + + + + true + true + + + + + =Fields!RunTime_RtngLine.Value + + + + + + + Textbox83 + + + + + + + + true + true + + + + + =Fields!CostTime.Value + + + + + + + Textbox84 + + + + + + + + true + true + + + + + =Fields!ProdUnitCost.Value + + + + + + + Textbox85 + + + + + + + + true + true + + + + + =Fields!ProdTotalCost.Value + + + + + + + Textbox86 + + + 2 + + + + + + + 0.35278cm + + + + + true + true + + + + + =Fields!ProdBOMLineLevelType.Value + + + + + + + Textbox87 + + + + + + + + true + true + + + + + =Fields!ProdBOMLineLevelNo.Value + + + + + + + Textbox88 + + + + + + + + true + true + + + + + =Fields!ProdBOMLineLevelDesc.Value + + + + + + + Textbox89 + + + 2 + + + + + + + true + true + + + + + =Fields!ProdBOMLineLevelQuantity.Value + + + + + + + Textbox90 + + + + + + + + true + true + + + + + =Fields!CompItemBaseUOM.Value + + + + + + + Textbox91 + + + 2 + + + + + + + true + true + + + + + =Fields!CompItemUnitCost.Value + + + + + + + Textbox92 + + + + + + + + true + true + + + + + =Fields!CostTotal.Value + + + + + + + Textbox93 + + + + + + + + true + true + + + + + + + + + + + + Textbox94 + + + + + + + + true + true + + + + + + + + + + + + Textbox95 + + + + + + + + true + true + + + + + + + + + + + + Textbox96 + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox97 + + + 12 + + + + + + + + + + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox98 + + + 8 + + + + + + + + + + + + + true + true + + + + + + + + + + + + Textbox99 + + + + + + Top + 5pt + + + + + + + + true + true + + + + + + + + + + + + Textbox100 + + + 3 + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + + + + Textbox101 + + + + + + + + true + true + + + + + + + + + + + + Textbox102 + + + + + + + + true + true + + + + + + + + + + + + Textbox103 + + + + + + + + true + true + + + + + + + + + + + + Textbox104 + + + + + + + + true + true + + + + + + + + + + + + Textbox105 + + + + + + + + true + true + + + + + =Fields!TotalCost1Caption.Value + + + + + + + Textbox106 + + + 3 + + + + + + + + true + true + + + + + =Sum(Fields!CostTotal.Value) + + + + + + + Textbox107 + + + + + + + + true + true + + + + + + + + + Textbox108 + + + + + + + + true + true + + + + + + + + + + + + Textbox109 + + + + + + + + true + true + + + + + + + + + + + + Textbox110 + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox111 + + + + + + + + true + true + + + + + + + + + + + + Textbox112 + + + + + + + + true + true + + + + + + + + + + + + Textbox113 + + + + + + + + true + true + + + + + + + + + + + + Textbox114 + + + + + + + + true + true + + + + + + + + + + + + Textbox115 + + + + + + + + true + true + + + + + + + + + + + + + + + + true + true + + + + + + + + + + + + Textbox117 + + + + + + + + true + true + + + + + + + + + + + + Textbox118 + + + + + + + + true + true + + + + + + + + + + + + Textbox119 + + + + + + + + true + true + + + + + + + + + + + + Textbox120 + + + + + + + + true + true + + + + + + + + + + + + Textbox121 + + + + + + + + true + true + + + + + + + + + + + + Textbox122 + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox123 + + + 11 + + + + + + + + + + + + + + + + true + true + + + + + + + + + + + + Textbox124 + + + + + + Top + 5pt + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + Textbox125 + + + 7 + + + + + + + + + + + + true + true + + + + + =First(Fields!TotalCostCaption.Value) + + + + + + + Textbox126 + + + 4 + + + + + + + + + true + true + + + + + =Sum(Fields!ProdTotalCost.Value) + + + + + + + Textbox127 + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + Textbox128 + + + 7 + + + + + + + + + + + + true + true + + + + + + + + + Textbox129 + + + + + + + + true + true + + + + + + + + + Textbox130 + + + + + + + + true + true + + + + + + + + + Textbox131 + + + + + + + + true + true + + + + + + + + + Textbox132 + + + + + + + + true + true + + + + + + + + + + + + Textbox133 + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + + + + Textbox134 + + + 7 + + + + + + + + + + + + true + true + + + + + =Last(Fields!CostOfProductionCaption.Value) + + + + + + + Textbox135 + + + 4 + + + + + + + + + true + true + + + + + =Sum(Fields!FooterProdTotalCost.Value) + + + + + + + Textbox136 + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + + + + Textbox137 + + + 7 + + + + + + + + + + + + true + true + + + + + =Last(Fields!CostOfComponentsCaption.Value) + + + + + + + Textbox138 + + + 4 + + + + + + + + + true + true + + + + + =Sum(Fields!FooterCostTotal.Value) + + + + + + + Textbox139 + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + + + + Textbox140 + + + 7 + + + + + + + + + + + + true + true + + + + + =Last(Fields!SingleLevelMfgOverheadCostCaption.Value) + + + + + + + Textbox141 + + + 4 + + + + + + + + + true + true + + + + + =Sum(Fields!SingleLevelMfgOvhd.Value) + + + + + + + Textbox142 + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox143 + + + 7 + + + + + + + + + + + + true + true + + + + + + + + + Textbox144 + + + + + + + + true + true + + + + + + + + + + + + Textbox145 + + + + + + + + true + true + + + + + + + + + Textbox146 + + + + + + + + true + true + + + + + + + + + Textbox147 + + + + + + + + true + true + + + + + + + + + + + + Textbox148 + + + + + + + + 0.17638cm + + + + + true + true + + + + + + + + + + + + Textbox149 + + + 11 + + + + + + + + + + + + + + + + true + true + + + + + + + + + + + + Textbox150 + + + + + + Top + 5pt + + + + + + + + 0.35278cm + + + + + true + true + + + + + + + + + + + + Textbox151 + + + 7 + + + + + + + + + + + + true + true + + + + + + + + + Textbox152 + + + + + + + + true + true + + + + + =Last(Fields!UnitCostCaption.Value) + + + + + + + Textbox153 + + + 3 + + + + + + + + true + true + + + + + =Sum(Fields!UnitCost_Item.Value) + + + + + + + Textbox154 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + =iif(Fields!ItemFilter.Value = "",true,false) + + After + true + + + + =iif(Fields!ItemFilter.Value = "",true,false) + + After + true + + + + + =Fields!No_Item.Value + + + End + + + + + After + true + + + After + true + + + After + true + + + After + true + + + After + true + + + After + true + + + + + =Fields!InRouting.Value + + + + + + =iif(Fields!InRouting.Value,false,true) + + After + true + + + + =iif(Fields!InRouting.Value,false,true) + + After + true + + + + =iif(Fields!InRouting.Value,false,true) + + After + + + + + =Fields!InBOM.Value + + + + + + =iif(Fields!InBOM.Value,false,true) + + After + true + + + + =iif(Fields!InBOM.Value,false,true) + + After + true + + + + =iif(Fields!InBOM.Value,false,true) + + After + + + + Detail + + + + + =iif(Fields!OperationNo_RtngLine.Value = "",true,false) + + + + + =iif(Fields!ShowLine.Value,false,true) + + + + Detail_Collection + Output + true + + + + =iif(Fields!TotalCost1Caption.Value = "",true,false) + + Before + + + + =iif(Fields!TotalCost1Caption.Value = "",true,false) + + Before + + + + =iif(Fields!TotalCost1Caption.Value = "",true,false) + + Before + true + + + + =iif(Fields!TotalCostCaption.Value = "",true,false) + + Before + true + + + + =iif(Fields!TotalCostCaption.Value = "",true,false) + + Before + + + + + + =iif(First(Fields!TotalCostCaption.Value) = "",true,false) + + Before + true + + + + =iif(First(Fields!TotalCostCaption.Value) = "",true,false) + + Before + true + + + + + Before + true + + + Before + true + + + Before + true + + + Before + true + + + Before + + + Before + true + + + + + + DataSet_Result + 8.52024cm + 18.15273cm + + + + + + + true + + + + + =Fields!PageNoCaption.Value + + + + + + + 0.3595cm + 12.57817cm + 11pt + 5.10758cm + 1 + + + + true + + + + + =Fields!CalculateDate.Value + + + + + + + true + + + + + =Fields!CompanyName.Value + + + + + + + true + + + + + =Fields!TodayFormatted.Value + + + + + + + 12.50762cm + 11pt + 5.64511cm + 4 + + + + true + true + + + + + =User!UserID + + + + + + + 0.75075cm + 14.41376cm + 11pt + 3.73896cm + 5 + + =iif(Fields!DetailedCalculationCaption.Value = "",true,false) + + NoOutput + + + + + + + + 29.7cm + 21cm + 1.76389cm + 1.05833cm + 1.05833cm + 1.48167cm + 1.27cm +