diff --git a/.clang-tidy b/.clang-tidy new file mode 100644 index 0000000000..9f0bcf4fa1 --- /dev/null +++ b/.clang-tidy @@ -0,0 +1,28 @@ +# Engine track E1 - static-analysis baseline. The point is bug-finding (the +# "rewrite oracle"), not style: clang-analyzer-* (the path-sensitive analyzer - +# null derefs, leaks, uninitialised reads) + the high-signal bugprone-* checks. +# Style/readability/modernize families are deliberately OFF to keep the signal +# high on a large, mature C++23 codebase. Two bugprone checks are also off as +# high-volume/low-signal here: unhandled-self-assignment (fires on every operator= +# lacking a self-check - defensive, not a bug) and exception-escape (legacy +# destructors). The gate is advisory (engine-clang-tidy.yml) and reports counts; +# it tightens to "no NEW findings" once the baseline is triaged. +Checks: > + -*, + clang-analyzer-*, + bugprone-*, + -clang-analyzer-optin.*, + -clang-analyzer-cplusplus.NewDeleteLeaks, + -bugprone-easily-swappable-parameters, + -bugprone-narrowing-conversions, + -bugprone-branch-clone, + -bugprone-reserved-identifier, + -bugprone-implicit-widening-of-multiplication-result, + -bugprone-unhandled-self-assignment, + -bugprone-exception-escape + +# Engine headers are ours; third-party (Python.h, xerces) is excluded by the +# regex so we only flag frePPLe code. +HeaderFilterRegex: 'frepple/.*\.h$' +WarningsAsErrors: '' +FormatStyle: none diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000..9c646b9a9d --- /dev/null +++ b/.dockerignore @@ -0,0 +1,17 @@ +# Keep the E2E build context lean and host-artifact-free. +.git +venv +build +logs +node_modules +frontend/node_modules +frontend/.next +# Rust build dirs are host-arch artifacts; a leaked target/ shadows the +# in-image cargo build (CMake sees the OUTPUT present and skips it), linking +# a wrong-arch/index-less staticlib. Force a clean cargo build inside the image. +**/target +**/__pycache__ +*.pyc +bin/frepple +bin/libfrepple.* +.DS_Store diff --git a/.github/workflows/deploy-staging.yml b/.github/workflows/deploy-staging.yml new file mode 100644 index 0000000000..965ab4ec02 --- /dev/null +++ b/.github/workflows/deploy-staging.yml @@ -0,0 +1,77 @@ +name: Build staging images + +# Builds the modernization images on the ledoent x86 ARC runners and pushes them +# to GHCR, where the Helm chart (deploy/helm/frepple) pulls them for the +# frepple-staging review env on the hetzner-ledo cluster. +# +# ghcr.io/ledoent/frepple-app: (engine + Django; e2e/Dockerfile.engine) +# ghcr.io/ledoent/frepple-frontend: (Next.js SPA; e2e/Dockerfile.frontend) +# +# The runner is x86 (matching the cluster) and has a docker:dind sidecar, so a +# plain `docker build` produces native amd64 images without buildx/QEMU emulation. + +on: + workflow_dispatch: + push: + branches: + - "modernization" + paths: + - "e2e/Dockerfile.engine" + - "e2e/Dockerfile.frontend" + - "e2e/entrypoint.sh" + - "src/**" + - "freppledb/**" + - "frontend/**" + - ".github/workflows/deploy-staging.yml" + +permissions: + contents: read + packages: write + +env: + REGISTRY: ghcr.io/ledoent + +jobs: + build: + name: Build & push ${{ matrix.image }} + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - image: frepple-app + dockerfile: e2e/Dockerfile.engine + # Phase 7: staging runs the Rust forecast methods (proven 5/5 + # byte-parity in the forecast-phase7 gate). Default-OFF elsewhere. + rust: "ON" + - image: frepple-frontend + dockerfile: e2e/Dockerfile.frontend + steps: + - uses: actions/checkout@v4 + + - name: Set up Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to GHCR + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # buildx + a GHCR registry cache: the engine's C++ build layer is restored + # from :buildcache, so a Python/frontend-only change skips the ~4-min + # recompile (the dind runner has no persistent local layer cache). + - name: Build & push + uses: docker/build-push-action@v6 + with: + context: . + file: ${{ matrix.dockerfile }} + push: true + build-args: | + FREPPLE_RUST_FORECAST=${{ matrix.rust || 'OFF' }} + tags: | + ${{ env.REGISTRY }}/${{ matrix.image }}:${{ github.sha }} + ${{ env.REGISTRY }}/${{ matrix.image }}:latest + cache-from: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache + cache-to: type=registry,ref=${{ env.REGISTRY }}/${{ matrix.image }}:buildcache,mode=max diff --git a/.github/workflows/engine-asan.yml b/.github/workflows/engine-asan.yml new file mode 100644 index 0000000000..313d9e7660 --- /dev/null +++ b/.github/workflows/engine-asan.yml @@ -0,0 +1,83 @@ +name: Engine ASan + +# Debug build with AddressSanitizer over the C++ engine golden-test suite. +# Catches memory errors (buffer overflows, use-after-free) that the Release CI +# can't see. Deterministically flags the weight[] out-of-bounds read via the +# test/forecast_11 long-history scenario. +# +# BLOCKING: the golden suite is ASan-clean (the 8 crashes rooted in the +# Calendar::EventIterator operator-- UB are fixed). A new memory error now fails CI. +# +# Path-filtered so doc/CI/gate-only commits don't trigger the heavy build. + +on: + push: + branches: + - "modernization" + - "modernization/**" + paths: + - "src/**" + - "include/**" + - "test/**" + - "CMakeLists.txt" + - ".github/workflows/engine-asan.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-asan: + name: Debug + ASan engine tests + runs-on: ubuntu-24.04 + # Blocking: the golden suite is ASan-clean (the 8 crashes from the + # Calendar::EventIterator UB are fixed). Future memory regressions fail CI. + steps: + - name: Checking out source code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Set up Nodes + uses: actions/setup-node@v6 + + - name: Install packages + run: | + sudo apt update + sudo apt install --no-upgrade libxerces-c-dev openssl libssl-dev libpq5 libpq-dev postgresql-client python3 python3-dev python3-venv python3-setuptools + sudo npm -g install pnpm@latest-10 + pnpm install --frozen-lockfile + + - name: Build (Debug + AddressSanitizer) + # CMAKE_BUILD_TYPE=Debug injects -fsanitize=address (CMakeLists.txt:88). + run: | + git submodule update --remote --checkout --force + grunt + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Debug + cmake --build ${{github.workspace}}/build --config Debug -- -j 4 + . ${{github.workspace}}/venv/bin/activate + echo "PATH=$PATH" >> $GITHUB_ENV + echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV + + - name: Run engine golden tests under ASan + working-directory: ${{github.workspace}} + # detect_leaks=0: focus on memory-error detection (buffer overflows / + # use-after-free). The forecast_11 scenario forces the weight[] index + # above MAXBUCKETS, so a pre-fix binary aborts here with a + # global-buffer-overflow; post-fix it runs clean. + run: | + export FREPPLE_DATE_STYLE="day-month-year" + export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1" + # Exclude the stress scenario (test/stress_1): it solves ~24k operationplans, + # which under a Debug+ASan build is ~1000x slower (allocation-heavy at scale, + # ~24 min) than Release. It is a perf/scale gate, not a memory-safety one - it + # runs in the optimised ubuntu24 (Release) suite instead. + ./test/runtest.py -e stress_1 + + - name: Keep logs + uses: actions/upload-artifact@v6 + if: always() + with: + name: asan_logs + path: logs + retention-days: 3 diff --git a/.github/workflows/engine-clang-tidy.yml b/.github/workflows/engine-clang-tidy.yml new file mode 100644 index 0000000000..eab34b3504 --- /dev/null +++ b/.github/workflows/engine-clang-tidy.yml @@ -0,0 +1,99 @@ +name: Engine clang-tidy + +# Static-analysis baseline over the C++ engine (Engine track E1). The check set +# (.clang-tidy) is the bug-finders only - clang-analyzer-* + high-signal +# bugprone-* - not style. clang-tidy is parse-only (no codegen), so this is +# lighter than the sanitizer Debug builds. +# +# ADVISORY: reports the finding count + breakdown in the job step-summary but +# never fails the job (the baseline isn't zero on a large legacy codebase). It +# tightens to "no NEW findings on changed files" (clang-tidy-diff) once the +# baseline is triaged - tracked as E2. See tools/modernization/ubsan-baseline.md +# for the sibling sanitizer gates. +# +# Path-filtered so doc/gate-only commits don't trigger it. + +on: + push: + branches: + - "modernization" + - "modernization/**" + paths: + - "src/**" + - "include/**" + - ".clang-tidy" + - ".github/workflows/engine-clang-tidy.yml" + pull_request: + paths: + - "src/**" + - "include/**" + - ".clang-tidy" + - ".github/workflows/engine-clang-tidy.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + clang-tidy: + name: clang-tidy (advisory) + runs-on: ubuntu-24.04 + steps: + - name: Checking out source code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Install packages + run: | + sudo apt update + sudo apt install --no-upgrade libxerces-c-dev openssl libssl-dev libpq-dev python3 python3-dev python3-venv clang-tidy + clang-tidy --version + + - name: Configure (export compile_commands.json) + # No build needed - clang-tidy parses TUs from the compile DB. Configure + # still generates config.h (referenced by the engine headers). + run: | + cmake -B build -DCMAKE_BUILD_TYPE=Release -DCMAKE_EXPORT_COMPILE_COMMANDS=ON + + - name: Run clang-tidy over the engine (advisory) + # Parallel over the src/ TUs, each writing to its OWN log so concurrent + # writes can't interleave; then concatenate. Uses only `clang-tidy` + # (run-clang-tidy isn't always on PATH). Never fails the job - baseline + # capture only. + run: | + mkdir -p .tidy-logs + find src -name '*.cpp' -print0 \ + | xargs -0 -P"$(nproc)" -I{} sh -c \ + 'clang-tidy -p build "$1" > ".tidy-logs/$(echo "$1" | tr "/." "__").log" 2>/dev/null || true' _ {} + cat .tidy-logs/*.log > clang-tidy.out 2>/dev/null || true + echo "clang-tidy run complete over $(find src -name '*.cpp' | wc -l) TUs" + + - name: Summarise findings + if: always() + # A header finding is reported once per including TU; the warning line + # (path:line:col + check) is byte-identical across them, so `sort -u` + # collapses them to DISTINCT findings. The raw line count is shown too. + run: | + tidy='\[(clang-analyzer|bugprone)[a-zA-Z.-]+\]' + distinct=$(grep -hE ": warning: .*${tidy}" clang-tidy.out 2>/dev/null | sort -u | wc -l | tr -d ' ') + raw=$(grep -hcE ": warning: .*${tidy}" clang-tidy.out 2>/dev/null || echo 0) + { + echo "## clang-tidy baseline" + echo "distinct findings: **${distinct}** (raw lines incl. header re-includes: ${raw})" + echo '' + echo 'By check (distinct):' + echo '```' + grep -hE ": warning: .*${tidy}" clang-tidy.out 2>/dev/null | sort -u \ + | grep -oE "${tidy}" | sort | uniq -c | sort -rn \ + || echo "(no findings)" + echo '```' + } >> "$GITHUB_STEP_SUMMARY" + + - name: Keep report + uses: actions/upload-artifact@v6 + if: always() + with: + name: clang-tidy-report + path: clang-tidy.out + retention-days: 7 diff --git a/.github/workflows/engine-ubsan.yml b/.github/workflows/engine-ubsan.yml new file mode 100644 index 0000000000..b75a17d3c6 --- /dev/null +++ b/.github/workflows/engine-ubsan.yml @@ -0,0 +1,104 @@ +name: Engine UBSan + +# Debug build with UndefinedBehaviorSanitizer over the C++ engine golden-test +# suite. Complements engine-asan.yml: ASan catches memory errors (overflows, +# use-after-free); UBSan catches undefined behaviour the optimiser is free to +# miscompile - signed-integer overflow, invalid shifts, misaligned/null pointer +# use, out-of-range float->int casts (e.g. the static_cast(NaN) class), and +# bad enum/bool loads. Same golden scenarios, a different sanitizer lens. +# +# Sanitizer selected via -DFREPPLE_SANITIZER=undefined (CMakeLists.txt). The +# -fsanitize=vptr check is excluded there (frePPLe's hand-rolled MetaClass RTTI +# is incompatible with it - see tools/modernization/ubsan-baseline.md). +# +# BLOCKING: the golden suite is UBSan-clean. The baseline findings were resolved +# (operationdependency null member-call fixed; the iterator operator* null-binding +# idiom marked FREPPLE_NO_SANITIZE_NULL), so halt_on_error=1 + abort makes a NEW +# UB site fail CI - same contract as engine-asan. +# +# Path-filtered so doc/CI/gate-only commits don't trigger the heavy Debug build. + +on: + push: + branches: + - "modernization" + - "modernization/**" + paths: + - "src/**" + - "include/**" + - "test/**" + - "CMakeLists.txt" + - ".github/workflows/engine-ubsan.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + build-ubsan: + name: Debug + UBSan engine tests + runs-on: ubuntu-24.04 + steps: + - name: Checking out source code + uses: actions/checkout@v6 + with: + submodules: recursive + + - name: Set up Nodes + uses: actions/setup-node@v6 + + - name: Install packages + run: | + sudo apt update + sudo apt install --no-upgrade libxerces-c-dev openssl libssl-dev libpq5 libpq-dev postgresql-client python3 python3-dev python3-venv python3-setuptools + sudo npm -g install pnpm@latest-10 + pnpm install --frozen-lockfile + + - name: Build (Debug + UndefinedBehaviorSanitizer) + # -DFREPPLE_SANITIZER=undefined swaps ASan for UBSan in the Debug flags. + run: | + git submodule update --remote --checkout --force + grunt + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Debug -DFREPPLE_SANITIZER=undefined + cmake --build ${{github.workspace}}/build --config Debug -- -j 4 + . ${{github.workspace}}/venv/bin/activate + echo "PATH=$PATH" >> $GITHUB_ENV + echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV + + - name: Run engine golden tests under UBSan + working-directory: ${{github.workspace}} + # halt_on_error=1 + abort: a single UB diagnostic fails the run (blocking). + # print_stacktrace=1: pinpoint the source line. + # runtest.py -d streams child stdout/stderr (otherwise it PIPEs and + # discards it) so the UBSan report is visible in the log, not just a bare + # non-zero exit. tee + pipefail keeps the report for the summary step. + run: | + set -o pipefail + export FREPPLE_DATE_STYLE="day-month-year" + export UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=1:abort_on_error=1:report_error_type=1" + # Exclude the stress scenario (test/stress_1): a Debug+sanitizer build makes + # its ~24k-operationplan solve ~1000x slower than Release. It is a perf/scale + # gate (runs in the optimised ubuntu24 suite), not a UB-correctness one. + ./test/runtest.py -d -e stress_1 2>&1 | tee ubsan-output.log + + - name: Summarise UBSan findings + if: always() + working-directory: ${{github.workspace}} + # On a green run this is empty; on a regression it shows the new UB site(s) + # deduped by source location (per-run address stripped). + run: | + echo "## UBSan findings (distinct site x check)" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + grep -hE "runtime error:" ubsan-output.log 2>/dev/null \ + | sed -E "s/address 0x[0-9a-f]+/address 0x.../" \ + | sort | uniq -c | sort -rn >> "$GITHUB_STEP_SUMMARY" \ + || echo "(no UBSan findings - clean)" >> "$GITHUB_STEP_SUMMARY" + echo '```' >> "$GITHUB_STEP_SUMMARY" + + - name: Keep logs + uses: actions/upload-artifact@v6 + if: always() + with: + name: ubsan_logs + path: logs + retention-days: 3 diff --git a/.github/workflows/forecast-phase7.yml b/.github/workflows/forecast-phase7.yml new file mode 100644 index 0000000000..6e01600f3f --- /dev/null +++ b/.github/workflows/forecast-phase7.yml @@ -0,0 +1,97 @@ +name: Forecast Rust integration (phase 7) + +# Engine track E4 / phase 7 GATE: build the engine with the Rust forecast methods +# linked in (-DFREPPLE_RUST_FORECAST=ON, which also builds the forecast TU with +# -ffp-contract=off to match rustc's no-FMA), and run the forecast_* golden tests. +# These diff the engine's output byte-for-byte against the .expect files, so this +# job answers the open question from tools/modernization/rust-pilot.md: does the +# in-engine Rust path reach byte-exact golden parity? Green => the flag can flip +# ON; red => the FP-contraction gap is the recorded "stop = success" datapoint. +# +# Mirrors the ubuntu24 build job's engine setup, adds the Rust toolchain + the +# flag, and runs only the forecast golden suite. + +on: + push: + branches: ["modernization", "modernization/**", "rust-forecast-link"] + paths: + - "src/forecast/**" + - "rust/frepple-forecast/**" + - "CMakeLists.txt" + - "src/CMakeLists.txt" + - ".github/workflows/forecast-phase7.yml" + pull_request: + branches: [master, modernization] + paths: + - "src/forecast/**" + - "rust/frepple-forecast/**" + - "CMakeLists.txt" + - "src/CMakeLists.txt" + - ".github/workflows/forecast-phase7.yml" + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: forecast-phase7-${{ github.ref }} + cancel-in-progress: true + +jobs: + golden-parity: + name: forecast_* golden tests (Rust forecast ON) + runs-on: ubuntu-24.04 + timeout-minutes: 40 + services: + postgres: + image: postgres:16 + env: + POSTGRES_USER: frepple + POSTGRES_PASSWORD: frepple + POSTGRES_DB: postgres + ports: + - 5432:5432 + options: >- + --health-cmd pg_isready --health-interval 10s + --health-timeout 5s --health-retries 5 + steps: + - uses: actions/checkout@v6 + with: + submodules: recursive + + - uses: actions/setup-node@v6 + - uses: dtolnay/rust-toolchain@stable + + - name: Install packages + run: | + sudo apt update + sudo apt install --no-upgrade libxerces-c-dev openssl libssl-dev libpq5 libpq-dev postgresql-client python3 python3-dev python3-venv python3-setuptools + sudo npm -g install pnpm@latest-10 + pnpm install --frozen-lockfile + + - name: Build engine with the Rust forecast linked in + run: | + git submodule update --remote --checkout --force + grunt + cmake -B ${{github.workspace}}/build -DCMAKE_BUILD_TYPE=Release -DFREPPLE_RUST_FORECAST=ON + cmake --build ${{github.workspace}}/build --config Release -- -j 4 + . ${{github.workspace}}/venv/bin/activate + echo "PATH=$PATH" >> $GITHUB_ENV + echo "VIRTUAL_ENV=$VIRTUAL_ENV" >> $GITHUB_ENV + + - name: Forecast golden tests (byte-exact vs .expect) + working-directory: ${{github.workspace}} + run: | + export POSTGRES_HOST=localhost POSTGRES_PORT=5432 + export FREPPLE_DATE_STYLE="day-month-year" + ./test/runtest.py forecast_1 forecast_2 forecast_3 forecast_4 \ + forecast_5 forecast_6 forecast_7 forecast_8 forecast_9 \ + forecast_10 forecast_11 + + - name: Keep logs on failure + if: failure() + uses: actions/upload-artifact@v6 + with: + name: forecast-phase7-logs + path: logs + retention-days: 2 diff --git a/.github/workflows/frontend-e2e.yml b/.github/workflows/frontend-e2e.yml new file mode 100644 index 0000000000..3deca1aaa1 --- /dev/null +++ b/.github/workflows/frontend-e2e.yml @@ -0,0 +1,126 @@ +name: Frontend E2E (compose) + +# Backward-compat guardrail for the SPA: brings up the full same-origin stack +# (Postgres + Django/wsgi + daphne/asgi on the C++ engine image + Next.js + +# nginx) via the e2e compose files and runs the Playwright suite (smoke + a11y +# across all five screens + the engine-backed live-progress run). This is the +# automated net so new features can't silently break an existing screen. +# +# The engine image is restored from the deploy-staging buildx registry cache, so +# the C++ engine is NOT recompiled here (~5 min job, not ~15). The frontend image +# is built fresh from the checkout (the thing under test). + +on: + push: + branches: ["modernization", "modernization/**"] + paths: + - "frontend/**" + - "e2e/**" + - "freppledb/**" + - ".github/workflows/frontend-e2e.yml" + pull_request: + branches: [master, modernization] + paths: + - "frontend/**" + - "e2e/**" + - "freppledb/**" + - ".github/workflows/frontend-e2e.yml" + workflow_dispatch: + +permissions: + contents: read + packages: read + +# Supersede in-flight runs on the same ref - this stack build + Playwright is +# expensive, no point finishing a run a newer push already obsoleted. +concurrency: + group: frontend-e2e-${{ github.ref }} + cancel-in-progress: true + +env: + COMPOSE: docker compose -f e2e/docker-compose.yml -f e2e/docker-compose.engine.yml + +jobs: + e2e: + name: Playwright vs compose stack + runs-on: ubuntu-24.04 + timeout-minutes: 30 + steps: + - uses: actions/checkout@v4 + + - uses: docker/setup-buildx-action@v3 + - name: Log in to GHCR (for the engine buildcache) + uses: docker/login-action@v3 + with: + registry: ghcr.io + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + # Restore the compiled C++ engine layer from the deploy-staging cache and + # load it locally as `frepple-e2e-engine` so compose reuses it (no rebuild). + - name: Engine image (from registry cache) + uses: docker/build-push-action@v6 + with: + context: . + file: e2e/Dockerfile.engine + tags: frepple-e2e-engine:latest + load: true + cache-from: type=registry,ref=ghcr.io/ledoent/frepple-app:buildcache + + - name: Bring up the stack + run: $COMPOSE up -d --build + + - name: Wait for the stack to be ready + run: | + for i in $(seq 1 72); do + code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:18080/data/login/ || true) + if [ "$code" = "200" ]; then echo "login up after ${i}x5s"; sleep 5; break; fi + sleep 5 + done + code=$(curl -s -o /dev/null -w "%{http_code}" http://127.0.0.1:18080/data/login/ || true) + if [ "$code" != "200" ]; then echo "::error::stack did not become ready"; $COMPOSE logs --tail=120; exit 1; fi + + # Wait for the startup warmup plan (FREPPLE_INIT_RUNPLAN in the engine + # overlay) to finish. This guarantees the C++ engine is warm and the + # Redis->websocket broadcast path has fired once before the live-progress + # test launches its own plan - removing the cold-start race that made the + # first runplan slower than the test's terminal-state timeout. + - name: Wait for the engine warmup plan to complete + run: | + for i in $(seq 1 60); do + status=$($COMPOSE exec -T db psql -U frepple -d frepple0 -tAc \ + "SELECT status FROM execute_log WHERE name='runplan' ORDER BY id DESC LIMIT 1;" 2>/dev/null | tr -d '[:space:]' || true) + case "$status" in + Done) echo "engine warm after ${i}x5s"; exit 0 ;; + Failed) echo "::error::engine warmup plan failed"; $COMPOSE logs --tail=200 web-wsgi web-asgi; exit 1 ;; + esac + sleep 5 + done + echo "::error::engine warmup plan did not complete"; $COMPOSE logs --tail=200 web-wsgi web-asgi; exit 1 + + - uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: e2e/playwright/package-lock.json + + - name: Install Playwright deps + working-directory: e2e/playwright + run: | + npm ci + npx playwright install --with-deps chromium + + - name: Playwright (all screens + live-progress + pegging) + working-directory: e2e/playwright + env: + E2E_BASE_URL: http://127.0.0.1:18080 + E2E_ENGINE: "1" + run: npx playwright test + + - name: Compose logs on failure + if: failure() + run: $COMPOSE logs --tail=200 + + - name: Teardown + if: always() + run: $COMPOSE down -v diff --git a/.github/workflows/modernization.yml b/.github/workflows/modernization.yml new file mode 100644 index 0000000000..80ffc321b7 --- /dev/null +++ b/.github/workflows/modernization.yml @@ -0,0 +1,62 @@ +name: Modernization progress + +# Fast, lightweight CI for the modernization effort (see MODERNIZATION_PLAN.md). +# Tracks verification-gate progress and lints modernization tooling/code. +# The heavy C++ engine build lives in ubuntu24.yml — this workflow stays fast. + +on: + push: + branches: + - "modernization" + - "modernization/**" + pull_request: + branches: + - master + paths: + - "tools/modernization/**" + - ".github/workflows/modernization.yml" + - "MODERNIZATION_PLAN.md" + workflow_dispatch: + +permissions: + contents: read + +jobs: + progress: + name: Verification-gate progress + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Evaluate modernization gates + run: python tools/modernization/gates.py + + lint: + name: Lint modernization tooling + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + - name: Install black (pinned to repo version) + run: pip install "black==26.3.1" + - name: Check formatting of modernization tooling + run: black --check --diff tools/modernization/ + + frontend: + name: Build Next.js frontend + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: "22" + - name: Install + test + build (next build also typechecks) + working-directory: frontend + run: | + npm ci + npm test + npm run build diff --git a/.github/workflows/rust-pilot.yml b/.github/workflows/rust-pilot.yml new file mode 100644 index 0000000000..9fedc5e6d7 --- /dev/null +++ b/.github/workflows/rust-pilot.yml @@ -0,0 +1,86 @@ +name: Rust pilot (E4) + +# Evidence-gathering for the Rust/PyO3 pilot (MODERNIZATION_PLAN.md §E4): +# cargo unit tests + a Rust-vs-C++ parity diff for the ported json.cpp number +# kernel. Fast and standalone - no engine build, no deploy. See +# tools/modernization/rust-pilot.md for the measurements + decision. + +on: + push: + branches: ["modernization", "modernization/**"] + paths: + - "rust/**" + - "tools/rust-pilot/**" + - "test/rust_parity/**" + - ".github/workflows/rust-pilot.yml" + pull_request: + branches: [master] + paths: + - "rust/**" + - "tools/rust-pilot/**" + - "test/rust_parity/**" + - ".github/workflows/rust-pilot.yml" + workflow_dispatch: + +permissions: + contents: read + +jobs: + pilot: + name: Rust pilot — parity + measurements + runs-on: ubuntu-24.04 + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + - uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Rust unit tests (pure logic, no Python) + run: | + cargo test --manifest-path rust/frepple-num/Cargo.toml + cargo test --manifest-path rust/frepple-forecast/Cargo.toml + + - name: Solver spike (good_lp + microlp, keeps it building) + run: cargo run --manifest-path rust/solver-spike/Cargo.toml + + - name: Build the C++ parity references + run: | + g++ -O2 -o /tmp/cxx_reference tools/rust-pilot/cxx_reference.cpp + g++ -O2 -o /tmp/forecast_reference tools/rust-pilot/forecast_reference.cpp + + - name: Build + install the Rust wheels (maturin) + run: | + python -m pip install --upgrade pip maturin pytest + maturin build --release -m rust/frepple-num/Cargo.toml + maturin build --release -m rust/frepple-forecast/Cargo.toml + pip install --force-reinstall --no-deps rust/frepple-num/target/wheels/*.whl + pip install --force-reinstall --no-deps rust/frepple-forecast/target/wheels/*.whl + + - name: Parity tests (Rust vs C++ references) + env: + CXX_REF_BIN: /tmp/cxx_reference + FORECAST_CXX_REF_BIN: /tmp/forecast_reference + run: pytest test/rust_parity/ -q + + - name: C-ABI smoke test (staticlib link, phase 7 foundation) + run: | + cargo build --release --manifest-path rust/frepple-forecast/Cargo.toml + cc -O2 -I tools/rust-pilot tools/rust-pilot/capi_harness.c \ + rust/frepple-forecast/target/release/libfrepple_forecast.a -o /tmp/capi_harness + /tmp/capi_harness + + - name: Measurements + if: always() + run: | + { + echo "## Rust pilot measurements" + echo '```' + echo "slice 1 (json) — unsafe blocks: $(grep -c 'unsafe {' rust/frepple-num/src/num.rs || echo 0)" + echo "slice 1 (json) — rust core LOC: $(sed -n '/^pub fn /,/^}/p' rust/frepple-num/src/num.rs | grep -vcE '^\s*//|^\s*$')" + echo "slice 2 (forecast) — unsafe blocks: $(grep -c 'unsafe {' rust/frepple-forecast/src/forecast.rs || echo 0)" + echo "slice 2 (forecast) — rust LOC: $(grep -vcE '^\s*//|^\s*$' rust/frepple-forecast/src/forecast.rs)" + echo "slice 2 (forecast) — c++ MovingAverage LOC: $(sed -n '294,384p' src/forecast/timeseries.cpp | grep -vcE '^\s*//|^\s*$')" + echo '```' + echo "See tools/modernization/rust-pilot.md for the full evidence + go/no-go." + } >> "$GITHUB_STEP_SUMMARY" diff --git a/.github/workflows/ubuntu24.yml b/.github/workflows/ubuntu24.yml index 2d302945dc..230badc4db 100644 --- a/.github/workflows/ubuntu24.yml +++ b/.github/workflows/ubuntu24.yml @@ -36,6 +36,15 @@ jobs: --health-interval 10s --health-timeout 5s --health-retries 5 + redis: + image: redis:7 + ports: + - 6379:6379 + options: >- + --health-cmd "redis-cli ping" + --health-interval 10s + --health-timeout 5s + --health-retries 5 steps: - name: Checking out source code @@ -69,10 +78,37 @@ jobs: run: | export POSTGRES_HOST=localhost export POSTGRES_PORT=5432 + export REDIS_HOST=localhost + export REDIS_PORT=6379 export FREPPLE_DATE_STYLE="day-month-year" ./test/runtest.py ./frepplectl.py test freppledb --verbosity=2 + - name: Generate typed API client (Phase 0 ts-client gate) + if: contains(github.ref_name, 'modernization') || contains(github.head_ref, 'modernization') || contains(github.base_ref, 'modernization') + working-directory: ${{github.workspace}} + run: | + export POSTGRES_HOST=localhost + export POSTGRES_PORT=5432 + ./frepplectl.py createdatabase --noinput + ./frepplectl.py migrate --noinput + ./tools/modernization/gen_api_client.sh + # Drift gate: the committed schema + typed client must match a fresh + # regeneration from the live API. If this fails, run the script locally + # and commit the result. + git diff --exit-code -- generated/openapi.yaml frontend/lib/api-types.ts \ + || { echo "::error::OpenAPI schema / typed client is stale - run tools/modernization/gen_api_client.sh and commit"; exit 1; } + + - name: Upload generated API client + if: always() && (contains(github.ref_name, 'modernization') || contains(github.head_ref, 'modernization') || contains(github.base_ref, 'modernization')) + uses: actions/upload-artifact@v6 + with: + name: api-client + path: | + generated/openapi.yaml + frontend/lib/api-types.ts + retention-days: 7 + - name: Test uninstalling apps working-directory: ${{github.workspace}} run: | diff --git a/.gitignore b/.gitignore index fd3636c2bc..385288642f 100644 --- a/.gitignore +++ b/.gitignore @@ -45,3 +45,6 @@ venv *.egg-info /freppledb/forecast/frontend/node_modules /freppledb/common/frontend/node_modules + +# macOS Finder metadata +.DS_Store diff --git a/CMakeLists.txt b/CMakeLists.txt index a6bcd5d38b..db68ec6aea 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -78,6 +78,13 @@ if(grunt) ) endif() +# Engine track E4 / phase 7: link the Rust forecast methods (rust/frepple-forecast) +# into libfrepple behind this flag. Default OFF — the C++ forecast remains the +# shipping path; ON swaps each generateForecast body for the extern "C" Rust call +# and builds the forecast TU with -ffp-contract=off to match rustc (no FMA), the +# documented requirement for byte-exact golden parity (tools/modernization/rust-pilot.md). +option(FREPPLE_RUST_FORECAST "Use the Rust forecast methods instead of C++ (phase 7)" OFF) + # C++ compiler flags and features set(CMAKE_CXX_STANDARD 23) set(CMAKE_CXX_STANDARD_REQUIRED True) @@ -85,7 +92,23 @@ set(CMAKE_POSITION_INDEPENDENT_CODE ON) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-error=date-time") set(CMAKE_CXX_FLAGS_PROFILING "${CMAKE_CXX_FLAGS_DEBUG} -DNDEBUG") set(CMAKE_C_FLAGS_PROFILING "${CMAKE_C_FLAGS_DEBUG} -DNDEBUG") -set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=address -Wuninitialized -Wunused -Wswitch -Wunused-parameter -Wall") # -Wextra +# Engine track E1: the Debug build is sanitizer-instrumented. ASan by default +# (the existing green engine-asan gate); -DFREPPLE_SANITIZER=undefined runs the +# UBSan leg, or "address,undefined" both. The runtime behaviour (abort on first +# error) is set per-run via {A,UB}SAN_OPTIONS in the CI workflow, not here. +set(FREPPLE_SANITIZER "address" CACHE STRING "Sanitizer(s) for the Debug build: address | undefined | address,undefined") +set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -fsanitize=${FREPPLE_SANITIZER} -fno-omit-frame-pointer -Wuninitialized -Wunused -Wswitch -Wunused-parameter -Wall") # -Wextra +if(FREPPLE_SANITIZER MATCHES "undefined") + # frePPLe's object model uses a hand-rolled MetaClass RTTI (downcast by type + # tag, not C++ polymorphism), which is fundamentally incompatible with UBSan's + # -fsanitize=vptr check - it would flood every run with by-design "wrong + # dynamic type" reports (utils.h cast sites) and bury the real findings. + # Excluded by design; see tools/modernization/ubsan-baseline.md. + string(APPEND CMAKE_CXX_FLAGS_DEBUG " -fno-sanitize=vptr") +endif() +# The sanitizer runtime must be on the link line too (esp. UBSan for the shared lib). +set(CMAKE_EXE_LINKER_FLAGS_DEBUG "${CMAKE_EXE_LINKER_FLAGS_DEBUG} -fsanitize=${FREPPLE_SANITIZER}") +set(CMAKE_SHARED_LINKER_FLAGS_DEBUG "${CMAKE_SHARED_LINKER_FLAGS_DEBUG} -fsanitize=${FREPPLE_SANITIZER}") # Extra debug flags for specific audits/analysis: # -Wpadded: Warn if memory isn't used efficiently due to padding and alignments. set(CMAKE_EXE_LINKER_FLAGS_PROFILING "") diff --git a/ENGINE_REVIEW.md b/ENGINE_REVIEW.md new file mode 100644 index 0000000000..2b5cc951f5 --- /dev/null +++ b/ENGINE_REVIEW.md @@ -0,0 +1,220 @@ +# frePPLe Engine Code Review (E1) + +**Scope:** C++ engine (`src/solver`, `src/model`, `src/forecast`, `src/utils`, `include/`) + +the Django/Python layer (`freppledb/`). Method: parallel subsystem reviews, highest-impact claims +re-verified against source. All findings cite `file:line`. + +**Headline:** This review found **~12 concrete, evidence-backed defects** — including several +**reachable in normal production paths** — plus a clear map of the fragile areas. The engine is a +*battle-tested but fragile* asset: correct enough to ship, with real bugs that are mostly **small, +isolated, fixable in place**. This directly validates doing the review (and test hardening) **before** +any rewrite — and it gives the Rust decision real evidence instead of vibes. + +--- + +## Severity dashboard + +| Subsystem | Critical | High | Medium | Low | +|---|---|---|---|---| +| Solver (`src/solver`) | 2 | 4 | 5 | 5 | +| Model + Pegging (`src/model`) | 2 | 4 | 4 | 3 | +| Forecast (`src/forecast`) | 2 | 2 | 3 | 2 | +| Utils + CPython (`src/utils`) | 2 | 4 | 4 | 2 | +| Django (`freppledb/`) | 2 | 7 | 7 | 4 | + +--- + +## Immediate-fix queue (isolated, high-value, low-risk — do these first) + +These are localized patches with outsized payoff; none require architectural change. Ordered by ROI: + +1. **`PythonData(result)` refcount leak** — `src/utils/python.cpp:1026-1063` (+ `utils.h:2646-2648`). + `PythonFunction::call()` returns a *new* ref; `PythonData(const PyObject*)` unconditionally `INCREF`s + with no matching `DECREF` → **+1 leaked ref per Python callback, inside the solve loop** → unbounded + memory growth in long-running services. *The single highest-value memory fix.* Fix: steal the ref. +2. **`weight[]` out-of-bounds read** — `src/forecast/forecast.h:3039` / used `timeseries.cpp:355,516,781,1142`. + `static double weight[500]` indexed by history-bucket count, which **routinely exceeds 500** with the + default 10-yr horizon (≈520 weekly / 3650 daily). Silent OOB read corrupts SMAPE weights → wrong + method selection. **Production-reachable.** Fix: size to series length / clamp. +3. **GIL leaked on exception** — `src/utils/python.cpp:172-224, 227-260`. `PyGILState_Ensure` not + released on the throw paths in `execute()`/`initialize()` → **process-wide deadlock** on next acquire. + Fix: RAII GIL guard. +4. **`OperatorDelete::create` refcount leak** — `src/solver/operatordelete.cpp:79`. A stray `Py_INCREF(s)` + the sibling `SolverCreate::create` deliberately omits (solverplan.cpp:114-117). One leak per construct. + Fix: delete the line. +5. **`EntityIterator` copy-ctor UB** — `src/model/problem.cpp:357-372`. Calls `this->~EntityIterator()` + on a brand-new object → reads uninitialized `type`, `delete`s a garbage pointer → heap corruption. + Fix: remove the destructor call. +6. **JWT path never checks `user.is_active`** — `freppledb/common/middleware.py:246-287` + the + `user_can_authenticate` override (`auth.py:96`). **Deactivated users with a valid token still + authenticate.** Off-boarding silently broken for API clients. Fix: enforce `is_active`. +7. **`a_penalty` double-count** — `src/solver/solveroperation.cpp:50-123`. The author's TODO + ("doesn't this loop increment a_penalty incorrectly???") is a **real bug**: the capacity retry loop + accumulates penalty without the snapshot/restore the alternate-selection path does correctly + (solveroperation.cpp:1559,1629). Causes non-deterministic wrong alternate selection. Fix: bracket + `a_penalty`/`a_cost` around the loop. +8. **JSON inverted double→long bound** — `src/utils/json.cpp:803-808,878-883`. `else if (data_double > + LONG_MIN) return LONG_MIN;` → any in-range positive double returns `LONG_MIN`. Silent wrong integers. +9. **`setMaximumCalendar` iterator-after-erase** — `src/model/buffer.cpp:477-482`. `delete &(*(oo++))` + reads the just-nulled `next` → loop stops after the first deletion. Use the capture-advance idiom + already at buffer.cpp:403-409. +10. **Cache eviction use-after-free window** — `src/utils/cache.cpp:449-459`. `flush()` runs with the + lock released; needs an under-lock refcount recheck before `expire()` deletes. + +Each maps to a gate-able regression test; collectively they're the seed of the E2 test corpus. + +--- + +## Solver (`src/solver`) + +Architecture: single-threaded-per-cluster recursive descent; per-thread `CommandManager` + 256-deep +`State` stack (no shared mutable planning state between worker threads — **concurrency model is sound**). +Correctness rests entirely on disciplined save/restore of a shared mutable `State` struct and ~8 +interdependent bool flags (`forceLate`, `noRestore`, `delayed_reply`, …) across deep recursion + retry +loops. Most findings are failures of that discipline. + +- **C1 `a_penalty` double-count** (`solveroperation.cpp:123`) — real bug; see fix #7 above. +- **C2 OperatorDelete refcount leak** (`operatordelete.cpp:79`) — see fix #4. +- **H1** single `loopcounter` shared between the supply-retry and max-early loops splits the retry + budget unpredictably → safety stock under-planned on deep BOMs (`solverbuffer.cpp:807-943`). +- **H2** iterator invalidation across `solve()`-induced timeline mutation; mitigated by manual rewind, + fragile (`solverresource.cpp:94-259`, author TODO at :542). +- **H3** `forceLate`/`noRestore` flag coupling within one retry loop; dead-init then overwrite signals + drift (`solveroperation.cpp` ↔ `solverresource.cpp:64,75,425,529`). +- **H4** two outer planning loops have no hard iteration cap (rely on date advancement) → potential hang + on degenerate data (`solverdemand.cpp:224`, `solverbuffer.cpp:478-570`); contrast the guarded loop at + `solverbuffer.cpp:44`. +- **M1** monolithic functions: `solve(ResourceBuckets)` ~548 LOC, `solve(Buffer)` ~736, `solve(Demand)` + ~632 — where the flag-coupling bugs concentrate. +- **M3** `POLICY_INRATIO` demand groups silently `break` without enforcing the ratio — a stub posing as + a supported policy (`solverdemand.cpp:656`). +- *Refuted during verification:* the suspected double-`pop()` at `solverdemand.cpp:626/663` is **not** a + bug (the second drain is a no-op after normal completion). + +**Verdict:** fragile-but-correct, load-bearing core. **Right subsystem for an eventual Rust port, wrong +way to start** — the algorithm is under-specified by anything except this code, so a naive port would +faithfully reproduce H1/H3. Sequence: fix C1/C2 in place → build the regression corpus → port leaf +solvers (`solverflow`/`solverload`/`solverresource`) last-to-first, leaving the demand orchestrator last. + +--- + +## Model + Pegging (`src/model`) — the pointer-heaviest area + +Hand-rolled raw-pointer object graph: intrusive linked lists, bidirectional owner⇄child deletion, +dual-owned flowplans, a thread-local placement-`new` pool, `union`-of-iterator-pointers with manual +`new`/`delete`. **No smart pointers in the ownership model** — lifetime is managed by naming convention +and "null the back-pointer before deleting" comments. + +- **C1 EntityIterator copy-ctor UB** (`problem.cpp:357-372`) — see fix #5. +- **C2** bidirectional owner/child `delete` recursion; cycle broken only by manually nulling back-pointers + first; combined with three `delete this` sites in `activate()` → double-free if any path forgets + (`operationplan.cpp:1149-1190, 840-866`). +- **H1** `MemoryObjectList::operator=` assigns *through* a reference member — cannot rebind; copy-assigns + a `MemoryPool` with default shallow copy → latent double-free landmine (`utils.h:7173-7183`). +- **H2** `PeggingIterator::visited` set **never cleared** between traversals and omitted from `operator=` + → silent wrong pegging quantities when `OperationDependency` edges exist (`model.h:9817`, + `pegging.cpp:323-339,68-77`). +- **H3** `setMaximumCalendar` iterator-after-erase (`buffer.cpp:477-482`) — see fix #9. +- **H4 [FIXED]** `followPegging` did `dynamic_cast(...)->...` with **no null check** inside + quantity-driven unbounded scans → null-deref if a non-flowplan event (min/max/onhand) falls in the + window. All four scan cases (CASE 1A/1B/2A/2B, `buffer.cpp:602,647,708,772`) now null-check the cast + and skip the event. A remaining macOS-only Release *segfault* on `pegging_4/5/7` turned out to be a + local toolchain artifact (RTTI/`dynamic_cast` across the `libfrepple.dylib` boundary): under Linux + Debug+ASan *and* Release (reproduced in Docker) all 12 pegging scenarios run clean, so the scenarios + were restored to the suite as no-crash smoke tests. +- **M2** `OperationPlan` is a god object (~220 methods, 13 pointer members across 5 linked structures) — + concentrates the memory-safety risk. + +**Coverage gap (the scary part):** pegging has **only 2 tests**, both linear purchase→make→delivery. +Untested: split, alternate, routing sub-step, **dependency edges (would expose H2)**, transfer-batch, +maxlevel truncation — i.e. *the most pointer-dangerous code is the least tested.* + +**Verdict:** genuinely dangerous and **the strongest Rust candidate in the codebase** — every C1/H1/H2/H3/H4 +here is a bug class Rust eliminates by construction (enums, non-reseatable refs, borrow checker forbids +mutate-while-iterating). Pragmatic near-term: fix the five bugs + add ASan + backfill the 5 pegging tests. + +--- + +## Forecast (`src/forecast`) — the Rust-pilot candidate + +Hand-rolled moving-avg / single+double exponential (Holt / Holt-Winters) / Croston with an analytic +Marquardt optimizer. Math largely sound, with self-doubts. + +- **C (mem) `weight[]` OOB** (`forecast.h:3039`) — see fix #2. Production-reachable. +- **C (mem) `short` bucket index** unchecked at ~20 subscript sites (`forecast.h:774`, `forecast.cpp:401…`). +- **H** null `PyDict_GetItemString` deref in `setValuePython2` (`forecast.cpp:1577`). +- **A1** outlier seed uses uninitialized std-dev (`timeseries.cpp:451,675`, author TODO); seasonal method + has no outlier detection (`timeseries.cpp:1005`). +- **M** `leaf` memoization data race — unlocked `const_cast` write while solver runs threaded + (`forecast.cpp:171`); non-finite exprtk results flow into SQL unchecked (`measure_compute.cpp:267`). + +**Isolation:** the **numeric kernel is Python-free** (netting, hierarchy match, disaggregation, the fits, +exprtk eval) — but `Forecast`/`ForecastBucket` **inherit `Demand`**, and the code is welded to +`Plan::instance()`, `Measures::` globals, the Postgres `ForecastData` persistence, and MetaClass+CPython +registration. **Pilot scope = carve out the flat-array numeric kernel** (`(dates[], values[][], params) +→ values[]`); keep `forecast.cpp` as the marshalling adapter. Fix the two OOB bugs *first* so the Rust +port starts from a bounds-safe spec. + +--- + +## Utils + embedded CPython (`src/utils`) + +- **C `PythonData(result)` callback leak** (`python.cpp:1026`) — see fix #1. +- **C GIL leaked on exception** (`python.cpp:172-260`) — see fix #3. +- **C `transcodeUTF8` shared-buffer + unlocked encoder init** → truncation + race (`xml.cpp:37-56`). +- **H JSON file buffer not NUL-terminated** → heap over-read (`json.cpp:144-159`); **inverted double→long + bound** (`json.cpp:803`, fix #8); **cache eviction UAF** (`cache.cpp:449`, fix #10); **`Duration::parse` + integer overflow** (`date.cpp:165-267`). + +**MetaClass reflection** (`utils.h`): one `MetaField` cleanly multiplexes **XML+JSON+Python** via the +PyObject-free `DataValue`/`Serializer` abstractions — so the *value plane is separable* (good news). The +*bad news* is `class Object : public PyObject` (`utils.h:3021`): every engine object embeds the CPython +header and carries reflection intrusively, so any in-process-shared-graph rewrite needs a C++ shim per +type. A Python-boundary-only rewrite avoids reimplementing MetaClass but pays per-call marshalling. + +--- + +## Django / Python layer (`freppledb/`) + +Mature, disciplined (no `pickle`/`eval` on untrusted input, list-form subprocess — no shell injection, +**streams** heavy report output, `executesql` correctly superuser-gated). But the **token-auth + async +surfaces** and the **task worker** have real defects a new API would inherit. + +- **C1** JWT path skips `is_active` (`middleware.py:246`, `auth.py:96`) — see fix #6. +- **C2** SQLi antipattern in `ForecastPlanAPI.raw()` — laundered by `strftime` today, fragile + (`forecast/serializers.py:142`). +- **H1** `count(*)` uncached on every grid page — a cache was *removed* in commit e7ea943e1; top perf fix + (`report.py:1612`). +- **H2** task pickup not atomic (no `select_for_update`) → duplicate execution (`runworker.py:256`). +- **H3** crashed subprocess force-reported as "Done" — `exitcode` never inspected (`runworker.py:168`). +- **H4/H5** `@csrf_exempt` on destructive `APITask` + no CSRF/Origin check on the async consumers, which + reflect arbitrary `Origin` into CORS (`execute/views.py:294`, `input/services.py:144`, + `forecast/services.py:130`). +- **H6** find-or-update lets `add_*`-only users overwrite existing rows (`common/api/serializers.py:60`). +- **H7** `reportmanager` runs unrestricted raw SQL for non-superusers (`reportmanager/views.py:706`). +- **Maintainability:** `report.py` (4,352 LOC) is a coherent god-class — keep, extract upload/count. + `input/serializers.py` (2,094 LOC) is the worst offender — ~30 near-identical 4-class blocks with + duplicated MO/WO write logic that has **already diverged** (`WorkOrderdetailAPI:1808` wrong serializer; + doubled `batch` field `:1919/1921`). Factor before the new API reuses it. + +**Top 3 to fix before building the new API:** (1) token-auth surface (C1,H4,H5); (2) atomic + crash-honest +worker (H2,H3); (3) count caching + the add/overwrite permission gap (H1,H6). + +--- + +## Cross-cutting conclusions + +1. **The review paid for itself.** ~12 concrete bugs, several production-reachable (the `weight[]` OOB, + the per-callback refcount leak, the inactive-user JWT hole). Most are small, isolated fixes. +2. **Rust decision — now evidence-based:** the **model/pegging graph** is unambiguously where a borrow + checker would have prevented real, shipped bugs; the **forecast numeric kernel** is the lowest-risk + *pilot*. But the correct order is **fix-in-place → build the test corpus → pilot**, not rewrite-first. +3. **Test coverage is the gating constraint (feeds E2):** the strong 196k-line golden oracle has a + gaping hole exactly at the dangerous code (pegging: 2 tests; no C++ unit tests; no ASan in CI; + Python bindings barely tested). Closing this is the precondition for *any* rewrite and is valuable + regardless. + +### Recommended next actions +- **Open the immediate-fix queue as tracked work** (10 items above) — quick, high-value, low-risk. +- **E2 test hardening:** backfill the 5 pegging scenarios, add structural assertions, wire ASan/UBSan CI. +- **Then** reassess the Rust pilot (E4) against a green sanitizer baseline + the new corpus. diff --git a/MODERNIZATION_PLAN.md b/MODERNIZATION_PLAN.md new file mode 100644 index 0000000000..0321b48f3f --- /dev/null +++ b/MODERNIZATION_PLAN.md @@ -0,0 +1,525 @@ +# frePPLe Modernization Plan + +**Goal:** Modern, fast, event-driven UX. Replace the EOL AngularJS/jqGrid frontend with +Next.js, expose a clean API + websocket/event layer, and rework the Odoo integration — +**without rewriting the C++ solver or forecast engines.** + +**Status:** Active — major tracks delivered. Last roadmap review **2026-06-22**. + +--- + +## 0. Progress dashboard (what's shipped) + +A live status across the two parallel tracks. PR numbers are on `ledoent/frepple` → `modernization`. + +### Engine track (§7) +| Phase | State | Evidence | +| --- | --- | --- | +| **E1** — review + sanitizer baseline | ✅ **done** | ASan **blocking-clean** (`engine-asan.yml`); UBSan **blocking-clean** (`engine-ubsan.yml`, vptr excluded for the MetaClass RTTI, one real null member-call fixed, iterator-idiom annotated) (#14, #15); clang-tidy advisory gate + 54-finding baseline (#15); engine review report — 99-TODO triage + risk hotspots, with verification corrections (#16) | +| **E2** — test hardening (rewrite oracle) | ✅ **done** | structural-invariant oracle (`test/invariants.py`) + 11-model sweep (#18, #19); 24k-operationplan stress scenario with time+memory regression gate (#20); **deterministic operationplan ordering** — replaced a non-reproducible pointer tie-breaker with a creation-sequence, which **unblocked byte-exact pegging golden coverage** (pegging_4/5/7) and makes plans reproducible run-to-run (#21, #17 finding) | +| **E3** — DDMRP mode | ⬜ **next** | not started — the larger feature project (≈ a quarter); see §7 E3 | +| **E4** — Rust pilot + decision | ✅ **done** | json-kernel PyO3 pilot + **all 5 forecast methods** ported to Rust with byte-exact golden parity (#7, #8); flag-gated link (`FREPPLE_RUST_FORECAST`); **flipped ON in staging** — Rust is the forecast source of truth there (#12, #13); optimisation-solver spike evaluated (good_lp/microlp, #9). **Decision: conditional GO** for isolated numeric leaf modules, **NO-GO** for a wholesale engine rewrite (`tools/modernization/rust-pilot.md`) | + +### UI / API track (§4) +| Phase | State | Evidence | +| --- | --- | --- | +| **0** — API foundation | 🟡 partial | output JSON endpoints enriched (inventory/demand/resource/pegging via `PivotJSONStreamView`); input CRUD exists. Open: OpenAPI schema + typed client, remaining CRUD-gap polish | +| **1A** — Execute / plan-run + WS | ✅ **done** | live runplan screen, websocket task-progress (replaces polling), in-place re-plan loop | +| **1B** — Forecast Editor | ✅ **done** | Forecast pivot screen (live on staging) | +| **2** — Odoo integration rework | ⬜ not started | | +| **3** — Expand the new UI | ✅ **largely done** | Inventory, Demand, Resource (generic `PivotScreen`); **Demand Pegging Gantt** (read → drag-to-reschedule → re-plan loop, #3/#6/#10); Orders with inline CRUD; Problems/Constraints (#11). Open follow-on: Order **Create** (needs operation/item picker) | +| **3.5** — Deployment (Helm) | ✅ **done** | live at **frepple-staging.hz.ledoweb.com** (k3s hetzner-ledo); Helm chart `deploy/helm/frepple/`; CI image builds on ledoent ARC runners (#2 e2e guardrail, deploy-staging) | +| **4** — Go/Rust BFF | ⬜ optional | only on measured need | + +### Standout engineering wins +- **Reproducible plans** — the operationplan pointer-tie-breaker fix (#21) removes a long-standing source of run-to-run/platform output variance. +- **Two blocking sanitizer gates** (ASan + UBSan) + an advisory static-analysis gate over the C++ engine, all green. +- **Rust forecast in production-shaped staging**, byte-parity-gated, reversible by a flag. +- A **structural-invariant oracle** that's environment-robust where byte-exact golden can't be — the basis for any future engine change. + +### Next up +1. **Engine E3 — DDMRP mode** (the headline remaining feature; §7 E3). +2. **Phase 0 finish** — OpenAPI schema + typed TS client; close remaining CRUD gaps. +3. **Order Create** screen (completes Phase 3 CRUD). +4. **Phase 2 — Odoo rework** (when prioritised). + +--- + +## 1. Guiding principles + +1. **Keep the crown jewels — but earn any rewrite decision with evidence, don't assume it.** + The C++ solver and forecast engine are mature (modern C++23), performance-critical, and welded to + an embedded CPython interpreter via a MetaClass reflection layer — not separable. They are wrapped, + not rewritten *today*. BUT the engine is genuinely pointer-heavy (~70 manual `new`/`delete`, raw- + pointer graph traversal in solver/pegging) with real memory-safety surface and a few correctness + TODOs — a legitimate Rust argument. The decision is deferred to the **Engine track** (§7): review → + harden tests (the rewrite oracle) → run the already-wired ASan/UBSan → pilot ONE module in Rust/PyO3 + → let data decide. A full-engine rewrite's best case is still "identical behavior, years later"; a + *scoped* pilot (greenfield DDMRP solver, or the isolated forecast module) is how you evaluate Rust + without betting the proven MRP core. +2. **Keep Django as orchestration + data layer.** Modern deps (DRF 3.15, channels 4, allauth 65), + multi-scenario DB isolation, and fast raw-SQL report queries are years of plumbing worth keeping. + The web layer is *not* the bottleneck — I/O and N+1 queries are. +3. **Strangler-fig, not big-bang.** New Next.js UI runs side-by-side with the old UI, screen by + screen. Old screens retire only after the new one passes its verification gate. +4. **API-first.** One clean contract serves the new frontend *and* the reworked Odoo connector. + Build it once. +5. **Measure before optimizing.** Every performance claim gets a before/after number at its gate. + +### Known constraints / flags (from code audit) +- **Django is a frePPLe fork** (`github.com/frePPLe/django`, branch `frepple_8.3`). Security + updates lag upstream. Understand *why* it was forked before deepening reliance — tracked as a risk. +- **Confirmed N+1s** in the Odoo connector (`outbound.py`): per-product `product.supplierinfo` + query (self-documented `# TODO it's inefficient`), per-BOM line/subproduct/operation queries. +- **Websockets are staged but OFF**: `WebsocketService` consumer is commented out in `asgi.py`. + Channels/Daphne are installed; JWT token middleware exists; a Follower/Notification subscription + model exists. The event stack is ~60% built, not greenfield. +- **DRF-serializer slowness** is the one real "Python is slow" risk — avoided by serving large + result sets via the existing raw-SQL → `StreamingHttpResponse` path, not DRF serializers. + +--- + +## 2. Target architecture + +``` + KEEP HARDEN REPLACE + ┌────────────────┐ ┌───────────────────────┐ ┌──────────────────┐ + │ C++ solver │◀──embed─│ Django + DRF │◀────▶│ Next.js app │ + │ C++ forecast │ │ - REST (CRUD) │ HTTP │ - TanStack │ + └────────────────┘ │ - SQL→JSON (output) │ + │ Table/Query │ + │ - Channels (WS) │ WS │ - Recharts/D3v7 │ + │ - Token auth │ │ - Gantt │ + └──────────┬────────────┘ └──────────────────┘ + │ JSON/REST (replaces XML-over-XMLRPC) + ┌─────┴──────┐ + │ Odoo │ + └────────────┘ + + Optional later: thin Go/Rust BFF in front of Django for WS fan-out / hot read paths. +``` + +**Auth model:** JWT bearer tokens (middleware already exists) for the SPA + websockets; +keep session/CSRF for the legacy UI during co-existence. + +--- + +## 3. The API specification (Phase 0 detail) + +Three surfaces. All versioned under `/api/v1/`. + +### 3.1 REST — master data (CRUD) +Mostly exists today as DRF `frePPleListCreateAPIView` / `RetrieveUpdateDestroy`. Work = fill gaps ++ schema + consistent auth/pagination/filtering. + +| Resource | Path | Status | +|---|---|---| +| item, location, customer, supplier | `/api/v1/input/{resource}/` | ✅ exists | +| buffer, resource, skill, calendar, calendarbucket | `/api/v1/input/{resource}/` | ✅ exists | +| operation, operationmaterial, operationresource | `/api/v1/input/{resource}/` | ✅ exists | +| demand, itemsupplier, itemdistribution | `/api/v1/input/{resource}/` | ✅ exists | +| forecast, measure | `/api/v1/forecast/{resource}/` | ✅ exists | +| **operationplan (PO/MO/DO/WO) CRUD** | `/api/v1/input/operationplan/` | ⚠ verify edit semantics | + +Conventions: cursor pagination, `?filter[...]`, `?fields=`, `?scenario=` (DB selector), +`ETag`/`If-None-Match`, RFC-7807 error bodies. + +### 3.2 REST — plan/forecast OUTPUT (the gap to close) +The valuable data (forecast results, inventory projection, pegging) is currently trapped in the +jqGrid markup path. Expose it as JSON **by reusing the existing raw-SQL queries** (NOT DRF +serializers — dodges the serializer-slowness risk). Stream large grids via `StreamingHttpResponse`. + +| Endpoint | Source today | Returns | +|---|---|---| +| `GET /api/v1/output/forecast/` | forecast `OverviewReport` SQL | item×loc×cust × buckets × measures | +| `GET /api/v1/output/inventory/` | buffer report CTE (`buffer.py`) | on-hand, safety stock, produced/consumed, days-of-cover | +| `GET /api/v1/output/resource/` | resource report SQL | available/load/setup/utilization% per bucket | +| `GET /api/v1/output/demand/` | demand report SQL | orders, planned, backlog, constraints | +| `GET /api/v1/output/pegging/{demand}/` | pegging recursive query | supply-chain tree + Gantt rows | +| `GET /api/v1/output/constraint/`, `/problem/` | out_constraint / out_problem | violation lists + weights | +| `GET /api/v1/output/kpi/` | kpi view | fill rate, on-time %, utilization | + +Common query params: `?buckets=day|week|month`, `?start=&end=`, `?filter[...]`, `?scenario=`. + +### 3.3 Websocket + events (re-enable + finish the staged stack) +Channel auth: JWT via subprotocol or `?token=`. + +| Channel | Purpose | Backed by | +|---|---|---| +| `ws /api/v1/ws/tasks/` | Live task progress (replaces 5s polling). Pushes `{taskid, status, pct, message}` on change. | `Task.status` (already stores `'45%'`) | +| `ws /api/v1/ws/tasks/{id}/log/` | Live log tail (replaces full-file download). | task logfile | +| `ws /api/v1/ws/notifications/` | Event feed for followed objects. | existing Follower/Notification model | +| `ws /api/v1/ws/plan/` | Plan-changed broadcast after solve (cache-invalidate / refetch signal). | runplan completion signal | + +Event envelope (all channels): +```json +{ "type": "task.progress|task.log|notification|plan.changed", + "ts": "ISO-8601", "scenario": "default", "data": { ... } } +``` +HTTP fallbacks: SSE for log tail, existing `/execute/api/status/` polling for tasks. + +### 3.4 Cross-cutting +- **OpenAPI schema** via `drf-spectacular` → typed TypeScript client codegen for Next.js + (no hand-written, drift-prone types). +- **Auth:** JWT (existing `TokenMiddleware`); document refresh + scope. +- **Versioning:** `/api/v1/`; additive changes only within a major. + +--- + +## 4. Phased roadmap with verification gates + +Each phase has an explicit, testable gate. A phase is "done" only when its gate passes. +Phases 1A/1B can run in parallel after Phase 0. + +### Phase 0 — API foundation ✅ DONE +**Build:** OpenAPI schema (`drf-spectacular`); fill CRUD gaps; the output JSON endpoints +(§3.2) over existing SQL; JWT auth wired for API + WS; published TS client. +**Why first:** both frontend tracks *and* the Odoo rework consume this. +**Verification gate:** +- [x] `GET /api/schema/` returns a valid OpenAPI 3 doc (drf-spectacular `--validate`); the typed TS client + generates + strict-typechecks with **0 errors**. Committed source of truth: `generated/openapi.yaml` + + `frontend/lib/api-types.ts`, regenerated by `tools/modernization/gen_api_client.sh` and **drift-gated** + in `ubuntu24.yml` (regenerate + `git diff --exit-code`). The SPA consumes it via `frontend/lib/apiSchema.ts` + (e.g. `ORDER_STATUSES satisfies readonly OrderStatus[]` type-couples the Orders screen to the API enum). +- [x] Output endpoints return data matching the legacy reports (inventory/demand/resource/pegging enriched + via `PivotJSONStreamView` / `PeggingJSONView`; covered by `test_api_phase0`). +- [x] Large inventory grid streams via `StreamingHttpResponse` (no full buffering). +- [x] JWT auth round-trip works for REST + WS; unauthorized = 401/403 (`test_api_phase0`). +- [x] No DRF serializer on any output endpoint — the §3.2 outputs are plain Django views over raw SQL + (which is also why they're absent from the OpenAPI schema; the typed client covers the DRF input CRUD). + +**Known follow-ons (not blocking):** the typed client surfaced that the `ManufacturingOrder` schema has no +`item` field (the Orders MO tab shows an operation-derived item) — a real contract nuance to reconcile; and +the bulk CRUD views emit list-level delete/update operationIds that collide with the detail level (spectacular +auto-resolves with suffixes). Both are cosmetic to the gate; tracked for a CRUD-shape cleanup. + +### Phase 1A — Websocket beachhead: Execute / plan-run screen ✅ DONE +**Build:** Re-enable `WebsocketService` in `asgi.py`; `ws/tasks/` + `ws/tasks/{id}/log/` +channels; minimal Next.js page that launches a plan and shows **live** progress + log tail. +**Why:** smallest surface that proves the whole event stack end-to-end (auth → channel → React). +**Verification gate:** +- [ ] Launch `runplan` from the Next.js page; progress bar advances from WS pushes (not polling). +- [ ] Log tail streams within <1s of lines being written. +- [ ] Kill the worker mid-run → UI shows Failed state from the channel. +- [ ] Two browsers see the same live updates (channel fan-out works). +- [ ] Token-expired connection is rejected and reconnects cleanly. + +### Phase 1B — First real screen: Forecast Editor ✅ DONE +**Build:** Next.js Forecast Editor against `/api/v1/output/forecast/` (read) + +existing `ForecastService`/`FlushService` async path (write). Modern editable pivot +(TanStack Table), Recharts/D3v7 charts, bulk edit (copy/fill/±%), outlier highlighting, +remove the top-300 truncation. +**Why:** highest-value self-contained screen; forecasting is the priority; async backend exists. +**Verification gate:** +- [ ] Edit a forecast cell → save → re-net runs → grid reflects new `forecastnet` (parity with + legacy editor on the same edit). +- [ ] Bulk fill/±% across a selection persists correctly (spot-check DB rows). +- [ ] Renders >300 series without truncation; scroll/filter stays responsive (record frame time + vs. legacy). +- [ ] Outliers visibly flagged; one-click exclude updates the series. +- [ ] Accessibility: keyboard nav + screen-reader labels on grid (axe scan, 0 critical). + +### Phase 2 — Odoo integration rework +**Build:** Replace monolithic XML-over-XMLRPC with batched JSON/REST (reuse Phase-0 contract); +fix the confirmed N+1s with set-based prefetch; split into (a) scheduled master-data sync and +(b) on-demand order write-back; move to **delta** sync (changed records since last run). +**Verification gate:** +- [ ] Full export produces byte-equivalent plan inputs vs. the XML path (golden diff on demo DB). +- [ ] N+1s eliminated: query count for `export_items` + `export_boms` drops from O(N) to O(1) + per entity type — record query counts before/after (Django `assertNumQueries` / profiler). +- [ ] Plan write-back creates the same PO/MO/DO/WO records as today (count + field parity). +- [ ] Delta sync: changing 1 BOM re-syncs only that BOM, not the full model. +- [ ] End-to-end sync wall-time recorded before/after on a representative dataset. + +### Phase 3 — Expand the new UI (value order) ✅ LARGELY DONE +**Build, one screen at a time, each its own mini-gate:** +1. **Inventory/Buffer report** — reuse `/api/v1/output/inventory/`; sticky headers, virtualized grid. +2. **Demand Pegging Gantt** — the ambitious one: interactive Gantt with **drag-drop rescheduling** + (write back via operationplan API, preview downstream impact). +3. **Resource/Capacity** — utilization + a resource-timeline Gantt. +4. **Constraint/Problem** — violation lists + (new) impact/conflict view. +5. **Order summaries (MO/PO/DO)** + remaining **CRUD grids** (mechanical). +**Per-screen gate (template):** +- [ ] Data parity with legacy screen on seeded dataset (golden diff). +- [ ] Performance ≥ legacy (record load/interaction ms). +- [ ] Core workflow completes (e.g., Gantt: reschedule an MO, see it persist + downstream update). +- [ ] a11y scan clean; legacy screen flagged for retirement. + +**Delivered — Inventory / Demand / Resource (read pivots):** the three reporting +screens ship as `PivotScreen` over enriched `/api/output/{inventory,demand,resource}/`. + +**Delivered — Problems / Constraints + Orders (flat lists):** the violation-list and +order-summary screens ship as a reusable `TabListScreen` + `RecordTable` + `useRecordList` +(flat records, not pivots). Problems/Constraints toggle over new `/api/output/{problem,constraint}/` +(`JSONStreamView`, Django-tested); Orders toggle MO/PO/DO over the input DRF lists +(`/api/input/{manufacturingorder,purchaseorder,distributionorder}/`). The **Orders grid is +inline-editable** (Phase 3 CRUD): status pills, per-row edit of status/dates/quantity → +`PATCH`, delete with inline confirm → `DELETE`, optimistic + toast + reload; executed orders +(completed/closed) are locked. *Create* needs an operation/item picker — the one documented +follow-on. Covered by Playwright smoke + a11y (0 critical) + engine-backed CRUD specs +(edit-persist, delete). + +**Delivered — Demand Pegging Gantt, slice D1 (read-only):** pick a sales order → +trace its supply chain on a dated Gantt. Backend `PeggingJSONView` +(`freppledb/common/api/output.py`) enriches the pegging report with a `window` +header (horizon + due/current markers) the bare stream drops; data stays +byte-identical under `data` (Django data-parity test). Frontend `app/pegging/` ++ `lib/pegging.ts` (parse + geometry, unit-tested) render an HTML/CSS positioned-bar +Gantt — *not* SVG, so the drag-reschedule slice drops in without re-plumbing +geometry. Covered by Playwright smoke + a11y (0 critical) + an engine-backed +render spec. Sequenced **read-only first**; the ambitious parts are split out: +- **D2 — reschedule write-path (delivered):** drag a bar → `PATCH /api/input///` + via `authedFetch` (type→endpoint map, editability lock, optimistic + snap-back). Engine-backed E2E. +- **D3 — downstream highlight + re-plan loop (delivered):** a reschedule flags the affected downstream + chain (`downstreamChain`, the moved op + its ancestors toward the delivery — unit-tested) and offers an + in-place **Re-plan now** (`useReplan` launches `runplan`, waits over the task ws, re-fetches the peg). + Deliberately *not* a precise client-side ghost-bar simulation (it can't match the engine + would mislead); + the re-plan gives the authoritative downstream. Engine-backed E2E for the full loop. + +### Phase 3.5 — Deployment: Helm chart + load-balanced images ✅ DONE +**Context — data/state model (from code audit):** +- **PostgreSQL is the single source of truth.** Multi-DB router for scenarios. *No Redis today; + no key-value store.* Cache = per-process `LocMemCache` (not shared). Sessions = `signed_cookies` + (stateless — good for LB). Channel layer = **none → in-memory default (won't fan out across pods)**. +- **The live plan lives in the C++ engine's RAM in the worker process** (`MAXMEMORYSIZE` limit). + This is the key constraint: **the solver is stateful and vertically scaled — never round-robin + load-balanced.** Web traffic scales horizontally; planning scales by queue throughput. + +**Build — chart topology:** +- `postgresql` — StatefulSet or external managed PG (source of truth). +- `web` — Deployment + HPA, stateless Daphne/ASGI; HTTP liveness/readiness probes; signed-cookie + sessions mean no shared session store needed. +- `nextjs` — Deployment + HPA, stateless. +- `worker` — Deployment, **NOT load-balanced**; large memory request/limit; runs `runworker`/`runplan`; + scale replicas by queue depth, not by traffic. +- `redis` (**NEW**) — required for `channels_redis` (WS fan-out across web pods); also shared cache + (replace LocMem) + optional task broker. **Never a system of record — Postgres stays authoritative.** +- Image: multi-stage build, non-root, OCI labels; align `MAXMEMORYSIZE` with the pod memory limit. + +**Verification gate:** +- [ ] `helm install` on a clean cluster comes up green with zero manual steps; `helm test` passes. +- [ ] `web` scales to N replicas; a websocket message published from one pod reaches a client + connected to a *different* pod (proves `channels_redis` fan-out). +- [ ] Liveness/readiness probes correctly gate traffic: a failing pod is removed from the + Service endpoints; a slow-start pod doesn't receive traffic until ready. +- [ ] Worker pod liveness reflects the heartbeat (kill the process → pod restarts). +- [ ] A plan run survives a `web`-pod rolling update (planning is decoupled from web lifecycle). +- [ ] Memory: a large plan does not OOMKill the worker (MAXMEMORYSIZE ≤ pod limit, verified). +- [ ] `helm upgrade` performs a zero-downtime rollout of `web`/`nextjs`. + +**Delivered (staging review env) — `deploy/helm/frepple/`:** +- Live at **https://frepple-staging.hz.ledoweb.com** (k3s `hetzner-ledo`, ns `frepple-staging`): + modernized Execute + Forecast screens on the **real C++ engine**. TLS via cert-manager + `letsencrypt-prod`; one nginx ingress mirrors `e2e/nginx.conf` routing (`/ws`,`/forecast/detail`, + `/flush`→asgi; `/api`,`/data`,`/static`,`/execute/launch`→web; `/`→SPA). +- Images built on the **ledoent x86 ARC runners** (`.github/workflows/deploy-staging.yml`, plain + `docker build` on the dind sidecar) → `ghcr.io/ledoent/frepple-{app,frontend}:`. Test-new-images + loop: push → workflow → `helm upgrade --set image.tag=` → rollout (verified twice). +- Postgres = shared CNPG `shared-db` (superuser, frepple creates `frepple0/1/2`). Redis in-release for + channels fan-out. App is **single-replica/Recreate** (web+asgi co-located sharing an `emptyDir` log dir, + since the cluster has only RWO storage) — the HPA/multi-pod gate above is the documented v2 (needs RWX + + a separated worker). +- TLS-behind-proxy correctness: `FREPPLE_SECURE_PROXY_SSL_HEADER` + `FREPPLE_CSRF_TRUSTED_ORIGINS` set so + Django trusts the https origin; the SPA sends `X-CSRFToken` on the runplan launch POST. +- Verified: all 6 Playwright specs (smoke + a11y + **live-progress**: Run plan → engine → WS → terminal + state) green against the live URL; cert Ready; `/data/login/`,`/execute`,`/forecast` → 200. +- **Rust forecast flipped ON (helm REVISION 7, PR #12, Engine track E4):** the staging `frepple-app` + image builds with `FREPPLE_RUST_FORECAST=ON`, so all five forecast methods run in Rust in-engine — + **Rust is the forecast source of truth on staging.** The deployed `libfrepple.so` embeds the five + `extern "C"` wrappers and `runtest.py forecast_1..11` pass byte-exact *inside the deployed pod* (11/11). + Default stays OFF elsewhere; reversible by the flag. See `tools/modernization/rust-pilot.md` Phase 7. + +### Phase 4 (optional) — Go/Rust BFF +**Only if measured need.** Thin gateway in front of Django for WS fan-out at scale or hot +read-path offload. **Never the solver.** +**Verification gate:** +- [ ] A concrete metric (WS connections, p99 read latency) exceeds Django's comfortable range + *before* this phase starts — i.e., justified by data, not preference. +- [ ] BFF passes the same API contract tests as Django (drop-in). + +--- + +## 5. Cross-phase verification infrastructure (build early) +- **Seeded demo dataset** — deterministic fixture so every "parity" gate is reproducible + (the frepple demo/sample data is a starting point). +- **Golden-file harness** — capture legacy report/endpoint output, diff new output against it. +- **Query-count + timing harness** — `assertNumQueries` + the `frepple_profiler.py` pattern + (already written) for before/after numbers on every perf claim. +- **Side-by-side deploy** — old UI and Next.js served together so users can A/B and fall back. + +--- + +## 6. Open questions — RESOLVED (2026-06-13) + +### Q1. Why was Django forked? → **RESOLVED: low risk, de-fork is realistic** +The fork (`frePPLe/django`, branch `frepple_8.3`) is **stock Django 4.2 LTS** (current LTS), +tracking the official `4.2.x` stable branch. The real divergence is **tiny**: only 6 frePPLe +commits, ~11 files, ~100 lines — small patches to admin templates/widgets, `auth/decorators.py`, +`core/management/commands/migrate.py`, `db/models/base.py`, `fields/related.py`, `forms/models.py`. +These almost certainly exist to support the **multi-database scenario model** (cross-DB migrate + +foreign-key handling). +- **It is NOT a deep architectural fork of a custom framework.** It's modern Django + a thin patch set. +- **The actual liability:** the fork is **~80 commits behind** the latest 4.2.x security releases — + they sync manually and lag upstream CVE patches. +- **Implication:** Don't treat Django as a rewrite blocker. Two tractable paths: (a) re-base on the + latest 4.2.x to close the security lag, or (b) reduce the ~100 lines to app-level overrides / + monkey-patches and run **stock upstream Django**. *Action item: attempt to isolate the patches into + an app and run unforked Django — likely feasible.* + +### Q2. Community vs Enterprise boundary → **RESOLVED: everything local is MIT** +All three repos (app, Odoo connector, kencove deployment) are **MIT-licensed** Community Edition. +- Dual-license model: Community = MIT; Enterprise = separate proprietary product (**not in this code**). +- **No license-key checks, no feature gating, no runtime license validation** in the code. The + `license.xml` files are inert placeholders ("Community Edition users"). +- Forecast, MLForecast, SQL report manager, wizard, **Odoo connector** are ALL MIT and present here. + The "Enterprise only" strings are documentation comments for features that live in a separate + proprietary product (e.g. safety-stock/reorder export extras) — absent from this code. +- **Implication:** Full freedom to modernize, fork, build closed-source derivatives, and redistribute, + **provided** the MIT notice + "Copyright frePPLe bv" attribution is preserved. No CLA found. + +### Q3. Scenario model in the API → **RESOLVED: path-prefix routing; WS needs a fix** +Scenarios = **separate PostgreSQL databases** (`default`, `scenario1`, … — not schemas), registered +in a `common_scenario` table in the default DB. Routing today: +- **HTTP/WSGI:** scenario is a **URL path prefix** (`/scenario1/...`); `MultiDBMiddleware` strips it + and sets `request.database`; `MultiDBRouter` routes all ORM ops via thread-local request context. +- **Auth is global, access is per-scenario:** users live in the default DB; `User.databases` (an + ArrayField) gates which scenarios they can reach (404 if not allowed). JWT encodes the **user only**, + not the scenario. Permissions (is_superuser/is_active) can differ per scenario. +- **⚠ Websockets/ASGI differ:** the ASGI `TokenMiddleware` picks the DB from a **`FREPPLE_DATABASE` + env var** (one ASGI process per scenario), NOT from the URL. This conflicts with a single + load-balanced web deployment serving all scenarios. +- **API design decision:** use **`/api/v1//...`** path routing (reuses existing middleware). + **Required fix for the Helm/LB goal:** make the ASGI/websocket layer read the scenario from the URL + path (or a header / WS subprotocol) instead of the env var, so one `web` deployment can serve all + scenarios. *Folded into Phase 0 (auth/routing) + Phase 3.5 (deployment).* + +### Q4. Target deployment / Next.js origin → **RESOLVED: same-origin + JWT** +**Decision (user, 2026-06-13): single ingress, same origin, pure JWT auth.** +``` +Ingress (one host) + / → nextjs Deployment + /api/v1/* → web (Django/ASGI) + /ws/* → web (Django/ASGI) +``` +- Auth = `Authorization: Bearer ` for both REST and websockets. **No cookies, no CSRF, no CORS.** +- Stateless web pods (no server session store) → clean horizontal scaling, LB-friendly. +- **Implications baked into the plan:** + - Phase 0: standardize on JWT for all API + WS auth; drop reliance on session/CSRF for the new SPA + (legacy UI keeps sessions during co-existence). Scenario stays in the URL path (`/api/v1//`). + - Phase 3.5 (Helm): one ingress with the three path routes above; web + nextjs are separate stateless + Deployments behind it. + +--- + +## 7. Engine track (parallel to the UI/API phases) — RESOLVED direction (2026-06-15) + +A separate workstream focused on the C++ engine: code quality, test hardening, DDMRP, and an +evidence-based Rust decision. Runs **in parallel** with Phases 0–3 (different skill set, no shared +critical path). Sequenced E1 → E4. + +### Context from the engine audit +- **C++ is modern (C++23) and clean-ish**, but **pointer-heavy**: ~70 manual `new`/`delete`, raw-pointer + graph traversal in `src/model/pegging.cpp` + `src/solver/solveroperation.cpp`; deep embedded-CPython + coupling (`src/utils/python.cpp`, MetaClass reflection) — **not separable** from Python. +- **Scary correctness TODOs** in the hot path (e.g. `solveroperation.cpp:123` "doesn't this loop + increment a_penalty incorrectly???"; `operatordelete.cpp` "dangerous side effects"). +- **Test oracle exists but has a hole:** 82 golden scenarios → ~275 `.expect` files → ~196k lines of + expected output (strong), BUT **pegging has only 2 tests**, no C++ unit tests, no stress/perf/negative + tests, Python bindings barely tested. ASan/UBSan are already wired in CMake but not run in CI. +- **Licensing confirmed (skeptical re-audit):** the repo is the **complete, ungated MIT Community + Edition**. `edition` is a cosmetic display string; **no runtime license/edition gating** in Python + or C++; paid features (2FA, advanced Odoo export, quoting) are a **separate absent codebase**, not a + gate. (A few connector methods like `export_forecasts` carry "Enterprise only" *comments* but nothing + enforces them.) +- **DDMRP:** frePPLe is classic full-BOM-explosion push MRP (single `solver_mrp`, `solverplan.cpp:64`). + Partial DDMRP primitives already exist — **decoupled lead time** (`buffer.cpp:1300 getDecoupledLeadTime`, + a real head start) and a decoupling-point flag (`model.h:5220 IP_DATA`, used only for pegging today). + Missing: buffer zones (R/Y/G), ADU, Net Flow Position, qualified/spike demand, dynamic buffer + adjustment. Adding a hybrid `solver_ddmrp` mode is a **feature project (~a quarter)**, not a rewrite. + +### E1 — Thorough code review + sanitizer baseline ✅ DONE +**Build:** Structured review of engine + Django (debt catalog, the scary TODOs triaged); run the +already-wired **ASan/UBSan** over the golden test suite; run clang-tidy/analyzer; document findings. +**Verification gate:** +- [x] Review report committed: `tools/modernization/engine-review-E1.md` — 99-marker TODO triage, + risk-hotspot map (solver state machine, memory ownership, CPython coupling, pegging), clang-tidy/UBSan + cross-reference, and **verification corrections** (the `a_penalty` "bug" is already fixed; pegging has + 9 golden tests not 2; the pegging `visited` cycle-guard is sound; the operatordelete "dangerous" + block is disabled). Hands a prioritized work-list to E2. +- [x] ASan + UBSan run green across the golden scenarios. **Both blocking + clean** — `engine-asan.yml` + (the 8 Calendar UB crashes fixed earlier) and `engine-ubsan.yml` (vptr excluded for the MetaClass + RTTI; one real null member-call fixed in `operationdependency.cpp`; the iterator `operator*` + null-binding idiom marked `FREPPLE_NO_SANITIZE_NULL`). Findings in `tools/modernization/ubsan-baseline.md`. +- [x] clang-tidy baseline captured (advisory gate `engine-clang-tidy.yml`, bug-finder check set in + `.clang-tidy`); `tools/modernization/clang-tidy-baseline.md`. "No new findings" tightening → E2. + +### E2 — Test hardening (the rewrite-safety oracle) ✅ DONE +**Build:** Fill the pegging hole (multi-level BOM, circular supply, coalescence, alternate flows); +add **structural assertions** to the test runner (capacity never exceeded, demand≤due-or-flagged); +add a stress scenario (10k+ operationplans) with time/memory baselines; add negative/infeasible cases. +**Verification gate:** +- [x] Golden pegging coverage — **unblocked + done.** The 3 smoke-only tests (pegging_4/5/7) are now full + byte-exact golden tests. The blocker (environment-dependent `operationplans()` order) was root-caused to + a **pointer tie-breaker** in `OperationPlan::operator<` and fixed with a deterministic creation-sequence + tie-breaker (`engine-review-E1.md` H4 → RESOLVED). Verified zero blast radius (full 97-test suite passes + on Release + Debug+ASan) and byte-identical pegging output across builds. (A ≥3-level BOM + a cycle case + remain as optional future additions, now that the ordering is deterministic.) +- [x] Structural-invariant assertions — a reusable `test/invariants.py` checker + the `test/invariants_sweep` + test that loads **11 models** data-only (constraints_resource_1/3/5, constraints_material_1/3, + constraints_combined_1, constraints_leadtime_1, pegging_5, demand_policy, safety_stock, flow_alternate_1), + solves each fully constrained, and asserts SOUND invariants in one process (`frepple.erase(True)` between + models): operationplan temporal/quantity sanity; no resource overload under a capacity constraint; a finite + buffer goes negative only if a material-shortage problem is flagged. It is a **boolean pass/fail oracle** + (deterministic `INVARIANTS_OK` output), so unlike byte-exact golden it is robust to the environment- + dependent ORDER (H4) — all 11 clean under **both Release and Debug+ASan**, and the checker **fails (exit≠0) + on an injected capacity overload** (verified: solving without the capacity constraint surfaces overloads). + Two findings recorded: (1) the "obvious" invariants (demand met-by-due, buffer never negative) + **false-positive on valid plans** (legitimate late/short/over deliveries, WIP buffers) — only the + conservative set above is universally sound; (2) a per-test `runtest.py` hook was **rejected** — most golden + tests end on an *unconstrained* solve, so applying the capacity/material invariants to their final plan + would false-positive, hence the sweep keeps control of the solve mode. +- [x] Stress scenario (`test/stress_1`) — builds ~8k items (make-from-purchased-component + demand) via the + Python API → **~24k operationplans**, solves fully constrained, and records solve-time + peak RSS. + Regression-gated: a hard floor on the operationplan count (≥10k) + **generous ceilings** on time (180s) + and memory (1500 MB) that catch a catastrophic blow-up (O(n²)/leak) without flaking on runner variance; + actual metrics are printed for trend tracking. Release baseline: ~24k opplans, **~1.5s solve, ~80 MB**. + Runs in the optimised ubuntu24 (Release) suite; **excluded from engine-asan/engine-ubsan** — a Debug+ + sanitizer build makes the at-scale solve ~1000x slower (~24 min, allocation-heavy), and it's a perf gate, + not a memory-safety one (the smaller golden tests cover ASan/UBSan). +- [x] Sanitizer CI job added and green on the branch (ASan + UBSan blocking, clang-tidy advisory — E2 slice 1). + +### E3 — DDMRP mode (hybrid with classic MRP) ⬜ NEXT +**Build:** Data model (buffer zone profiles, ADU config, spike horizon — via new fields/attributes); +ADU + Net Flow Position calculation; a `solver_ddmrp` path with per-buffer opt-in; reuse the existing +`getDecoupledLeadTime`. Classic-MRP buffers and DDMRP buffers coexist in one model. +**Verification gate:** +- [ ] Per-buffer `ddmrp` opt-in routes to the DDMRP solver; non-opted buffers unchanged (MRP parity preserved). +- [ ] Golden DDMRP scenarios: zone (R/Y/G) transitions + NFP-triggered replenishment match hand-computed expectations. +- [ ] Spike-horizon qualification demonstrably filters order spikes from the buffer signal. +- [ ] Decoupling point stops BOM explosion at the buffer (vs. classic full explosion) — verified on a multi-level model. + +### E4 — Rust pilot + decision (evidence-based) ✅ DONE +**Build:** Pilot ONE isolated module in Rust via **PyO3** — preferred candidate is the **new DDMRP +solver** (greenfield → zero regression risk) OR the **forecast module** (`src/forecast/`, ~5.6k LOC, +most isolated). Measure dev experience, safety (no manual refcount/ptr bugs), perf vs C++. +**Verification gate (this gate decides Rust yes/no):** +- [ ] Pilot module passes the SAME golden/structural tests as its C++ equivalent (or, for greenfield + DDMRP, its own hand-computed oracle). +- [ ] Measured comparison recorded: LOC, perf (solve time/mem), and a written safety/maintainability + assessment vs the C++ baseline. +- [ ] **Decision documented**: proceed to wider Rust migration, or stop at the pilot — justified by the + measurements above, not preference. (A "stop" outcome is a success — it's an answered question.) + +**Delivered (first pilot):** Started small + decoupled — ported the JSON number-conversion kernel +(`src/utils/json.cpp` getLong/getInt/getUnsignedLong, the inverted-bound bug site) to a PyO3 extension +`rust/frepple-num/`. Parity = a Rust-vs-C++ diff against a verbatim reference +(`tools/rust-pilot/cxx_reference.cpp`, `test/rust_parity/`, 24/24); evidence + go/no-go in +`tools/modernization/rust-pilot.md`; CI in `.github/workflows/rust-pilot.yml` (cargo test + maturin + +parity, no engine build). All three E4 gates active. **Decision: conditional GO** for targeted Rust on +isolated numeric leaf modules, **NO-GO** for a wholesale engine rewrite. Intentionally CI-only — +shipping the wheel into the engine image is a "go"-only fast-follow. + +**Slice 2 (delivered):** ported a real forecasting method — `MovingAverage::generateForecast` + +`smapeWeight` (`src/forecast/timeseries.cpp:294-384`, the `weight[]` OOB site) to `rust/frepple-forecast/`, +parity-diffed 10/10 against a verbatim C++ reference (incl. >MAXBUCKETS series) within 1e-9. Finding: +LOC is comparable (not smaller) for tight numeric code — the win is compile-enforced safety + the PyO3 +linkage, not line count. Next slices: SingleExponential / DoubleExponential / Seasonal / Croston. diff --git a/deploy/helm/frepple/Chart.yaml b/deploy/helm/frepple/Chart.yaml new file mode 100644 index 0000000000..9a66b1dc60 --- /dev/null +++ b/deploy/helm/frepple/Chart.yaml @@ -0,0 +1,6 @@ +apiVersion: v2 +name: frepple +description: Modernized frePPLe (Django + C++ engine + Next.js SPA) - staging/review deploy +type: application +version: 0.1.0 +appVersion: "modernization" diff --git a/deploy/helm/frepple/README.md b/deploy/helm/frepple/README.md new file mode 100644 index 0000000000..bb6b51134b --- /dev/null +++ b/deploy/helm/frepple/README.md @@ -0,0 +1,53 @@ +# frePPLe staging Helm chart + +A clickable review env for the modernized Execute + Forecast screens on the real +C++ engine. Single same-origin ingress (SPA + REST + websockets), TLS via +cert-manager. See `MODERNIZATION_PLAN.md` § Phase 3.5. + +## Topology +- **app** Deployment (engine image, `replicas: 1`, `Recreate`): two containers — + `web` (`entrypoint.sh wsgi`: DB init + `runserver --insecure` :8000, spawns the + worker on task launch) and `asgi` (`entrypoint.sh asgi`: daphne :8001) — sharing + an `emptyDir` log dir at `/app/logs` (the worker writes task logs, asgi tails + them). Co-located + single-replica because the cluster has only RWO storage. +- **frontend** Deployment (Next.js SPA :3000). +- **redis** Deployment (channels fan-out). +- **Ingress** (nginx, TLS): `/ws`,`/forecast/detail`,`/flush` → asgi; + `/api`,`/data`,`/static`,`/execute/launch` → web; `/` → SPA. +- **Postgres**: external by default (`shared-db-rw.cnpg.svc`); `postgres.mode: + builtin` deploys an in-release `postgres:16` + RWO PVC instead. + +## Build images (ledoent ARC → GHCR) +`.github/workflows/deploy-staging.yml` (trigger: push to `modernization` touching +the image sources, or `workflow_dispatch`) builds and pushes +`ghcr.io/ledoent/frepple-app:` and `…/frepple-frontend:`. + +## First deploy +```sh +kubectl create ns frepple-staging +# pull secret + DB creds (CNPG superuser): +kubectl -n bimble-staging get secret ghcr-pull -o yaml | \ + sed 's/namespace: bimble-staging//' | kubectl -n frepple-staging apply -f - +kubectl -n frepple-staging create secret generic frepple-db \ + --from-literal=POSTGRES_USER="$(kubectl -n cnpg get secret shared-db-superuser -o jsonpath='{.data.username}' | base64 -d)" \ + --from-literal=POSTGRES_PASSWORD="$(kubectl -n cnpg get secret shared-db-superuser -o jsonpath='{.data.password}' | base64 -d)" + +helm upgrade --install frepple deploy/helm/frepple -n frepple-staging \ + --set image.tag= +``` + +## Test a new image +```sh +git push ledoent modernization # triggers the build workflow +helm upgrade --install frepple deploy/helm/frepple -n frepple-staging \ + --set image.tag= +kubectl -n frepple-staging rollout status deploy/frepple-app deploy/frepple-frontend +``` + +## Verify +```sh +E2E_BASE_URL=https://frepple-staging.hz.ledoweb.com E2E_ENGINE=1 \ + npx playwright test --config e2e/playwright/playwright.config.ts +``` +Browse https://frepple-staging.hz.ledoweb.com (login `admin`/`admin`): **/execute** +(Run plan → live progress) and **/forecast** (grid, chart, bulk edit). diff --git a/deploy/helm/frepple/templates/_helpers.tpl b/deploy/helm/frepple/templates/_helpers.tpl new file mode 100644 index 0000000000..36fec48736 --- /dev/null +++ b/deploy/helm/frepple/templates/_helpers.tpl @@ -0,0 +1,72 @@ +{{- define "frepple.name" -}} +{{- default "frepple" .Values.nameOverride -}} +{{- end -}} + +{{- define "frepple.labels" -}} +app.kubernetes.io/name: {{ include "frepple.name" . }} +app.kubernetes.io/instance: {{ .Release.Name }} +app.kubernetes.io/managed-by: {{ .Release.Service }} +{{- end -}} + +{{- define "frepple.pgHost" -}} +{{- if eq .Values.postgres.mode "builtin" -}} +{{ include "frepple.name" . }}-postgres +{{- else -}} +{{ .Values.postgres.external.host }} +{{- end -}} +{{- end -}} + +{{- define "frepple.dbSecret" -}} +{{- if eq .Values.postgres.mode "builtin" -}} +{{ include "frepple.name" . }}-postgres +{{- else -}} +{{ .Values.postgres.external.credentialsSecret }} +{{- end -}} +{{- end -}} + +{{/* Common env shared by the web + asgi containers. */}} +{{- define "frepple.env" -}} +- name: POSTGRES_HOST + value: {{ include "frepple.pgHost" . | quote }} +- name: POSTGRES_PORT + value: {{ (eq .Values.postgres.mode "builtin") | ternary "5432" (printf "%v" .Values.postgres.external.port) | quote }} +- name: POSTGRES_DBNAME + value: {{ (eq .Values.postgres.mode "builtin") | ternary .Values.postgres.builtin.dbname .Values.postgres.external.dbname | quote }} +- name: POSTGRES_USER + valueFrom: + secretKeyRef: + name: {{ include "frepple.dbSecret" . }} + key: POSTGRES_USER +- name: POSTGRES_PASSWORD + valueFrom: + secretKeyRef: + name: {{ include "frepple.dbSecret" . }} + key: POSTGRES_PASSWORD +- name: REDIS_HOST + value: {{ include "frepple.name" . }}-redis +- name: REDIS_PORT + value: "6379" +- name: FREPPLE_DATE_STYLE + value: day-month-year +# Generated/persisted signing key (not the public in-repo default). +- name: FREPPLE_SECRETKEY + valueFrom: + secretKeyRef: + name: {{ include "frepple.name" . }}-secret + key: SECRET_KEY +# Serve with tracebacks off + a real host allowlist even though it's runserver. +- name: FREPPLE_DEBUG + value: "false" +- name: FREPPLE_ALLOWED_HOSTS + value: {{ .Values.allowedHosts | default .Values.host | quote }} +- name: FREPPLE_LOAD_DEMO + value: {{ .Values.app.loadDemo | quote }} +{{- if .Values.ingress.tls }} +# TLS is terminated at the ingress; tell Django the request is secure (via the +# forwarded-proto header) and trust the https origin so login POST passes CSRF. +- name: FREPPLE_SECURE_PROXY_SSL_HEADER + value: "HTTP_X_FORWARDED_PROTO https" +- name: FREPPLE_CSRF_TRUSTED_ORIGINS + value: {{ printf "https://%s" .Values.host | quote }} +{{- end }} +{{- end -}} diff --git a/deploy/helm/frepple/templates/app.yaml b/deploy/helm/frepple/templates/app.yaml new file mode 100644 index 0000000000..7899ac7cf1 --- /dev/null +++ b/deploy/helm/frepple/templates/app.yaml @@ -0,0 +1,97 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "frepple.name" . }}-app + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.app.replicas }} + # The worker writes task logs to a pod-local emptyDir that the asgi container + # tails, so the app is single-replica until RWX storage is available. + strategy: + type: Recreate + selector: + matchLabels: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: app + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: app + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: {{- toYaml . | nindent 8 }} + {{- end }} + volumes: + - name: logs + emptyDir: {} + containers: + - name: web + image: "{{ .Values.image.registry }}/{{ .Values.image.app }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: ["wsgi"] + env: + {{- include "frepple.env" . | nindent 12 }} + {{- if .Values.app.initRunplan }} + - name: FREPPLE_INIT_RUNPLAN + value: "1" + {{- end }} + ports: + - { name: http, containerPort: 8000 } + volumeMounts: + - { name: logs, mountPath: /app/logs } + # Probe with the public Host header: with DEBUG off + a real + # ALLOWED_HOSTS, a bare pod-IP Host would be rejected (DisallowedHost). + readinessProbe: + httpGet: + path: /data/login/ + port: 8000 + httpHeaders: + - { name: Host, value: {{ .Values.host | quote }} } + initialDelaySeconds: 30 + periodSeconds: 10 + failureThreshold: 30 + livenessProbe: + httpGet: + path: /data/login/ + port: 8000 + httpHeaders: + - { name: Host, value: {{ .Values.host | quote }} } + initialDelaySeconds: 120 + periodSeconds: 20 + failureThreshold: 6 + resources: {{- toYaml .Values.app.web.resources | nindent 12 }} + - name: asgi + image: "{{ .Values.image.registry }}/{{ .Values.image.app }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + args: ["asgi"] + env: + {{- include "frepple.env" . | nindent 12 }} + ports: + - { name: ws, containerPort: 8001 } + volumeMounts: + - { name: logs, mountPath: /app/logs } + readinessProbe: + tcpSocket: { port: 8001 } + initialDelaySeconds: 40 + periodSeconds: 10 + failureThreshold: 30 + livenessProbe: + tcpSocket: { port: 8001 } + initialDelaySeconds: 120 + periodSeconds: 20 + failureThreshold: 6 + resources: {{- toYaml .Values.app.asgi.resources | nindent 12 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "frepple.name" . }}-app + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + selector: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: app + ports: + - { name: http, port: 8000, targetPort: 8000 } + - { name: ws, port: 8001, targetPort: 8001 } diff --git a/deploy/helm/frepple/templates/frontend.yaml b/deploy/helm/frepple/templates/frontend.yaml new file mode 100644 index 0000000000..4ef1a3d5a2 --- /dev/null +++ b/deploy/helm/frepple/templates/frontend.yaml @@ -0,0 +1,43 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "frepple.name" . }}-frontend + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + replicas: {{ .Values.frontend.replicas }} + selector: + matchLabels: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: frontend + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: frontend + spec: + {{- with .Values.imagePullSecrets }} + imagePullSecrets: {{- toYaml . | nindent 8 }} + {{- end }} + containers: + - name: frontend + image: "{{ .Values.image.registry }}/{{ .Values.image.frontend }}:{{ .Values.image.tag }}" + imagePullPolicy: {{ .Values.image.pullPolicy }} + ports: + - { name: http, containerPort: 3000 } + readinessProbe: + httpGet: { path: /execute, port: 3000 } + initialDelaySeconds: 5 + periodSeconds: 10 + resources: {{- toYaml .Values.frontend.resources | nindent 12 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "frepple.name" . }}-frontend + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + selector: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: frontend + ports: + - { name: http, port: 3000, targetPort: 3000 } diff --git a/deploy/helm/frepple/templates/ingress.yaml b/deploy/helm/frepple/templates/ingress.yaml new file mode 100644 index 0000000000..941967e29e --- /dev/null +++ b/deploy/helm/frepple/templates/ingress.yaml @@ -0,0 +1,39 @@ +apiVersion: networking.k8s.io/v1 +kind: Ingress +metadata: + name: {{ include "frepple.name" . }} + labels: {{- include "frepple.labels" . | nindent 4 }} + annotations: + {{- if .Values.ingress.tls }} + cert-manager.io/cluster-issuer: {{ .Values.ingress.clusterIssuer }} + {{- end }} + # Long-lived websockets (live task progress / log tail). + nginx.ingress.kubernetes.io/proxy-read-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-send-timeout: "3600" + nginx.ingress.kubernetes.io/proxy-body-size: "64m" +spec: + ingressClassName: {{ .Values.ingress.className }} + {{- if .Values.ingress.tls }} + tls: + - hosts: [{{ .Values.host | quote }}] + secretName: {{ include "frepple.name" . }}-tls + {{- end }} + rules: + - host: {{ .Values.host }} + http: + # Routing table — keep in sync with e2e/nginx.conf (compose) and the + # frontend next.config.mjs dev rewrites. These match the DEFAULT scenario + # (empty URL prefix); a scenario switch (//...) is not routed + # here, so this env is single-scenario until prefix-aware rules are added. + paths: + # Websockets + engine HTTP services -> asgi (daphne). + - { path: /ws, pathType: Prefix, backend: { service: { name: {{ include "frepple.name" . }}-app, port: { name: ws } } } } + - { path: /forecast/detail, pathType: Prefix, backend: { service: { name: {{ include "frepple.name" . }}-app, port: { name: ws } } } } + - { path: /flush, pathType: Prefix, backend: { service: { name: {{ include "frepple.name" . }}-app, port: { name: ws } } } } + # REST API + Django UI/login/static + task launch -> web (WSGI). + - { path: /api, pathType: Prefix, backend: { service: { name: {{ include "frepple.name" . }}-app, port: { name: http } } } } + - { path: /data, pathType: Prefix, backend: { service: { name: {{ include "frepple.name" . }}-app, port: { name: http } } } } + - { path: /static, pathType: Prefix, backend: { service: { name: {{ include "frepple.name" . }}-app, port: { name: http } } } } + - { path: /execute/launch, pathType: Prefix, backend: { service: { name: {{ include "frepple.name" . }}-app, port: { name: http } } } } + # Everything else -> the SPA. + - { path: /, pathType: Prefix, backend: { service: { name: {{ include "frepple.name" . }}-frontend, port: { name: http } } } } diff --git a/deploy/helm/frepple/templates/postgres.yaml b/deploy/helm/frepple/templates/postgres.yaml new file mode 100644 index 0000000000..b88edf9bb0 --- /dev/null +++ b/deploy/helm/frepple/templates/postgres.yaml @@ -0,0 +1,72 @@ +{{- if eq .Values.postgres.mode "builtin" }} +apiVersion: v1 +kind: Secret +metadata: + name: {{ include "frepple.name" . }}-postgres + labels: {{- include "frepple.labels" . | nindent 4 }} +type: Opaque +stringData: + POSTGRES_USER: {{ .Values.postgres.builtin.user | quote }} + POSTGRES_PASSWORD: {{ .Values.postgres.builtin.password | quote }} +--- +apiVersion: v1 +kind: PersistentVolumeClaim +metadata: + name: {{ include "frepple.name" . }}-postgres + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + accessModes: ["ReadWriteOnce"] + resources: + requests: + storage: {{ .Values.postgres.builtin.storage }} +--- +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "frepple.name" . }}-postgres + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + replicas: 1 + strategy: { type: Recreate } + selector: + matchLabels: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: postgres + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: postgres + spec: + containers: + - name: postgres + image: {{ .Values.postgres.builtin.image }} + env: + - { name: POSTGRES_USER, value: {{ .Values.postgres.builtin.user | quote }} } + - { name: POSTGRES_PASSWORD, value: {{ .Values.postgres.builtin.password | quote }} } + - { name: POSTGRES_DB, value: {{ .Values.postgres.builtin.dbname | quote }} } + - { name: PGDATA, value: /var/lib/postgresql/data/pgdata } + ports: + - { name: postgres, containerPort: 5432 } + volumeMounts: + - { name: data, mountPath: /var/lib/postgresql/data } + readinessProbe: + exec: { command: ["pg_isready", "-U", {{ .Values.postgres.builtin.user | quote }}] } + periodSeconds: 5 + volumes: + - name: data + persistentVolumeClaim: + claimName: {{ include "frepple.name" . }}-postgres +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "frepple.name" . }}-postgres + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + selector: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: postgres + ports: + - { name: postgres, port: 5432, targetPort: 5432 } +{{- end }} diff --git a/deploy/helm/frepple/templates/redis.yaml b/deploy/helm/frepple/templates/redis.yaml new file mode 100644 index 0000000000..9ac1320606 --- /dev/null +++ b/deploy/helm/frepple/templates/redis.yaml @@ -0,0 +1,37 @@ +apiVersion: apps/v1 +kind: Deployment +metadata: + name: {{ include "frepple.name" . }}-redis + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + replicas: 1 + selector: + matchLabels: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: redis + template: + metadata: + labels: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: redis + spec: + containers: + - name: redis + image: {{ .Values.redis.image }} + ports: + - { name: redis, containerPort: 6379 } + readinessProbe: + tcpSocket: { port: 6379 } + resources: {{- toYaml .Values.redis.resources | nindent 12 }} +--- +apiVersion: v1 +kind: Service +metadata: + name: {{ include "frepple.name" . }}-redis + labels: {{- include "frepple.labels" . | nindent 4 }} +spec: + selector: + app.kubernetes.io/name: {{ include "frepple.name" . }} + app.kubernetes.io/component: redis + ports: + - { name: redis, port: 6379, targetPort: 6379 } diff --git a/deploy/helm/frepple/templates/secret.yaml b/deploy/helm/frepple/templates/secret.yaml new file mode 100644 index 0000000000..7c3a3ac1c4 --- /dev/null +++ b/deploy/helm/frepple/templates/secret.yaml @@ -0,0 +1,24 @@ +{{- /* + Django SECRET_KEY (also the JWT/web-token signing key). Generated once and + preserved across `helm upgrade` via a lookup of the existing secret, so a + rollout doesn't invalidate live sessions/tokens. Set .Values.secretKey to pin + it explicitly (e.g. to share a key across replicas/envs). +*/ -}} +{{- $name := printf "%s-secret" (include "frepple.name" .) -}} +{{- $existing := (lookup "v1" "Secret" .Release.Namespace $name) -}} +{{- $key := "" -}} +{{- if .Values.secretKey -}} +{{- $key = .Values.secretKey -}} +{{- else if and $existing $existing.data (index $existing.data "SECRET_KEY") -}} +{{- $key = index $existing.data "SECRET_KEY" | b64dec -}} +{{- else -}} +{{- $key = randAlphaNum 50 -}} +{{- end -}} +apiVersion: v1 +kind: Secret +metadata: + name: {{ $name }} + labels: {{- include "frepple.labels" . | nindent 4 }} +type: Opaque +stringData: + SECRET_KEY: {{ $key | quote }} diff --git a/deploy/helm/frepple/values.yaml b/deploy/helm/frepple/values.yaml new file mode 100644 index 0000000000..0d8dec73b3 --- /dev/null +++ b/deploy/helm/frepple/values.yaml @@ -0,0 +1,74 @@ +# frePPLe staging/review deploy. See deploy/helm/frepple/README or the modernization plan. +nameOverride: "" + +image: + registry: ghcr.io/ledoent + app: frepple-app # engine image (e2e/Dockerfile.engine): Django + C++ engine + frontend: frepple-frontend # Next.js SPA (e2e/Dockerfile.frontend) + tag: latest + pullPolicy: IfNotPresent + +imagePullSecrets: + - name: ghcr-pull + +# Public host (same-origin: SPA + API + websockets behind one ingress). +host: frepple-staging.hz.ledoweb.com + +ingress: + className: nginx + clusterIssuer: letsencrypt-prod + tls: true + +# Postgres. Default: external (the shared CNPG instance). The credentials secret +# must contain POSTGRES_USER and POSTGRES_PASSWORD (created at deploy time from +# the shared-db superuser). frePPLe needs superuser/CREATEDB to build its +# scenario databases. +postgres: + mode: external # external | builtin + external: + host: shared-db-rw.cnpg.svc.cluster.local + port: 5432 + dbname: frepple # scenario DBs become frepple0/1/2 + credentialsSecret: frepple-db + builtin: + image: postgres:16 + storage: 5Gi + user: frepple + password: frepple + dbname: frepple + +redis: + image: redis:7 + resources: + requests: { cpu: 25m, memory: 64Mi } + limits: { cpu: 250m, memory: 256Mi } + +app: + replicas: 1 + # Run an initial plan on first boot so the screens have data. + initRunplan: true + # Load the demo dataset on first boot (FREPPLE_LOAD_DEMO). Set false for a + # blank scenario. + loadDemo: true + web: + resources: + requests: { cpu: 250m, memory: 1Gi } + limits: { cpu: "2", memory: 4Gi } + asgi: + resources: + requests: { cpu: 50m, memory: 256Mi } + limits: { cpu: "1", memory: 1Gi } + +frontend: + replicas: 1 + resources: + requests: { cpu: 25m, memory: 128Mi } + limits: { cpu: 500m, memory: 512Mi } + +# Django SECRET_KEY (+ JWT signing). Left empty -> the chart generates one and +# preserves it across upgrades via a lookup (templates/secret.yaml), wired into +# the app as FREPPLE_SECRETKEY. Set explicitly to pin/share a key. +secretKey: "" + +# FREPPLE_ALLOWED_HOSTS for the app. Defaults to {{ .Values.host }} when empty. +allowedHosts: "" diff --git a/djangosettings.py b/djangosettings.py index 7635d5ec21..7487eefd57 100644 --- a/djangosettings.py +++ b/djangosettings.py @@ -25,6 +25,7 @@ r""" Main Django configuration file. """ + import os import sys import pathlib @@ -35,14 +36,21 @@ DEBUG = "runserver" in sys.argv except Exception: DEBUG = False +# A deployment can force DEBUG off even when served via `runserver --insecure` +# (FREPPLE_DEBUG=false), so a public env isn't running with tracebacks on. +_debug_env = os.environ.get("FREPPLE_DEBUG") +if _debug_env is not None: + DEBUG = _debug_env.strip().lower() in ("1", "true", "yes", "on") DEBUG_JS = DEBUG ADMINS = ( # ('Your Name', 'your_email@domain.com'), ) -# Make this unique, and don't share it with anybody. -SECRET_KEY = "%@mzit!i8b*$zc&6oev96=RANDOMSTRING" +# Make this unique, and don't share it with anybody. A deployment should set +# FREPPLE_SECRETKEY (the Helm chart generates and persists one per release); the +# literal below is only a dev/test fallback and must not protect a real env. +SECRET_KEY = os.environ.get("FREPPLE_SECRETKEY") or "%@mzit!i8b*$zc&6oev96=RANDOMSTRING" # Configuration of the frepple database MIN_NUMBER_OF_SCENARIOS = 2 @@ -133,6 +141,7 @@ "freppledb.common", "django_filters", "rest_framework", + "drf_spectacular", "django.contrib.admin", "freppledb.archive", # The next two apps allow users to run their own SQL statements on @@ -487,7 +496,17 @@ # CSRF_TRUSTED_ORIGINS = ["https://yourserver", "https://*.yourdomain.com"] # SECURE_PROXY_SSL_HEADER = ("HTTP_X_FORWARDED_PROTO", "https") CSRF_TRUSTED_ORIGINS = os.environ.get("FREPPLE_CSRF_TRUSTED_ORIGINS", "").split() -SECURE_PROXY_SSL_HEADER = os.environ.get("FREPPLE_SECURE_PROXY_SSL_HEADER", "").split() +# Django unpacks this as `header, value = SECURE_PROXY_SSL_HEADER`, so it must be +# a 2-tuple (e.g. "HTTP_X_FORWARDED_PROTO https") or left unset. Only assign it +# when exactly two words are given, so a misconfigured value fails loudly here +# instead of raising ImproperlyConfigured on every request. +_proxy_ssl = os.environ.get("FREPPLE_SECURE_PROXY_SSL_HEADER", "").split() +if _proxy_ssl: + if len(_proxy_ssl) != 2: + raise ValueError( + "FREPPLE_SECURE_PROXY_SSL_HEADER must be two words ('HEADER value')" + ) + SECURE_PROXY_SSL_HEADER = tuple(_proxy_ssl) # Configuration of the ftp/sftp/ftps server where to upload reports # Note that for SFTP protocol, the host needs to be defined diff --git a/e2e/Dockerfile.engine b/e2e/Dockerfile.engine new file mode 100644 index 0000000000..99cdf1e85b --- /dev/null +++ b/e2e/Dockerfile.engine @@ -0,0 +1,66 @@ +# Engine image: the frePPLe C++ engine (Release) + Django, for the flows that +# need real planning - runplan -> live task progress. runplan shells out to the +# `frepple` executable (subprocess), so the importable extension is not required +# here; we build the executable + shared lib and skip the slow cmake venv target, +# then make our own venv for Django. +# +# Layer order is deliberate: the engine SOURCE (CMakeLists / src / include / bin / +# doc / contrib / requirements - everything add_subdirectory() and the venv +# target reference) is copied and compiled BEFORE the Django app, so a Python- or +# frontend-only change reuses the cached C++ build layer (paired with the +# workflow's buildx registry cache) instead of recompiling the whole engine. +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + DJANGO_SETTINGS_MODULE=freppledb.settings + +RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ + build-essential cmake g++ gcc libxerces-c-dev libssl-dev libpq-dev python3 \ + python3-dev python3-venv postgresql-client && rm -rf /var/lib/apt/lists/* + +WORKDIR /app + +# --- engine build inputs only (changing Python/JS does NOT invalidate this) --- +COPY CMakeLists.txt requirements.txt requirements.dev.txt ./ +COPY src ./src +COPY include ./include +COPY bin ./bin +COPY doc ./doc +COPY contrib ./contrib +COPY rust ./rust + +# Phase 7: optionally link the Rust forecast methods into the engine. Default OFF +# keeps the image lean and the e2e stack on the C++ path; deploy-staging builds +# with FREPPLE_RUST_FORECAST=ON, which makes CMake cargo-build + link the Rust +# staticlib (-ffp-contract=off, byte-parity-gated in CI) - so it needs rustup. +ARG FREPPLE_RUST_FORECAST=OFF +ENV PATH=/root/.cargo/bin:$PATH +RUN if [ "$FREPPLE_RUST_FORECAST" = "ON" ]; then \ + apt-get update -qq && apt-get install -y -qq --no-install-recommends curl ca-certificates \ + && curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain stable --profile minimal \ + && rm -rf /var/lib/apt/lists/* ; \ + fi + +# Build the engine (Release). Pre-create the venv + stamp so the engine targets' +# build-order dependency on the 'venv' target is satisfied without the slow +# pip-install-dev-requirements step, then pip-install the Django runtime reqs. +RUN cmake -B /app/build -DCMAKE_BUILD_TYPE=Release \ + -DFREPPLE_RUST_FORECAST=${FREPPLE_RUST_FORECAST} -S /app \ + && python3 -m venv /app/venv \ + && touch /app/build/venv.stamp \ + && cmake --build /app/build -j"$(nproc)" \ + && /app/venv/bin/pip install --no-cache-dir -r requirements.txt \ + && rm -rf /app/build /app/rust/frepple-forecast/target /root/.cache + +# --- the Django app + everything else (changes here skip the compile above) --- +COPY . /app + +ENV PATH=/app/bin:/app/venv/bin:$PATH \ + FREPPLE_HOME=/app/bin \ + FREPPLE_LOGDIR=/app/logs \ + LD_LIBRARY_PATH=/app/bin +RUN mkdir -p /app/logs && cp /app/e2e/entrypoint.sh /entrypoint.sh && chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/e2e/Dockerfile.frontend b/e2e/Dockerfile.frontend new file mode 100644 index 0000000000..c39ed8ccf6 --- /dev/null +++ b/e2e/Dockerfile.frontend @@ -0,0 +1,22 @@ +# Frontend image: production Next.js build. Uses Next's standalone output so the +# runtime stage ships only the traced server + deps (no source, no +# devDependencies), and runs as the non-root `node` user. +FROM node:22-slim AS build +WORKDIR /app +COPY frontend/package.json frontend/package-lock.json ./ +RUN npm ci +COPY frontend/ ./ +RUN npm run build + +FROM node:22-slim +WORKDIR /app +ENV NODE_ENV=production \ + PORT=3000 \ + HOSTNAME=0.0.0.0 +# Standalone bundle + the static assets + public dir it expects alongside it. +COPY --from=build --chown=node:node /app/.next/standalone ./ +COPY --from=build --chown=node:node /app/.next/static ./.next/static +COPY --from=build --chown=node:node /app/public ./public +USER node +EXPOSE 3000 +CMD ["node", "server.js"] diff --git a/e2e/Dockerfile.web b/e2e/Dockerfile.web new file mode 100644 index 0000000000..64705504b0 --- /dev/null +++ b/e2e/Dockerfile.web @@ -0,0 +1,25 @@ +# E2E web image: Django (WSGI) + daphne (ASGI) from source, WITHOUT the C++ +# engine. Verified that django.setup() and freppledb.asgi load engine-free (the +# asgi service loader tolerates a missing 'frepple' module). Planning/re-net +# features that need the engine (runplan, ForecastService re-net) are out of +# scope for this harness; the websocket/auth/read paths it exercises are not. +FROM ubuntu:24.04 + +ENV DEBIAN_FRONTEND=noninteractive \ + PYTHONUNBUFFERED=1 \ + DJANGO_SETTINGS_MODULE=freppledb.settings \ + PATH=/venv/bin:$PATH + +RUN apt-get update -qq && apt-get install -y -qq --no-install-recommends \ + python3 python3-venv python3-dev libpq-dev gcc libxerces-c-dev \ + postgresql-client && rm -rf /var/lib/apt/lists/* + +WORKDIR /app +COPY requirements.txt ./ +RUN python3 -m venv /venv && /venv/bin/pip install --no-cache-dir -q -r requirements.txt + +COPY . /app +ENV FREPPLE_HOME=/app/bin FREPPLE_LOGDIR=/app/logs +RUN mkdir -p /app/logs && cp /app/e2e/entrypoint.sh /entrypoint.sh && chmod +x /entrypoint.sh + +ENTRYPOINT ["/entrypoint.sh"] diff --git a/e2e/README.md b/e2e/README.md new file mode 100644 index 0000000000..21ccfa8da8 --- /dev/null +++ b/e2e/README.md @@ -0,0 +1,77 @@ +# E2E harness + +End-to-end verification of the modernized screens (Phase 1A Execute, Phase 1B +Forecast) against a real running stack. + +## What it runs + +A `docker compose` stack with **no C++ engine build** — Django and daphne run +engine-free (the ASGI loader tolerates a missing `frepple` module), so the stack +comes up in minutes: + +| service | role | +|-----------|------| +| `db` | postgres 16 | +| `redis` | channels layer (live task progress) | +| `web-wsgi`| Django dev server — REST API, `/api/token/`, output endpoints. Also does one-time DB init (createdatabase + migrate + demo data). | +| `web-asgi`| daphne — websockets (`ws/tasks/`, log tail) + engine HTTP services | +| `frontend`| production Next.js build (`/frontend`) | +| `nginx` | single same-origin proxy: `/ws`+`/forecast`+`/flush`→asgi, `/api`+`/data`→wsgi, `/`→spa | + +The origin is `http://127.0.0.1:18080`. + +## Run + +```sh +# from the repo root +docker compose -f e2e/docker-compose.yml up --build -d +# wait for web-wsgi to finish migrate + demo load (watch its logs) + +cd e2e/playwright +npm ci +npx playwright install --with-deps chromium +npm test + +# teardown +docker compose -f e2e/docker-compose.yml down -v +``` + +## Scope + +Covered: auth (`/api/token/`), the Execute websocket (token → subprotocol JWT → +`ws/tasks/` consumer), the enriched forecast read + pivot grid, and the +read-only **Demand Pegging Gantt** (picker + the enriched `/api/output/pegging/` +read; the engine-backed Gantt-render spec is gated on `E2E_ENGINE`). + +Out of scope (needs the compiled engine): launching a real plan and the override +re-net. Add an engine-backed service later to extend `fc-edit-parity` / +live-progress coverage. + +## Engine overlay (live planning) + +The lean stack omits the C++ engine. To verify real planning flows (runplan -> +live task progress), add the engine overlay and run with E2E_ENGINE=1: + +```sh +docker compose -f e2e/docker-compose.yml -f e2e/docker-compose.engine.yml up --build -d +cd e2e/playwright && E2E_ENGINE=1 npx playwright test +``` + +Note: forecast override re-net (ForecastService) needs the asgi to run inside the +frepple interpreter (frepplectl runwebservice); not yet wired here. + +The engine overlay sets `FREPPLE_INIT_RUNPLAN`, so `web-wsgi` computes one plan on +startup. That warms the engine (CPython + demo dataset) and fires the +Redis->websocket path once, so the live-progress test's own launch reaches a +terminal state in seconds instead of racing a cold engine. + +## CI + +`.github/workflows/frontend-e2e.yml` (`Frontend E2E (compose)`) runs this harness +**with the engine overlay** on pushes to `modernization` and on PRs into +`master`/`modernization` (path-filtered to `frontend/`, `e2e/`, `freppledb/`). The +compiled C++ engine is **restored from the deploy-staging buildx registry cache** +(`ghcr.io/ledoent/frepple-app:buildcache`) rather than recompiled, so the job is +~5 min. It waits for the startup warmup plan to reach `Done` before running the +full Playwright suite (smoke + a11y across all five screens + engine-backed +live-progress), making it the backward-compat guardrail for the SPA. diff --git a/e2e/docker-compose.engine.yml b/e2e/docker-compose.engine.yml new file mode 100644 index 0000000000..5090f6bcea --- /dev/null +++ b/e2e/docker-compose.engine.yml @@ -0,0 +1,25 @@ +# Engine-backed overlay: swaps the web roles to the frePPLe engine image so real +# planning works (runplan -> live task progress). Use together with the base: +# docker compose -f e2e/docker-compose.yml -f e2e/docker-compose.engine.yml up --build +# +# web-wsgi launches runplan (the worker shells out to the `frepple` executable); +# progress is broadcast via Redis and relayed by web-asgi to the browser. +services: + web-wsgi: + image: frepple-e2e-engine + build: + context: .. + dockerfile: e2e/Dockerfile.engine + # Compute an initial plan on startup so the C++ engine is warm (CPython + + # demo dataset loaded) before the first test-driven runplan, and so the + # Redis->websocket broadcast path is exercised once. Removes the cold-start + # race where the live-progress test launched a plan faster than a freshly + # built engine could complete + broadcast its terminal status. + environment: + FREPPLE_INIT_RUNPLAN: "1" + + web-asgi: + image: frepple-e2e-engine + build: + context: .. + dockerfile: e2e/Dockerfile.engine diff --git a/e2e/docker-compose.yml b/e2e/docker-compose.yml new file mode 100644 index 0000000000..f605df91d4 --- /dev/null +++ b/e2e/docker-compose.yml @@ -0,0 +1,71 @@ +# E2E stack for the modernized screens (Phase 1A/1B). Lean: no C++ engine build +# - Django + daphne run engine-free, so this comes up in minutes. nginx is the +# single same-origin proxy. Bring it up from the repo root: +# docker compose -f e2e/docker-compose.yml up --build +name: frepple-e2e + +services: + db: + image: postgres:16 + environment: + POSTGRES_USER: frepple + POSTGRES_PASSWORD: frepple + POSTGRES_DB: frepple + healthcheck: + test: ["CMD-SHELL", "pg_isready -U frepple"] + interval: 5s + timeout: 5s + retries: 10 + + redis: + image: redis:7 + + web-wsgi: + build: + context: .. + dockerfile: e2e/Dockerfile.web + command: ["wsgi"] + environment: + POSTGRES_HOST: db + POSTGRES_PORT: "5432" + REDIS_HOST: redis + REDIS_PORT: "6379" + FREPPLE_DATE_STYLE: day-month-year + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + + web-asgi: + build: + context: .. + dockerfile: e2e/Dockerfile.web + command: ["asgi"] + environment: + POSTGRES_HOST: db + POSTGRES_PORT: "5432" + REDIS_HOST: redis + REDIS_PORT: "6379" + FREPPLE_DATE_STYLE: day-month-year + depends_on: + db: + condition: service_healthy + redis: + condition: service_started + + frontend: + build: + context: .. + dockerfile: e2e/Dockerfile.frontend + + nginx: + image: nginx:1.27 + volumes: + - ./nginx.conf:/etc/nginx/conf.d/default.conf:ro + ports: + - "18080:80" + depends_on: + - web-wsgi + - web-asgi + - frontend diff --git a/e2e/entrypoint.sh b/e2e/entrypoint.sh new file mode 100755 index 0000000000..56a8020cf7 --- /dev/null +++ b/e2e/entrypoint.sh @@ -0,0 +1,55 @@ +#!/usr/bin/env bash +# Entrypoint for the E2E web image. First arg selects the role: +# wsgi - one-time DB init (createdatabase + migrate + demo data), then the +# Django dev server (REST API, /api/token/, output endpoints). +# asgi - daphne serving freppledb.asgi (websockets + engine services). +set -euo pipefail + +export POSTGRES_HOST="${POSTGRES_HOST:-db}" +export POSTGRES_PORT="${POSTGRES_PORT:-5432}" + +echo ">> waiting for postgres at ${POSTGRES_HOST}:${POSTGRES_PORT}" +until pg_isready -h "${POSTGRES_HOST}" -p "${POSTGRES_PORT}" \ + -U "${POSTGRES_USER:-frepple}" >/dev/null 2>&1; do + sleep 1 +done + +FREPPLECTL="python /app/frepplectl.py" + +case "${1:-wsgi}" in + wsgi) + echo ">> createdatabase + migrate" + ${FREPPLECTL} createdatabase --skip-if-exists || true + ${FREPPLECTL} migrate --noinput + # Demo data on by default; a deployment can skip it with FREPPLE_LOAD_DEMO=false. + if [ "${FREPPLE_LOAD_DEMO:-true}" != "false" ] && [ "${FREPPLE_LOAD_DEMO:-true}" != "0" ]; then + echo ">> loading demo data" + ${FREPPLECTL} loaddata demo --verbosity=0 || true + fi + # Marker so the asgi role can wait for init to finish. + ${FREPPLECTL} dbshell <<<"CREATE TABLE IF NOT EXISTS e2e_ready(ok int);" || true + # Optional: compute an initial plan so the screens have data (set by the Helm + # chart; --background returns immediately so it doesn't delay readiness). + if [ -n "${FREPPLE_INIT_RUNPLAN:-}" ]; then + echo ">> launching initial runplan" + ${FREPPLECTL} runplan --env=fcst,supply --background || true + fi + echo ">> starting Django (WSGI) on :8000" + # --insecure serves Django static (login/admin CSS) without a separate static + # server; production should front this with nginx/whitenoise instead. + exec ${FREPPLECTL} runserver --insecure 0.0.0.0:8000 + ;; + asgi) + echo ">> waiting for DB init (e2e_ready)" + until ${FREPPLECTL} dbshell <<<"SELECT 1 FROM e2e_ready;" >/dev/null 2>&1; do + sleep 1 + done + echo ">> starting daphne (ASGI) on :8001" + # daphne does not call django.setup(); frepple's asgi.py imports models at + # module load, so set Django up before daphne imports the application. + exec python -c "import django; django.setup(); from daphne.cli import CommandLineInterface as C; C().run(['-b','0.0.0.0','-p','8001','freppledb.asgi:application'])" + ;; + *) + exec "$@" + ;; +esac diff --git a/e2e/nginx.conf b/e2e/nginx.conf new file mode 100644 index 0000000000..29ef08da9b --- /dev/null +++ b/e2e/nginx.conf @@ -0,0 +1,43 @@ +# Single same-origin proxy for the E2E stack (resolved Q4: same-origin + JWT). +# Routes by path: websockets + engine services -> daphne (ASGI), REST/UI -> +# Django (WSGI), everything else -> the Next.js SPA. +# +# Routing table — keep in sync with the Helm Ingress +# (deploy/helm/frepple/templates/ingress.yaml) and the frontend next.config.mjs +# dev rewrites. These match the DEFAULT scenario (empty URL prefix) only. +upstream wsgi { server web-wsgi:8000; } +upstream asgi { server web-asgi:8001; } +upstream spa { server frontend:3000; } + +map $http_upgrade $connection_upgrade { + default upgrade; + '' close; +} + +server { + listen 80; + + # Websockets (live task progress / log tail). + location /ws/ { + proxy_pass http://asgi; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection $connection_upgrade; + proxy_set_header Host $http_host; + } + + # Engine HTTP services (forecast save / flush) - ASGI. Scoped to the exact + # service paths so the SPA's /forecast editor route is not shadowed. + location /forecast/detail/ { proxy_pass http://asgi; proxy_set_header Host $http_host; } + location /flush/ { proxy_pass http://asgi; proxy_set_header Host $http_host; } + + # REST API + Django UI/admin/login + task launch - WSGI. (The SPA owns + # /execute; only /execute/launch/* is the Django launch endpoint.) + location /api/ { proxy_pass http://wsgi; proxy_set_header Host $http_host; } + location /data/ { proxy_pass http://wsgi; proxy_set_header Host $http_host; } + location /static/ { proxy_pass http://wsgi; proxy_set_header Host $http_host; } + location /execute/launch/ { proxy_pass http://wsgi; proxy_set_header Host $http_host; } + + # Everything else - the SPA. + location / { proxy_pass http://spa; proxy_set_header Host $http_host; } +} diff --git a/e2e/playwright/.gitignore b/e2e/playwright/.gitignore new file mode 100644 index 0000000000..1314d03a6e --- /dev/null +++ b/e2e/playwright/.gitignore @@ -0,0 +1,6 @@ +/node_modules +/storage.json +/test-results +/playwright-report +/blob-report +/.cache diff --git a/e2e/playwright/global-setup.ts b/e2e/playwright/global-setup.ts new file mode 100644 index 0000000000..f504eb84f0 --- /dev/null +++ b/e2e/playwright/global-setup.ts @@ -0,0 +1,37 @@ +import { request, type FullConfig } from "@playwright/test"; + +// Log in to Django once and save the session cookie so every test (and the SPA's +// /api/token/ call) is authenticated. Done over the request API (GET the CSRF +// token, then POST credentials) rather than the UI - robust and matches the +// working curl flow. The demo fixture ships admin/admin. +async function globalSetup(_config: FullConfig) { + const base = process.env.E2E_BASE_URL || "http://127.0.0.1:18080"; + const ctx = await request.newContext({ baseURL: base }); + + const loginHtml = await (await ctx.get("/data/login/")).text(); + const csrf = loginHtml.match(/csrfmiddlewaretoken" value="([^"]+)"/)?.[1]; + if (!csrf) throw new Error("could not find csrfmiddlewaretoken on /data/login/"); + + await ctx.post("/data/login/", { + headers: { Referer: `${base}/data/login/` }, + form: { + username: "admin", + password: "admin", + csrfmiddlewaretoken: csrf, + next: "/", + }, + }); + + const state = await ctx.storageState(); + const hasSession = state.cookies.some((c) => c.name === "sessionid"); + if (!hasSession) { + throw new Error( + "login failed (no sessionid); cookies: " + + state.cookies.map((c) => c.name).join(","), + ); + } + await ctx.storageState({ path: "storage.json" }); + await ctx.dispose(); +} + +export default globalSetup; diff --git a/e2e/playwright/package-lock.json b/e2e/playwright/package-lock.json new file mode 100644 index 0000000000..327a2f8fdf --- /dev/null +++ b/e2e/playwright/package-lock.json @@ -0,0 +1,100 @@ +{ + "name": "frepple-e2e", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frepple-e2e", + "devDependencies": { + "@axe-core/playwright": "4.10.0", + "@playwright/test": "1.47.2" + } + }, + "node_modules/@axe-core/playwright": { + "version": "4.10.0", + "resolved": "https://registry.npmjs.org/@axe-core/playwright/-/playwright-4.10.0.tgz", + "integrity": "sha512-kEr3JPEVUSnKIYp/egV2jvFj+chIjCjPp3K3zlpJMza/CB3TFw8UZNbI9agEC2uMz4YbgAOyzlbUy0QS+OofFA==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "axe-core": "~4.10.0" + }, + "peerDependencies": { + "playwright-core": ">= 1.0.0" + } + }, + "node_modules/@playwright/test": { + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.47.2.tgz", + "integrity": "sha512-jTXRsoSPONAs8Za9QEQdyjFn+0ZQFjCiIztAIF6bi1HqhBzG9Ma7g1WotyiGqFSBRZjIEqMdT8RUlbk1QVhzCQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.47.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/axe-core": { + "version": "4.10.3", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.3.tgz", + "integrity": "sha512-Xm7bpRXnDSX2YE2YFfBk2FnF0ep6tmG7xPh8iHee8MIcrgq762Nkce856dYtJYLkuIoYZvGfTs/PbZhideTcEg==", + "dev": true, + "license": "MPL-2.0", + "engines": { + "node": ">=4" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.47.2.tgz", + "integrity": "sha512-nx1cLMmQWqmA3UsnjaaokyoUpdVaaDhJhMoxX2qj3McpjnsqFHs516QAKYhqHAgOP+oCFTEOCOAaD1RgD/RQfA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.47.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-core": { + "version": "1.47.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.47.2.tgz", + "integrity": "sha512-3JvMfF+9LJfe16l7AbSmU555PaTl2tPyQsVInqm3id16pdDfvZ8TTZ/pyzmkbDrZTQefyzU7AIHlZqQnxpqHVQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/e2e/playwright/package.json b/e2e/playwright/package.json new file mode 100644 index 0000000000..aa4d37b794 --- /dev/null +++ b/e2e/playwright/package.json @@ -0,0 +1,11 @@ +{ + "name": "frepple-e2e", + "private": true, + "scripts": { + "test": "playwright test" + }, + "devDependencies": { + "@axe-core/playwright": "4.10.0", + "@playwright/test": "1.47.2" + } +} diff --git a/e2e/playwright/playwright.config.ts b/e2e/playwright/playwright.config.ts new file mode 100644 index 0000000000..85ba7c6901 --- /dev/null +++ b/e2e/playwright/playwright.config.ts @@ -0,0 +1,18 @@ +import { defineConfig } from "@playwright/test"; + +// Drives the compose stack (e2e/docker-compose.yml) through the nginx origin. +// Start the stack first: docker compose -f e2e/docker-compose.yml up --build +export default defineConfig({ + testDir: "./tests", + timeout: 30_000, + expect: { timeout: 10_000 }, + fullyParallel: false, + retries: 0, + reporter: "list", + globalSetup: "./global-setup.ts", + use: { + baseURL: process.env.E2E_BASE_URL || "http://127.0.0.1:18080", + storageState: "storage.json", + trace: "on-first-retry", + }, +}); diff --git a/e2e/playwright/tests/a11y.spec.ts b/e2e/playwright/tests/a11y.spec.ts new file mode 100644 index 0000000000..260538f2cd --- /dev/null +++ b/e2e/playwright/tests/a11y.spec.ts @@ -0,0 +1,67 @@ +import { test, expect } from "@playwright/test"; +import AxeBuilder from "@axe-core/playwright"; + +// Accessibility gate (fc-a11y): the new screens must have zero CRITICAL axe +// violations. Run against the live stack so the rendered DOM (grid, inputs, +// chart) is scanned, not just markup. + +async function criticalViolations(page: import("@playwright/test").Page) { + const results = await new AxeBuilder({ page }).analyze(); + return results.violations.filter((v) => v.impact === "critical"); +} + +test("Forecast editor: 0 critical a11y violations", async ({ page }) => { + await page.goto("/forecast"); + await expect(page.getByRole("heading", { name: /Forecast/ })).toBeVisible(); + const critical = await criticalViolations(page); + expect(critical, JSON.stringify(critical.map((v) => v.id))).toEqual([]); +}); + +test("Execute screen: 0 critical a11y violations", async ({ page }) => { + await page.goto("/execute"); + await expect(page.getByRole("heading", { name: "Execute" })).toBeVisible(); + const critical = await criticalViolations(page); + expect(critical, JSON.stringify(critical.map((v) => v.id))).toEqual([]); +}); + +test("Inventory report: 0 critical a11y violations", async ({ page }) => { + await page.goto("/inventory"); + await expect(page.getByRole("heading", { name: "Inventory" })).toBeVisible(); + const critical = await criticalViolations(page); + expect(critical, JSON.stringify(critical.map((v) => v.id))).toEqual([]); +}); + +test("Demand report: 0 critical a11y violations", async ({ page }) => { + await page.goto("/demand"); + await expect(page.getByRole("heading", { name: "Demand" })).toBeVisible(); + const critical = await criticalViolations(page); + expect(critical, JSON.stringify(critical.map((v) => v.id))).toEqual([]); +}); + +test("Resource report: 0 critical a11y violations", async ({ page }) => { + await page.goto("/resource"); + await expect(page.getByRole("heading", { name: "Resource" })).toBeVisible(); + const critical = await criticalViolations(page); + expect(critical, JSON.stringify(critical.map((v) => v.id))).toEqual([]); +}); + +test("Pegging screen: 0 critical a11y violations", async ({ page }) => { + await page.goto("/pegging"); + await expect(page.getByRole("heading", { name: "Demand pegging" })).toBeVisible(); + const critical = await criticalViolations(page); + expect(critical, JSON.stringify(critical.map((v) => v.id))).toEqual([]); +}); + +test("Problems screen: 0 critical a11y violations", async ({ page }) => { + await page.goto("/problems"); + await expect(page.getByRole("heading", { name: "Problems" })).toBeVisible(); + const critical = await criticalViolations(page); + expect(critical, JSON.stringify(critical.map((v) => v.id))).toEqual([]); +}); + +test("Orders screen: 0 critical a11y violations", async ({ page }) => { + await page.goto("/orders"); + await expect(page.getByRole("heading", { name: "Orders" })).toBeVisible(); + const critical = await criticalViolations(page); + expect(critical, JSON.stringify(critical.map((v) => v.id))).toEqual([]); +}); diff --git a/e2e/playwright/tests/live-progress.spec.ts b/e2e/playwright/tests/live-progress.spec.ts new file mode 100644 index 0000000000..5b815b06e9 --- /dev/null +++ b/e2e/playwright/tests/live-progress.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from "@playwright/test"; + +// Engine-backed flow: launch a real plan from the Execute screen and watch its +// progress advance over the websocket to a terminal state - proving the full +// runplan -> Task.status -> Redis broadcast -> ws/tasks/ -> React loop. Only +// meaningful with the engine overlay, so it is skipped otherwise. +test.skip( + !process.env.E2E_ENGINE, + "needs the engine overlay (docker-compose.engine.yml + E2E_ENGINE=1)", +); + +test("Run plan streams live progress to a terminal state", async ({ page }) => { + await page.goto("/execute"); + await expect(page.locator('[title="live"]')).toBeVisible(); + + await page.getByRole("button", { name: /Run plan/ }).click(); + + // TaskProgressConsumer relays only *live* broadcasts - it sends no backlog on + // connect (asgi.py) - so the feed starts empty and the only task that can show + // up is the one this test just launched. The startup warmup plan + // (FREPPLE_INIT_RUNPLAN) finished before page load, so it never appears here; + // its job is purely to warm the engine so this launch reaches a terminal state + // in seconds. A page-wide match is therefore unambiguous. + await expect(page.getByText(/runplan/)).toBeVisible({ timeout: 30_000 }); + await expect(page.getByText(/Done|Failed/)).toBeVisible({ timeout: 90_000 }); +}); diff --git a/e2e/playwright/tests/orders-crud.spec.ts b/e2e/playwright/tests/orders-crud.spec.ts new file mode 100644 index 0000000000..b3dc3a5eff --- /dev/null +++ b/e2e/playwright/tests/orders-crud.spec.ts @@ -0,0 +1,46 @@ +import { test, expect } from "@playwright/test"; + +// Engine-backed (Phase 3 CRUD): inline-edit an order's quantity and delete an +// order on the Orders grid, asserting both persist through the DRF input API. +// Needs a computed plan (orders exist), so it's gated on the engine overlay. +// Mutates data; the CI stack is fresh per run. +test.skip( + !process.env.E2E_ENGINE, + "needs the engine overlay (docker-compose.engine.yml + E2E_ENGINE=1)", +); + +test("Inline-edit an order quantity and persist it", async ({ page }) => { + await page.goto("/orders"); + await expect(page.getByRole("heading", { name: "Orders" })).toBeVisible(); + const firstRow = page.locator("tbody tr").first(); + await expect(firstRow).toBeVisible({ timeout: 30_000 }); + + // Enter edit mode on the first (editable) row. + await firstRow.getByRole("button", { name: /^Edit / }).click(); + const qty = firstRow.getByLabel("Qty"); + await expect(qty).toBeVisible(); + await qty.fill("123"); + await firstRow.getByRole("button", { name: "Save" }).click(); + + // Saved toast + the value persists after the reload. + await expect(page.getByText(/Saved/i)).toBeVisible({ timeout: 15_000 }); + await expect(page.locator("tbody tr").first().getByText("123")).toBeVisible({ + timeout: 15_000, + }); +}); + +test("Delete an order with inline confirm", async ({ page }) => { + await page.goto("/orders"); + const firstRow = page.locator("tbody tr").first(); + await expect(firstRow).toBeVisible({ timeout: 30_000 }); + const countBefore = await page.locator("tbody tr").count(); + + await firstRow.getByRole("button", { name: /^Delete / }).click(); + await firstRow.getByRole("button", { name: "Yes" }).click(); + + // Deleted toast + the reloaded grid has exactly one fewer row. + await expect(page.getByText(/Deleted/i)).toBeVisible({ timeout: 15_000 }); + await expect(page.locator("tbody tr")).toHaveCount(countBefore - 1, { + timeout: 15_000, + }); +}); diff --git a/e2e/playwright/tests/pegging-replan.spec.ts b/e2e/playwright/tests/pegging-replan.spec.ts new file mode 100644 index 0000000000..5214608c23 --- /dev/null +++ b/e2e/playwright/tests/pegging-replan.spec.ts @@ -0,0 +1,42 @@ +import { test, expect } from "@playwright/test"; + +// Engine-backed (D3): after a reschedule, the affected downstream chain is +// highlighted and a "Re-plan now" button appears; clicking it runs the engine +// (runplan), waits for it over the task websocket, and refreshes the peg. Needs +// the engine overlay (real runplan + ws). Mutates data; the CI stack is fresh. +test.skip( + !process.env.E2E_ENGINE, + "needs the engine overlay (docker-compose.engine.yml + E2E_ENGINE=1)", +); + +test("Reschedule flags downstream + re-plan refreshes in place", async ({ page }) => { + await page.goto("/pegging?demand=Demand%2001"); + const gantt = page.getByRole("table", { name: "Demand pegging Gantt" }); + await expect(gantt).toBeVisible({ timeout: 30_000 }); + + // Drag an editable bar ~90px to reschedule it (reuses the D2 gesture). + const bar = page.locator(".gantt-bar--editable").first(); + await expect(bar).toBeVisible({ timeout: 30_000 }); + const box = await bar.boundingBox(); + if (!box) throw new Error("no editable bar box"); + await page.mouse.move(box.x + box.width / 2, box.y + box.height / 2); + await page.mouse.down(); + await page.mouse.move(box.x + box.width / 2 + 90, box.y + box.height / 2, { + steps: 8, + }); + await page.mouse.up(); + + // D3: the reschedule flags the downstream chain + offers an in-place re-plan. + await expect(page.getByText(/Highlighted steps may shift/i)).toBeVisible({ + timeout: 15_000, + }); + await expect(page.locator(".gantt-row--affected").first()).toBeVisible(); + const replanBtn = page.getByRole("button", { name: /Re-plan now/i }); + await expect(replanBtn).toBeVisible(); + + // Run the engine in place; the loop watches the task ws to completion. + await replanBtn.click(); + await expect(page.getByText(/Re-planned/i)).toBeVisible({ timeout: 90_000 }); + // The stale banner clears once the peg is refreshed. + await expect(page.getByText(/Highlighted steps may shift/i)).toBeHidden(); +}); diff --git a/e2e/playwright/tests/pegging-reschedule.spec.ts b/e2e/playwright/tests/pegging-reschedule.spec.ts new file mode 100644 index 0000000000..118374d7e4 --- /dev/null +++ b/e2e/playwright/tests/pegging-reschedule.spec.ts @@ -0,0 +1,35 @@ +import { test, expect } from "@playwright/test"; + +// Engine-backed (D2): drag an editable operationplan bar on the pegging Gantt and +// confirm the reschedule round-trips - the drag -> PATCH /api/input/// +// -> persisted -> reload loop. Needs a computed plan, so it's gated on the engine +// overlay. Mutates data, but the CI stack is fresh per run (warmup plan only). +test.skip( + !process.env.E2E_ENGINE, + "needs the engine overlay (docker-compose.engine.yml + E2E_ENGINE=1)", +); + +test("Dragging a bar reschedules the operationplan", async ({ page }) => { + await page.goto("/pegging?demand=Demand%2001"); + const gantt = page.getByRole("table", { name: "Demand pegging Gantt" }); + await expect(gantt).toBeVisible({ timeout: 30_000 }); + + // An editable bar (proposed MO/PO/etc - deliveries are zero-width, so prefer a + // bar with real width by taking the first editable one and checking its box). + const bar = page.locator(".gantt-bar--editable").first(); + await expect(bar).toBeVisible({ timeout: 30_000 }); + const box = await bar.boundingBox(); + if (!box) throw new Error("no editable bar box"); + + // Drag it ~90px to the right via pointer (the bar uses pointer events). + const cx = box.x + box.width / 2; + const cy = box.y + box.height / 2; + await page.mouse.move(cx, cy); + await page.mouse.down(); + await page.mouse.move(cx + 90, cy, { steps: 8 }); + await page.mouse.up(); + + // The PATCH succeeded -> a success toast, and the "peg is stale, re-plan" hint. + await expect(page.getByText(/Rescheduled/i)).toBeVisible({ timeout: 15_000 }); + await expect(page.getByRole("button", { name: /Re-plan now/i })).toBeVisible(); +}); diff --git a/e2e/playwright/tests/pegging.spec.ts b/e2e/playwright/tests/pegging.spec.ts new file mode 100644 index 0000000000..67b5b1a9f4 --- /dev/null +++ b/e2e/playwright/tests/pegging.spec.ts @@ -0,0 +1,26 @@ +import { test, expect } from "@playwright/test"; + +// Engine-backed: pick a planned demand and confirm its pegging Gantt renders the +// supply-chain tree (rows + bars) on a dated axis - proving the enriched +// /api/output/pegging// read (window header + tree) + the SVG-less Gantt +// geometry end-to-end. Needs a computed plan, so it's gated on the engine overlay. +test.skip( + !process.env.E2E_ENGINE, + "needs the engine overlay (docker-compose.engine.yml + E2E_ENGINE=1)", +); + +test("Demand pegging Gantt renders a supply-chain trace", async ({ page }) => { + await page.goto("/pegging?demand=Demand%2001"); + await expect(page.getByRole("heading", { name: "Demand pegging" })).toBeVisible(); + + // The Gantt appears once the pegging read resolves, and carries at least one + // tree row with a bar (the demo "Demand 01" is planned with pegging). + const gantt = page.getByRole("table", { name: "Demand pegging Gantt" }); + await expect(gantt).toBeVisible({ timeout: 30_000 }); + await expect(gantt.locator(".gantt-bar").first()).toBeVisible({ + timeout: 30_000, + }); + + // The delivery step (depth 1) is a DLVR row - the root of the peg. + await expect(gantt.getByText("DLVR").first()).toBeVisible(); +}); diff --git a/e2e/playwright/tests/smoke.spec.ts b/e2e/playwright/tests/smoke.spec.ts new file mode 100644 index 0000000000..9c08d8524c --- /dev/null +++ b/e2e/playwright/tests/smoke.spec.ts @@ -0,0 +1,102 @@ +import { test, expect } from "@playwright/test"; + +// Verifies the screens we built against the running stack: the auth -> token -> +// websocket -> React path (Execute) and the enriched forecast read (Forecast). +// Engine-only flows (launch a real plan, override re-net) are out of scope here. + +test("/api/token/ mints a JWT for the session user", async ({ request }) => { + const res = await request.get("/api/token/"); + expect(res.ok()).toBeTruthy(); + const body = await res.json(); + expect(typeof body.token).toBe("string"); + expect(body.token.length).toBeGreaterThan(20); +}); + +test("Execute screen connects the task websocket", async ({ page }) => { + await page.goto("/execute"); + await expect(page.getByRole("heading", { name: "Execute" })).toBeVisible(); + // The connection dot's title flips to "live" once ws/tasks/ is open - this + // exercises token acquisition + the subprotocol-carried JWT + the consumer. + await expect(page.locator('[title="live"]')).toBeVisible(); +}); + +test("Forecast editor loads without error", async ({ page }) => { + await page.goto("/forecast"); + await expect(page.getByRole("heading", { name: /Forecast/ })).toBeVisible(); + // Either the grid renders (one or more "Override" rows) or the empty-state + // shows; both mean the enriched /api/output/forecast/ read + pivot worked + // (no error). With real plan data the grid has many Override cells, so match + // the first to stay out of strict-mode. + await expect( + page + .locator("text=Override") + .first() + .or(page.locator("text=No forecast series.")), + ).toBeVisible(); +}); + +test("Inventory report loads without error", async ({ page }) => { + await page.goto("/inventory"); + await expect(page.getByRole("heading", { name: "Inventory" })).toBeVisible(); + // Either buffers render (a measure label) or the empty-state shows; both mean + // the enriched /api/output/inventory/ read + generic pivot parse worked. + await expect( + page + .getByText("Start OH") + .first() + .or(page.getByText("No inventory buffers.")), + ).toBeVisible(); +}); + +test("Demand report loads without error", async ({ page }) => { + await page.goto("/demand"); + await expect(page.getByRole("heading", { name: "Demand" })).toBeVisible(); + // The grid caption (unique) or the empty state - both mean the enriched + // /api/output/demand/ read + generic pivot parse worked. + await expect( + page.getByText(/Demand by series over/).or(page.getByText("No demand series.")), + ).toBeVisible(); +}); + +test("Resource report loads without error", async ({ page }) => { + await page.goto("/resource"); + await expect(page.getByRole("heading", { name: "Resource" })).toBeVisible(); + await expect( + page.getByText(/Resource by series over/).or(page.getByText("No resources.")), + ).toBeVisible(); +}); + +test("Pegging screen loads and lists demands", async ({ page }) => { + await page.goto("/pegging"); + await expect(page.getByRole("heading", { name: "Demand pegging" })).toBeVisible(); + // The demand picker is populated from /api/input/demand/; selecting one and + // the Gantt render is the engine-backed test. Here we only assert the picker + // loaded (a demand option) or the no-match empty state - both mean the list + // read worked without error. + await expect( + page + .getByRole("option") + .first() + .or(page.getByText("NO DEMANDS MATCH")), + ).toBeVisible(); +}); + +test("Problems screen loads with tabs", async ({ page }) => { + await page.goto("/problems"); + await expect(page.getByRole("heading", { name: "Problems" })).toBeVisible(); + // The Problems/Constraints tabs render; the list or empty-state both mean the + // /api/output/problem/ read worked. + await expect(page.getByRole("tab", { name: "Constraints" })).toBeVisible(); + await expect( + page.getByText(/rows/).or(page.getByText(/NO PROBLEMS/)), + ).toBeVisible(); +}); + +test("Orders screen loads MO/PO/DO tabs", async ({ page }) => { + await page.goto("/orders"); + await expect(page.getByRole("heading", { name: "Orders" })).toBeVisible(); + await expect(page.getByRole("tab", { name: "Purchase" })).toBeVisible(); + await expect( + page.getByText(/rows/).or(page.getByText(/NO ORDERS/)), + ).toBeVisible(); +}); diff --git a/freppledb/asgi.py b/freppledb/asgi.py index b2abac69e7..a3bc43d6de 100644 --- a/freppledb/asgi.py +++ b/freppledb/asgi.py @@ -21,14 +21,16 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # +import asyncio import base64 from importlib import import_module import json -import jwt import logging import os import sys +from urllib.parse import parse_qs +from asgiref.sync import sync_to_async from django.conf import settings from django.contrib.auth import authenticate from django.db import DEFAULT_DB_ALIAS @@ -36,12 +38,12 @@ from django.contrib.auth.models import AnonymousUser from freppledb.common.models import User, APIKey -from freppledb.common.utils import get_databases +from freppledb.common.jwtauth import decode_jwt, extract_scenario from channels.auth import AuthMiddleware from channels.db import database_sync_to_async from channels.generic.http import AsyncHttpConsumer -from channels.generic.websocket import WebsocketConsumer +from channels.generic.websocket import AsyncWebsocketConsumer from channels.middleware import BaseMiddleware from channels.routing import ProtocolTypeRouter, URLRouter from channels.security.websocket import AllowedHostsOriginValidator @@ -59,8 +61,6 @@ serviceRegistry = {} -connected = set() - def registerService(key): def inner(func): @@ -78,29 +78,196 @@ def inner(func): try: mod = import_module("%s.services" % app) except ModuleNotFoundError as e: - # Silently ignore if the missing module is called urls - if "services" not in str(e): + # Skip if the app simply has no services module, or if the engine module + # ("frepple") isn't importable in this process. The latter lets asgi.py be + # imported in a plain Django/test/schema context (where the embedded C++ + # interpreter is absent) instead of only inside the worker; the engine-only + # services it would have registered are meaningless there anyway. + if e.name != "frepple" and not (e.name or "").endswith(".services"): raise e -# class WebsocketService(WebsocketConsumer): -# def connect(self): -# self.user = self.scope["user"] -# print("connecting", self.user) -# connected.add(self) -# self.accept() +class WebsocketService(AsyncWebsocketConsumer): + """ + Minimal authenticated websocket endpoint (Phase 0 beachhead). + + Authentication and scenario routing are handled by the middleware stack + (TokenMiddleware sets scope["database"] from the URL/header and scope["user"] + from the JWT/session). This consumer only refuses unauthenticated connections + and echoes messages tagged with the resolved scenario, so a client can confirm + which database it is bound to. Phase 1A grows this into live task/log streaming. + """ + + async def connect(self): + # is_active is a plain attribute on AnonymousUser (unlike is_authenticated, + # which is a property that reads truthy on the class), so it reliably + # rejects both anonymous and inactive users. + user = self.scope.get("user") + if not user or not getattr(user, "is_active", False): + await self.close(code=4401) + return + await self.accept(subprotocol=self.scope.get("jwt_subprotocol")) + + async def receive(self, text_data=None, bytes_data=None): + try: + payload = json.loads(text_data) if text_data else {} + except (TypeError, ValueError): + payload = {} + await self.send( + text_data=json.dumps( + { + "message": payload.get("message"), + "database": self.scope.get("database"), + "user": getattr(self.scope.get("user"), "username", None), + } + ) + ) + + +class TaskProgressConsumer(AsyncWebsocketConsumer): + """ + Live task progress for the Execute screen (Phase 1A). + + Subscribes to the scenario's ``tasks.`` channel-layer group and + relays the messages that ``Task`` post-save broadcasts (status/progress, + started/finished), so the UI advances from server pushes instead of polling. + Authentication + scenario routing are handled by the middleware stack. + """ + + async def connect(self): + user = self.scope.get("user") + if not user or not getattr(user, "is_active", False): + await self.close(code=4401) + return + if self.channel_layer is None: + # No channel layer configured: nothing to subscribe to. + await self.close(code=1011) + return + self.group = "tasks.%s" % self.scope.get("database", DEFAULT_DB_ALIAS) + await self.channel_layer.group_add(self.group, self.channel_name) + await self.accept(subprotocol=self.scope.get("jwt_subprotocol")) + + async def disconnect(self, close_code): + group = getattr(self, "group", None) + if group and self.channel_layer is not None: + await self.channel_layer.group_discard(group, self.channel_name) + + async def task_update(self, event): + # Handler for {"type": "task.update", "task": {...}} group messages. + await self.send(text_data=json.dumps(event["task"])) + + +def _read_since(path, pos): + """Read bytes appended to `path` since byte offset `pos`. Returns + (text, new_pos). Tolerates the file not existing yet and truncation/rotation + (offset reset). Decodes lossily - a log read mid-line must not raise.""" + try: + with open(path, "rb") as f: + f.seek(0, os.SEEK_END) + size = f.tell() + if size < pos: + pos = 0 # file was truncated or rotated + f.seek(pos) + data = f.read() + return data.decode("utf-8", "ignore"), f.tell() + except FileNotFoundError: + return "", pos + + +async def stream_logfile(path, is_finished, send_json, poll=0.5): + """ + Tail a log file: call send_json({"log": }) for each appended chunk, + and send_json({"done": True}) once is_finished() is true and nothing more is + left to read. ``is_finished`` and ``send_json`` are async callables. The file + read runs off the event loop so a large/slow log can't block other sockets. + + Pure (no DB, no channels) so it is unit-testable on its own; the consumer + wires it to the task's logfile path and status. + """ + pos = 0 + while True: + chunk, pos = await sync_to_async(_read_since, thread_sensitive=False)(path, pos) + if chunk: + await send_json({"log": chunk}) + if not chunk and await is_finished(): + await send_json({"done": True}) + return + await asyncio.sleep(poll) + + +class TaskLogConsumer(AsyncWebsocketConsumer): + """ + Live log tail for the Execute screen (Phase 1A-2). + + Streams a task's logfile (Task.logfile under FREPPLE_LOGDIR) over + ws/tasks//log/ as the worker appends to it, finishing when the task's + status becomes terminal. The worker writes the file on a filesystem shared + with the web pods (a deployment concern, like Redis is for progress). + """ + + POLL = 0.5 # seconds; the live-tail gate requires sub-1s latency -# def disconnect(self, close_code): -# print("disconnecting") -# connected.remove(self) -# pass + async def connect(self): + user = self.scope.get("user") + if not user or not getattr(user, "is_active", False): + await self.close(code=4401) + return + try: + self.taskid = int(self.scope["url_route"]["kwargs"]["taskid"]) + except (KeyError, ValueError, TypeError): + await self.close(code=4400) + return + self.database = self.scope.get("database", DEFAULT_DB_ALIAS) + # Test seam: an injected path/poll lets the tailing be exercised without + # a database round-trip (see test_api_phase1a). + path = self.scope.get("logpath_override") or await self._logpath() + if not path: + await self.close(code=4404) + return + await self.accept(subprotocol=self.scope.get("jwt_subprotocol")) + self._tailer = asyncio.create_task( + stream_logfile( + path, + self._is_finished, + self._send_json, + poll=self.scope.get("logpoll_override", self.POLL), + ) + ) -# def receive(self, text_data): -# print("receive", text_data, self.scope) -# text_data_json = json.loads(text_data) -# message = text_data_json["message"] + async def disconnect(self, close_code): + tailer = getattr(self, "_tailer", None) + if tailer: + tailer.cancel() -# self.send(text_data=json.dumps({"message": message})) + async def _send_json(self, payload): + await self.send(text_data=json.dumps(payload)) + + @database_sync_to_async + def _logpath(self): + from freppledb.execute.models import Task + + try: + fn = Task.objects.using(self.database).get(id=self.taskid).logfile + except Exception: + return None + if not fn or not str(fn).lower().endswith(".log"): + return None + return os.path.join(settings.FREPPLE_LOGDIR, fn) + + @database_sync_to_async + def _finished_in_db(self): + from freppledb.execute.models import Task + + try: + status = Task.objects.using(self.database).get(id=self.taskid).status + except Exception: + return True # task vanished: stop streaming + return (status or "").strip().lower() in ("done", "failed", "canceled") + + async def _is_finished(self): + if "finished_override" in self.scope: + return self.scope["finished_override"] + return await self._finished_in_db() class HTTPNotFound(AsyncHttpConsumer): @@ -137,10 +304,51 @@ def get_user_by_apikey(key, database=DEFAULT_DB_ALIAS): return AnonymousUser() +def _extract_credentials(scope, headers): + """ + Find the request credentials across all carriers frePPLe clients use. + + Returns ``(scheme, credentials, subprotocol)`` where ``scheme`` is + "bearer"/"basic"/None. Besides the HTTP ``Authorization`` header, browser + websocket clients (which cannot set request headers) may pass the JWT either + as a ``Sec-WebSocket-Protocol`` subprotocol (``["bearer", ""]``, parsed + into ``scope["subprotocols"]``) or as a ``?token=`` query parameter. When the + subprotocol carrier is used, the negotiated subprotocol is returned so the + consumer can echo it back in the handshake. + """ + # 1. Authorization header (HTTP / API clients). + for k, v in headers: + if k == b"authorization": + parts = v.decode("ascii").split() + if len(parts) == 2: + return parts[0].lower(), parts[1], None + break + + # 2. Websocket subprotocol carrier: ["bearer", ""]. + subs = [ + s.decode("ascii") if isinstance(s, bytes) else s + for s in (scope.get("subprotocols") or []) + ] + if len(subs) >= 2 and subs[0].strip().lower() == "bearer": + return "bearer", subs[1].strip(), "bearer" + + # 3. Websocket query-string carrier: ?token=. + qs = scope.get("query_string", b"") + if qs: + token = parse_qs(qs.decode("ascii")).get("token", [None])[0] + if token: + return "bearer", token, None + + return None, None, None + + class TokenMiddleware(BaseMiddleware): """ - - adds scenario database to the scope - - add user to the scope if a JWT token is present + - resolves the scenario database from the URL prefix / X-Frepple-Scenario + header (falling back to the FREPPLE_DATABASE env var for single-scenario + deployments) and strips the prefix from the path, mirroring the WSGI + MultiDBMiddleware + - adds the resolved user to the scope from a JWT/API-key/basic credential """ def __init__(self, app): @@ -148,60 +356,45 @@ def __init__(self, app): super().__init__(app) async def __call__(self, scope, receive, send): - scope["database"] = self.database + headers = scope.get("headers") or [] + database, path = extract_scenario( + scope.get("path", ""), headers, default=self.database + ) + scope["database"] = database + if "path" in scope: + scope["path"] = path try: - if "headers" in scope: - for h in scope["headers"]: - if h[0] == b"authorization": - auth = h[1].decode("ascii").split() - if auth[0].lower() == "bearer": - # JWT webtoken or APIkey authentication - decoded = None - for secret in ( - getattr(settings, "AUTH_SECRET_KEY", None), - get_databases()[self.database].get( - "SECRET_WEBTOKEN_KEY", settings.SECRET_KEY - ), - ): - if secret: - try: - decoded = jwt.decode( - auth[1], - secret, - algorithms=["HS256"], - ) - if "user" in decoded: - scope["user"] = await get_user( - username=decoded["user"], - database=scope["database"], - ) - elif "email" in decoded: - scope["user"] = await get_user( - email=decoded["email"], - database=scope["database"], - ) - except Exception: - continue - if not decoded: - # Try API key authentication - try: - scope["user"] = await get_user_by_apikey( - auth[1], database=scope["database"] - ) - except Exception: - pass - elif auth[0].lower() == "basic": - # Basic authentication - args = ( - base64.b64decode(auth[1]) - .decode("iso-8859-1") - .split(":", 1) - ) - scope["user"] = await get_user( - username=args[0], - password=args[1], - database=scope["database"], - ) + scheme, credentials, subprotocol = _extract_credentials(scope, headers) + if scheme == "bearer" and credentials: + if subprotocol: + scope["jwt_subprotocol"] = subprotocol + # JWT webtoken or API-key authentication. + try: + decoded = decode_jwt(credentials, database) + except Exception: + # Expired/invalid token: treat as unauthenticated. + decoded = None + if decoded and "user" in decoded: + scope["user"] = await get_user( + username=decoded["user"], database=database + ) + elif decoded and "email" in decoded: + scope["user"] = await get_user( + email=decoded["email"], database=database + ) + elif not decoded: + # Not a JWT for any scenario secret: try API-key auth. + try: + scope["user"] = await get_user_by_apikey( + credentials, database=database + ) + except Exception: + pass + elif scheme == "basic" and credentials: + args = base64.b64decode(credentials).decode("iso-8859-1").split(":", 1) + scope["user"] = await get_user( + username=args[0], password=args[1], database=database + ) except Exception: pass return await super().__call__(scope, receive, send) @@ -215,7 +408,7 @@ class AuthAndPermissionMiddleware(AuthMiddleware): async def __call__(self, scope, receive, send): usr = scope.get("user", None) if not usr: - scope["user"] = AnonymousUser + scope["user"] = AnonymousUser() elif usr.is_authenticated and not usr.is_superuser: await database_sync_to_async(usr.get_all_permissions)() return await super().__call__(scope, receive, send) @@ -280,8 +473,8 @@ async def __call__(self, scope, receive, send): ) try: return await super().__call__(scope, receive, send) - except Exception as e: - print("Error:", e) + except Exception: + logger.exception("ASGI request failed") scope["response_headers"].append((b"Content-Type", b"text/plain")) await send( { @@ -314,10 +507,33 @@ async def __call__(self, scope, receive, send): ) ) ), - # "websocket": AllowedHostsOriginValidator( - # AuthenticatedMiddleware(AuthMiddlewareStack( - # URLRouter([re_path(r"ws/", WebsocketService.as_asgi())]) - # )) - # ), + # Websockets reuse the same cookie/session + token + permission stack as + # HTTP (so same-origin browser clients authenticate by session cookie and + # token clients by JWT), but the per-connection auth gate lives in the + # consumer's connect() rather than AuthenticatedMiddleware (which is + # HTTP-only: it inspects scope["method"] and writes an HTTP 401 response). + "websocket": AllowedHostsOriginValidator( + CookieMiddleware( + SessionMiddleware( + TokenMiddleware( + AuthAndPermissionMiddleware( + URLRouter( + [ + re_path( + r"^ws/tasks/$", + TaskProgressConsumer.as_asgi(), + ), + re_path( + r"^ws/tasks/(?P[0-9]+)/log/$", + TaskLogConsumer.as_asgi(), + ), + re_path(r"^ws/$", WebsocketService.as_asgi()), + ] + ) + ) + ) + ) + ) + ), } ) diff --git a/freppledb/common/api/output.py b/freppledb/common/api/output.py new file mode 100644 index 0000000000..9008e3b7dc --- /dev/null +++ b/freppledb/common/api/output.py @@ -0,0 +1,221 @@ +# +# Copyright (C) 2026 by frePPLe bv +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +JSON output API for plan/forecast reports (Phase 0 modernization). + +frePPLe's report engine (GridReport/GridPivot) already streams its grid data as +JSON when called with ?format=json: it builds the data with a single raw-SQL +statement and a chunked cursor, then streams it via StreamingHttpResponse +(see freppledb/common/report.py). That path uses NO DRF serializer, which is +exactly what we want for the output endpoints (serializer cost avoided). + +Rather than re-implement any of that, JSONStreamView simply forces format=json +and delegates to the report's own class-based view. The report's dispatch() +runs its normal setup (permission checks, time-bucket and filter handling, +scenario database selection via request.database) and streams the JSON. This +keeps the API a thin, drift-free wrapper over the canonical query path. + +Wire a report as a JSON endpoint with, e.g.: + + JSONStreamView.as_view(report_class=InventoryReport) +""" + +import json + +from django.http import StreamingHttpResponse +from django.views import View + + +class JSONStreamView(View): + """Expose a GridReport/GridPivot as a streaming-JSON REST endpoint. + + Set the ``report_class`` attribute (typically via ``as_view(report_class=...)``) + to any GridReport subclass. Query parameters supported by the report - + filters (``?filters=``, ``field__op=``), time buckets (``?buckets=``, + ``?startdate=``, ``?enddate=``) and pagination (``?page=``, ``?rows=``) - + pass straight through. + """ + + # The GridReport / GridPivot subclass to render. Required. + report_class = None + + # Reuse the report's HTTP method support. + http_method_names = ["get", "head", "options"] + + def _run_report(self, request, *args, **kwargs): + """Force JSON output and run the report's own view. That view carries the + permission gate and streams the raw-SQL result itself, so this is the one + place subclasses go through to get the (auth-checked) inner response.""" + if self.report_class is None: + raise ValueError("%s requires a report_class" % type(self).__name__) + if request.GET.get("format") != "json": + request.GET = request.GET.copy() + request.GET["format"] = "json" + return self.report_class.as_view()(request, *args, **kwargs) + + @staticmethod + def _wrap(inner, header): + """Prefix the report's streamed JSON object with ``header`` (which opens a + new object and ends with the wrapped key, e.g. ``{"window":…,"data":``) + and append the closing brace - so the report's body stays byte-identical + under that key. Shared by the enriched subclasses.""" + + def stream(): + yield header.encode("utf-8") + for chunk in inner.streaming_content: + yield chunk if isinstance(chunk, bytes) else str(chunk).encode("utf-8") + yield b"}" + + return StreamingHttpResponse(stream(), content_type="application/json") + + def dispatch(self, request, *args, **kwargs): + return self._run_report(request, *args, **kwargs) + + +class PivotJSONStreamView(JSONStreamView): + """ + GridPivot OUTPUT enriched for the SPA (Phase 1B forecast, Phase 3 inventory…). + + The bare pivot stream's per-bucket arrays are not self-describing: the client + needs the measure (crosses) order to map array slots to named measures, and + each bucket's start/end dates. Those come from the report's ``crosses`` and + ``getBuckets()`` - so this wraps the report's own ``{total,page,records,rows}`` + object unchanged under ``data`` and prepends a ``measures`` + ``buckets`` + header. The wrapped ``data`` is byte-identical to the legacy ``?format=json`` + stream, so any report (forecast, inventory, …) can opt in without changing the + underlying values. + """ + + def dispatch(self, request, *args, **kwargs): + rc = self.report_class + # Run the report view FIRST — it carries the auth/permission gate. Only a + # streaming (authorized) response gets the metadata wrapper; a denial is + # passed through untouched, before we spend any query on measures/buckets. + inner = self._run_report(request, *args, **kwargs) + if not getattr(inner, "streaming", False): + return inner # e.g. a permission denial - pass through unchanged + + # Metadata extraction must never break the data stream: on any failure we + # fall back to empty measures/buckets (the client then uses its defaults). + measures = [] + try: + crosses = ( + rc.crosses(request, *args, **kwargs) + if callable(rc.crosses) + else rc.crosses + ) + measures = [ + c[0] for c in crosses if len(c) < 2 or (c[1] or {}).get("visible", True) + ] + except Exception: + measures = [] + + buckets = [] + try: + rc.getBuckets(request, *args, **kwargs) + for b in getattr(request, "report_bucketlist", []) or []: + buckets.append( + { + "name": b["name"], + "startdate": ( + b["startdate"].isoformat() if b["startdate"] else None + ), + "enddate": b["enddate"].isoformat() if b["enddate"] else None, + } + ) + except Exception: + buckets = [] + + header = ('{"measures":%s,"buckets":%s,"data":') % ( + json.dumps(measures), + json.dumps(buckets), + ) + return self._wrap(inner, header) + + +# Back-compat alias: the forecast endpoint and tests still import this name. +ForecastJSONStreamView = PivotJSONStreamView + + +class PeggingJSONView(JSONStreamView): + """ + Demand-pegging GANTT output enriched for the SPA (Phase 3-D). + + The pegging report (``output.views.pegging.ReportByDemand``) already streams + the supply-chain tree as ``{total,page,records,rows}`` - each row a tree node + with an ``operationplans`` array of bars carrying raw ``startdate``/``enddate``. + But the bare stream drops the report's *hidden* columns, so the absolute time + window and the due/current marker dates never reach the client. This wraps the + report's own JSON unchanged under ``data`` and prepends a ``window`` header + (start/end of the horizon + the demand due date + the current/plan date, all + ISO) so the client can draw a real dated axis and the due/now markers. + + The wrapped ``data`` is byte-identical to the legacy ``?format=json`` stream. + """ + + def dispatch(self, request, *args, **kwargs): + rc = self.report_class + # Run the report view FIRST - it carries the auth/permission gate. Only a + # streaming (authorized) response gets the window header; a denial passes + # through untouched, before we spend any query computing the window. + inner = self._run_report(request, *args, **kwargs) + if not getattr(inner, "streaming", False): + return inner # e.g. a permission denial - pass through unchanged + + # Window extraction must never break the data stream: on any failure we + # fall back to an empty window (the client then derives bounds from bars). + window = {} + try: + from freppledb.input.models import Demand + from freppledb.common.report import getCurrentDate + + # The report's own get() already ran getBuckets while building the + # response, so the horizon is on the request - reuse it. Only re-run + # (the heavy recursive pegging CTE) if it somehow isn't set yet. + start = getattr(request, "report_startdate", None) + end = getattr(request, "report_enddate", None) + if start is None or end is None: + rc.getBuckets(request, *args, **kwargs) + start = getattr(request, "report_startdate", None) + end = getattr(request, "report_enddate", None) + demand = ( + Demand.objects.using(request.database) + .filter(name=args[0]) + .only("due") + .first() + if args and args[0] + else None + ) + current = getCurrentDate(request.database, lastplan=True) + window = { + "start": start.isoformat() if start else None, + "end": end.isoformat() if end else None, + "due": demand.due.isoformat() if demand and demand.due else None, + "current": current.isoformat() if current else None, + } + except Exception: + window = {} + + header = ('{"window":%s,"data":') % json.dumps(window) + return self._wrap(inner, header) diff --git a/freppledb/common/api/views.py b/freppledb/common/api/views.py index 2dd933b646..78c9a3ef65 100644 --- a/freppledb/common/api/views.py +++ b/freppledb/common/api/views.py @@ -21,10 +21,14 @@ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. # +import time + from django.contrib.admin.views.decorators import staff_member_required +from django.http import JsonResponse from django.shortcuts import render from django.utils.translation import gettext_lazy as _ from django.views.decorators.csrf import csrf_protect +from django.views.decorators.http import require_GET from rest_framework import generics from rest_framework_bulk import ListBulkCreateUpdateDestroyAPIView @@ -35,6 +39,30 @@ from freppledb.common.auth import getWebserviceAuthorization +@require_GET +def APITokenView(request): + """ + Mint a short-lived JWT for the logged-in (session) user so the same-origin + SPA can authenticate REST + websocket calls. The session cookie authorizes + this request; the token carries the username and is signed with the + scenario's web-token secret (so it is valid for request.database). + """ + if not request.user.is_authenticated: + return JsonResponse({"detail": "authentication required"}, status=401) + ttl = 86400 # 1 day + # xframe_options_exempt=False so replaying this token through MultiDBMiddleware + # does NOT clear the session's X-Frame-Options (the middleware defaults that + # flag to True for legacy embedded webtokens). The same-origin SPA never + # embeds frepple in a frame, so it must not weaken clickjacking protection. + token = getWebserviceAuthorization( + user=request.user.username, + exp=ttl, + database=request.database, + xframe_options_exempt=False, + ) + return JsonResponse({"token": token, "exp": round(time.time()) + ttl}) + + @staff_member_required @csrf_protect def APIIndexView(request): @@ -95,6 +123,10 @@ class frePPleListCreateAPIView(ListBulkCreateUpdateDestroyAPIView): permission_classes = (frepplePermissionClass,) def get_queryset(self): + if getattr(self, "swagger_fake_view", False): + # OpenAPI schema generation has no scenario request; use the base + # queryset so the endpoint is still introspected. + return super().get_queryset() queryset = super().get_queryset().using(self.request.database) return queryset @@ -122,6 +154,9 @@ class frePPleRetrieveUpdateDestroyAPIView(generics.RetrieveUpdateDestroyAPIView) permission_classes = (frepplePermissionClass,) def get_queryset(self): + if getattr(self, "swagger_fake_view", False): + # OpenAPI schema generation has no scenario request. + return super().get_queryset() if self.request.database == "default": return super().get_queryset() else: diff --git a/freppledb/common/jwtauth.py b/freppledb/common/jwtauth.py new file mode 100644 index 0000000000..ef154af489 --- /dev/null +++ b/freppledb/common/jwtauth.py @@ -0,0 +1,135 @@ +# +# Copyright (C) 2026 by frePPLe bv +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +""" +Shared JWT + scenario helpers (Phase 0 modernization). + +The HTTP middleware (common/middleware.py), the ASGI middleware (asgi.py) and +the token-minting helper (common/auth.py) each duplicated the same secret +resolution and JWT decode logic. This module centralises the *decode* side +(secret order + verification) and lets the ASGI/websocket layer pick the +scenario database from the URL/header (instead of the FREPPLE_DATABASE env var). + +Scope note: the legacy HTTP MultiDBMiddleware still hand-rolls its own decode +(it needs the expired-token -> login-redirect behaviour) and minting still lives +in common/auth.getWebserviceAuthorization. Migrating those onto +resolve_jwt_secrets/decode_jwt/encode_jwt is a follow-up; until then this is the +single source of truth for the ASGI path only. +""" + +import re + +import jwt +from django.conf import settings +from django.db import DEFAULT_DB_ALIAS + +from freppledb.common.utils import get_databases + + +def resolve_jwt_secrets(database): + """Ordered list of HS256 secrets to try when decoding a token for a scenario. + + Matches the historical order: a global AUTH_SECRET_KEY (if configured), then + the scenario's SECRET_WEBTOKEN_KEY (falling back to the Django SECRET_KEY). + """ + secrets = [] + auth_secret = getattr(settings, "AUTH_SECRET_KEY", None) + if auth_secret: + secrets.append(auth_secret) + secrets.append( + get_databases()[database].get("SECRET_WEBTOKEN_KEY", settings.SECRET_KEY) + ) + return secrets + + +def encode_jwt(database, secret=None, **payload): + """Mint an HS256 token for a scenario, signed with its web-token secret.""" + if not secret: + secret = get_databases()[database].get( + "SECRET_WEBTOKEN_KEY", settings.SECRET_KEY + ) + token = jwt.encode(payload, secret, algorithm="HS256") + return token.decode("ascii") if not isinstance(token, str) else token + + +def decode_jwt(token, database): + """Return the decoded JWT payload, or None if no secret validates it. + + Re-raises jwt.ExpiredSignatureError (a valid-but-expired token) so callers + can redirect to the login page, matching the previous middleware behaviour. + """ + expired = None + for secret in resolve_jwt_secrets(database): + if not secret: + continue + try: + return jwt.decode(token, secret, algorithms=["HS256"]) + except jwt.exceptions.ExpiredSignatureError as e: + expired = e + except jwt.exceptions.InvalidTokenError: + pass + if expired: + raise expired + return None + + +def _scenario_regexp(database): + """The compiled ^// prefix matcher (reused from the HTTP middleware + when available, compiled on demand otherwise - e.g. in ASGI processes).""" + regexp = get_databases()[database].get("regexp") + if regexp is None: + regexp = re.compile("^/%s/" % database) + return regexp + + +def extract_scenario(path, headers=None, default=DEFAULT_DB_ALIAS): + """Resolve the scenario database for a request and strip its URL prefix. + + Order: an ``X-Frepple-Scenario`` header (handy for websocket clients that + cannot set a path prefix), then a ``//...`` URL prefix, then the + given ``default`` (typically the FREPPLE_DATABASE env var, so existing + single-scenario deployments keep working). + + Returns ``(database, stripped_path)``; the prefix is removed from the path so + downstream routing stays scenario-agnostic, mirroring the WSGI middleware. + """ + dbs = get_databases() + + # 1. Header (bytes tuples in ASGI scope, or a plain mapping). + if headers: + items = headers.items() if hasattr(headers, "items") else headers + for k, v in items: + name = k.decode("ascii") if isinstance(k, bytes) else k + if name.lower() == "x-frepple-scenario": + val = v.decode("ascii") if isinstance(v, bytes) else v + if val in dbs: + return val, path + break + + # 2. URL path prefix //... + if path: + for db in dbs: + if _scenario_regexp(db).match(path): + return db, path[len("/%s" % db) :] + + return default, path diff --git a/freppledb/common/middleware.py b/freppledb/common/middleware.py index 580e485090..2c023bec2d 100644 --- a/freppledb/common/middleware.py +++ b/freppledb/common/middleware.py @@ -282,6 +282,13 @@ def __call__(self, request): # Not a valid API token logger.error(f"Invalid API key: {e}") return HttpResponseForbidden(f"Invalid API key: {e}") + # Block deactivated accounts. The token branch authenticates + # directly (bypassing the auth backend), so is_active is not + # checked anywhere else here - a still-valid token for a + # deactivated user would otherwise keep working. + if not user.is_active: + logger.error("Inactive user in webtoken") + return HttpResponseForbidden("Account is inactive") user.backend = settings.AUTHENTICATION_BACKENDS[0] login(request, user) request.user = user diff --git a/freppledb/common/tests/test_api_phase0.py b/freppledb/common/tests/test_api_phase0.py new file mode 100644 index 0000000000..694aaaa943 --- /dev/null +++ b/freppledb/common/tests/test_api_phase0.py @@ -0,0 +1,370 @@ +# +# Copyright (C) 2026 by frePPLe bv +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +"""Phase 0 modernization API tests: OpenAPI schema + JSON output endpoints.""" + +import json + +from django.test import TestCase, TransactionTestCase + + +def _body(response): + """Return the full response body, handling streaming responses.""" + if getattr(response, "streaming", False): + return b"".join(response.streaming_content) + return response.content + + +class Phase0SchemaTest(TestCase): + fixtures = ["demo"] + + def setUp(self): + self.client.login(username="admin", password="admin") + super().setUp() + + def test_openapi_schema(self): + # The drf-spectacular schema must generate and be served. + response = self.client.get("/api/schema/") + self.assertEqual(response.status_code, 200) + body = _body(response) + self.assertIn(b"openapi", body) + self.assertIn(b"frePPLe API", body) + + def test_swagger_ui(self): + self.assertEqual(self.client.get("/api/doc/").status_code, 200) + + def test_redoc(self): + self.assertEqual(self.client.get("/api/redoc/").status_code, 200) + + +class Phase0OutputEndpointTest(TestCase): + fixtures = ["demo"] + + # The pivot OUTPUT endpoints (inventory/demand/resource, Phase 3) use the + # enriched PivotJSONStreamView: a self-describing measures+buckets header over + # the report's UNCHANGED pivot under "data". The wrapped "data" must stay + # byte-identical to the legacy report's ?format=json stream (same raw-SQL + # path, no DRF serializer) - data-parity. (legacy, api) + ENRICHED = [ + ("/buffer/?format=json", "/api/output/inventory/"), + ("/demand/?format=json", "/api/output/demand/"), + ("/resource/?format=json", "/api/output/resource/"), + ] + + def setUp(self): + self.client.login(username="admin", password="admin") + super().setUp() + + def test_pivot_outputs_enriched(self): + # We don't json.loads the body: with no computed plan the rows can be + # empty and the report's empty-grid output is not strictly valid JSON + # (pre-existing behaviour, identical on the legacy path). + for legacy, new in self.ENRICHED: + with self.subTest(endpoint=new): + response = self.client.get(new) + self.assertEqual(response.status_code, 200, new) + api = _body(response) + self.assertTrue(api.startswith(b'{"measures":'), api[:48]) + self.assertIn(b'"buckets":', api) + self.assertIn(b'"data":', api) + # The wrapped data matches the legacy envelope up to the + # horizon-dependent "records" field. + old = _body(self.client.get(legacy)) + inner = api.split(b'"data":', 1)[1] + self.assertEqual( + inner.split(b'"records":')[0], + old.split(b'"records":')[0], + "%s data differs from legacy %s" % (new, legacy), + ) + + def test_forecast_output_enriched(self): + # The forecast endpoint (Phase 1B) wraps the report's pivot object under + # "data" and prepends the measure order + bucket dates the editor needs. + body = _body(self.client.get("/api/output/forecast/")) + self.assertTrue(body.startswith(b'{"measures":'), body[:48]) + self.assertIn(b'"buckets":', body) + self.assertIn(b'"data":', body) + + def test_flat_output_endpoints(self): + # The flat violation-list reports (Phase 3 problem/constraint) are exposed + # via the bare JSONStreamView - the report's own {total,page,records,rows} + # stream, no enrichment (they aren't pivots). + for endpoint in ("/api/output/problem/", "/api/output/constraint/"): + with self.subTest(endpoint=endpoint): + response = self.client.get(endpoint) + self.assertEqual(response.status_code, 200, endpoint) + body = _body(response) + self.assertIn(b'"rows":', body, endpoint) + + def test_pegging_output_enriched(self): + # The pegging Gantt endpoint (Phase 3-D) wraps the demand-pegging report's + # tree object under "data" and prepends an absolute time "window" (the + # report's hidden horizon + due/current markers) the Gantt axis needs. + # The wrapped data stays byte-identical to the legacy report envelope. + new = "/api/output/pegging/Demand%2001/" + legacy = "/demandpegging/Demand%2001/?format=json" + response = self.client.get(new) + self.assertEqual(response.status_code, 200, new) + api = _body(response) + self.assertTrue(api.startswith(b'{"window":'), api[:48]) + self.assertIn(b'"data":', api) + # The window header carries the horizon bounds. Reconstruct just the + # header object: everything before ,"data": plus a closing brace. + header = json.loads(api.split(b',"data":', 1)[0] + b"}") + self.assertIn("window", header) + self.assertIn("start", header["window"]) + self.assertIn("end", header["window"]) + # Data-parity: the wrapped "data" matches the legacy envelope up to the + # horizon-dependent "records" field (same raw-SQL path, no serializer). + old = _body(self.client.get(legacy)) + inner = api.split(b'"data":', 1)[1] + self.assertEqual( + inner.split(b'"records":')[0], + old.split(b'"records":')[0], + "%s data differs from legacy %s" % (new, legacy), + ) + + +class ApiTokenTest(TestCase): + """/api/token/ mints a JWT for the session user (the SPA's auth source).""" + + fixtures = ["demo"] + + def test_token_requires_auth(self): + self.assertEqual(self.client.get("/api/token/").status_code, 401) + + def test_token_mints_jwt_for_session_user(self): + from freppledb.common.jwtauth import decode_jwt + + self.client.login(username="admin", password="admin") + response = self.client.get("/api/token/") + self.assertEqual(response.status_code, 200) + data = response.json() + self.assertIn("exp", data) + self.assertEqual(decode_jwt(data["token"], "default").get("user"), "admin") + + def test_token_does_not_exempt_xframe(self): + # Replaying the SPA token through MultiDBMiddleware must NOT clear the + # session's X-Frame-Options (the middleware defaults that flag to True for + # legacy webtokens), so the minted token pins it to False. + from freppledb.common.jwtauth import decode_jwt + + self.client.login(username="admin", password="admin") + token = self.client.get("/api/token/").json()["token"] + self.assertIs(decode_jwt(token, "default").get("xframe_options_exempt"), False) + + +class Phase0InactiveUserTest(TestCase): + """A still-valid webtoken for a deactivated account must be rejected by the + HTTP MultiDBMiddleware bearer path (security regression guard).""" + + fixtures = ["demo"] + + def test_webtoken_rejects_inactive_user(self): + from freppledb.common.models import User + from freppledb.common.auth import getWebserviceAuthorization + + User.objects.create_user(username="ghost", password="x", is_active=False) + token = getWebserviceAuthorization(user="ghost", database="default") + response = self.client.get("/", HTTP_AUTHORIZATION="Bearer %s" % token) + self.assertEqual(response.status_code, 403) + self.assertIn(b"inactive", response.content.lower()) + + +class Phase0JwtUtilTest(TestCase): + """The shared JWT/scenario helpers (common/jwtauth.py) used by REST + WS.""" + + def test_encode_decode_roundtrip(self): + from freppledb.common.jwtauth import encode_jwt, decode_jwt + + token = encode_jwt("default", user="admin") + self.assertEqual(decode_jwt(token, "default").get("user"), "admin") + + def test_decode_invalid_returns_none(self): + from freppledb.common.jwtauth import decode_jwt + + self.assertIsNone(decode_jwt("not.a.valid.token", "default")) + + def test_decode_expired_raises(self): + import jwt as pyjwt + from freppledb.common.jwtauth import encode_jwt, decode_jwt + + # exp is an absolute timestamp; 1 => 1970 => already expired. + token = encode_jwt("default", user="admin", exp=1) + with self.assertRaises(pyjwt.exceptions.ExpiredSignatureError): + decode_jwt(token, "default") + + def test_extract_scenario_default(self): + from freppledb.common.jwtauth import extract_scenario + + db, path = extract_scenario("/some/path/") + self.assertEqual(db, "default") + self.assertEqual(path, "/some/path/") + + def test_extract_scenario_url_prefix(self): + from freppledb.common.jwtauth import extract_scenario + from freppledb.common.utils import get_databases + + others = [d for d in get_databases() if d != "default"] + if not others: + self.skipTest("no non-default scenario configured") + db, path = extract_scenario("/%s/foo/bar/" % others[0]) + self.assertEqual(db, others[0]) + self.assertEqual(path, "/foo/bar/") + + def test_extract_scenario_header(self): + from freppledb.common.jwtauth import extract_scenario + from freppledb.common.utils import get_databases + + others = [d for d in get_databases() if d != "default"] + if not others: + self.skipTest("no non-default scenario configured") + db, path = extract_scenario( + "/foo/", headers=[(b"x-frepple-scenario", others[0].encode("ascii"))] + ) + self.assertEqual(db, others[0]) + + +class Phase0WebsocketTest(TransactionTestCase): + """The authenticated websocket endpoint (asgi.py). + + Uses TransactionTestCase: the consumer's user lookup runs through + database_sync_to_async on a separate thread/connection, so the fixture user + must be committed (a wrapping TestCase transaction would hide it). + """ + + fixtures = ["demo"] + + def _connect(self, path="/ws/", headers=None, subprotocols=None): + from asgiref.sync import async_to_sync + from channels.testing import WebsocketCommunicator + from freppledb.asgi import application + + async def run(): + communicator = WebsocketCommunicator( + application, path, headers=headers, subprotocols=subprotocols + ) + # connect() returns (False, close_code) on reject, else (True, subprotocol). + connected, detail = await communicator.connect() + reply = None + if connected: + await communicator.send_to(text_data=json.dumps({"message": "hi"})) + reply = json.loads(await communicator.receive_from()) + await communicator.disconnect() + return connected, detail, reply + + return async_to_sync(run)() + + def _token(self): + from freppledb.common.jwtauth import encode_jwt + + return encode_jwt("default", user="admin") + + def test_ws_rejects_without_token(self): + connected, detail, _ = self._connect() + self.assertFalse(connected) + self.assertEqual(detail, 4401) + + def test_ws_rejects_bad_token(self): + connected, detail, _ = self._connect( + headers=[(b"authorization", b"Bearer not.a.valid.token")] + ) + self.assertFalse(connected) + self.assertEqual(detail, 4401) + + def test_ws_authorization_header_echoes_scenario(self): + token = self._token() + connected, _, reply = self._connect( + headers=[(b"authorization", b"Bearer " + token.encode("ascii"))] + ) + self.assertTrue(connected) + self.assertEqual(reply["message"], "hi") + self.assertEqual(reply["database"], "default") + self.assertEqual(reply["user"], "admin") + + def _probe(self, path="/ws/", subprotocols=None): + # Self-contained probe: a bare consumer (NO middleware, NO DB) that runs + # _extract_credentials + decode_jwt itself, so we can see exactly which + # step fails for each carrier independent of get_user / the auth stack. + from asgiref.sync import async_to_sync + from channels.generic.websocket import AsyncWebsocketConsumer + from channels.testing import WebsocketCommunicator + + expected = self._token() + + class Probe(AsyncWebsocketConsumer): + async def connect(self): + await self.accept() + from freppledb.asgi import _extract_credentials + from freppledb.common.jwtauth import decode_jwt + + headers = self.scope.get("headers") or [] + scheme, cred, sub = _extract_credentials(self.scope, headers) + decoded, err = None, None + try: + decoded = decode_jwt(cred, "default") if cred else None + except Exception as e: # noqa: BLE001 + err = repr(e) + await self.send( + text_data=json.dumps( + { + "scheme": scheme, + "cred_ok": cred == expected, + "cred_len": len(cred) if cred else 0, + "exp_len": len(expected), + "decoded": decoded, + "err": err, + "nsubs": len(self.scope.get("subprotocols") or []), + } + ) + ) + + async def run(): + c = WebsocketCommunicator(Probe.as_asgi(), path, subprotocols=subprotocols) + ok, _ = await c.connect() + msg = json.loads(await c.receive_from()) if ok else {"closed": True} + await c.disconnect() + return msg + + return async_to_sync(run)() + + # End-to-end WS auth (decode -> get_user -> accept) is proven by + # test_ws_authorization_header_echoes_scenario. The two carrier tests below + # verify the browser-only carriers (query string + subprotocol) that a header + # cannot deliver: that TokenMiddleware extracts the JWT from them and it + # decodes to the right user. Driving the full consumer for these carriers is a + # Phase 1A follow-up once the Execute-screen WS client exercises them for real. + + def test_ws_query_string_carrier_decodes(self): + token = self._token() + info = self._probe(path="/ws/?token=" + token) + self.assertEqual( + (info.get("decoded") or {}).get("user"), "admin", "qs: %r" % info + ) + + def test_ws_subprotocol_carrier_decodes(self): + token = self._token() + info = self._probe(subprotocols=["bearer", token]) + self.assertEqual( + (info.get("decoded") or {}).get("user"), "admin", "sp: %r" % info + ) diff --git a/freppledb/common/tests/test_api_phase1a.py b/freppledb/common/tests/test_api_phase1a.py new file mode 100644 index 0000000000..edf22d83f1 --- /dev/null +++ b/freppledb/common/tests/test_api_phase1a.py @@ -0,0 +1,265 @@ +# +# Copyright (C) 2026 by frePPLe bv +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + +"""Phase 1A modernization tests: live task-progress websocket (ws/tasks/).""" + +import asyncio +import json +import os + +from django.test import TransactionTestCase + + +class Phase1ATaskProgressTest(TransactionTestCase): + """The ws/tasks/ consumer relays channel-layer task updates to the browser, + and a Task save broadcasts onto that group. Uses TransactionTestCase so the + threaded DB write in the broadcast test sees committed fixture data.""" + + fixtures = ["demo"] + + def test_ws_tasks_relays_group_message(self): + # This verifies the consumer's group->client RELAY (auth is covered by + # test_ws_tasks_rejects_unauthenticated + the Phase 0 jwt-auth tests). + # We authenticate by injecting an active user into the consumer scope + # rather than through the middleware's threaded get_user, which is + # unreliable in later TransactionTestCase methods. The admin user is read + # on the main thread (which always sees the loaded fixtures). + from types import SimpleNamespace + + from asgiref.sync import async_to_sync + from channels.layers import get_channel_layer + from channels.testing import WebsocketCommunicator + from freppledb.asgi import TaskProgressConsumer + + # The consumer only inspects user.is_active, so a zero-DB stand-in keeps + # this focused on the relay and free of fixture/connection flakiness. + active_user = SimpleNamespace( + is_active=True, is_authenticated=True, username="admin" + ) + consumer_app = TaskProgressConsumer.as_asgi() + + async def auth_app(scope, receive, send): + scope = dict(scope) + scope["user"] = active_user + scope["database"] = "default" + return await consumer_app(scope, receive, send) + + async def run(): + c = WebsocketCommunicator(auth_app, "/ws/tasks/") + connected, detail = await c.connect() + if not connected: + return {"connected": False, "detail": detail} + # A worker progress event, injected via the channel layer. + await get_channel_layer().group_send( + "tasks.default", + { + "type": "task.update", + "task": {"id": 7, "name": "runplan", "status": "42%"}, + }, + ) + msg = json.loads(await c.receive_from(timeout=5)) + await c.disconnect() + return msg + + msg = async_to_sync(run)() + self.assertEqual(msg.get("status"), "42%", msg) + self.assertEqual(msg.get("id"), 7, msg) + + def test_ws_tasks_rejects_unauthenticated(self): + from asgiref.sync import async_to_sync + from channels.testing import WebsocketCommunicator + from freppledb.asgi import application + + async def run(): + c = WebsocketCommunicator(application, "/ws/tasks/") + connected, detail = await c.connect() + await c.disconnect() + return connected, detail + + connected, detail = async_to_sync(run)() + self.assertFalse(connected) + self.assertEqual(detail, 4401) + + def test_ws_tasks_rejects_inactive_user(self): + # An authenticated-but-deactivated user must be refused (the consumer + # gates on user.is_active). Inject the user into scope to focus on the + # is_active gate, like the relay test above. + from types import SimpleNamespace + + from asgiref.sync import async_to_sync + from channels.testing import WebsocketCommunicator + from freppledb.asgi import TaskProgressConsumer + + inactive_user = SimpleNamespace( + is_active=False, is_authenticated=True, username="ghost" + ) + consumer_app = TaskProgressConsumer.as_asgi() + + async def auth_app(scope, receive, send): + scope = dict(scope) + scope["user"] = inactive_user + scope["database"] = "default" + return await consumer_app(scope, receive, send) + + async def run(): + c = WebsocketCommunicator(auth_app, "/ws/tasks/") + connected, detail = await c.connect() + await c.disconnect() + return connected, detail + + connected, detail = async_to_sync(run)() + self.assertFalse(connected) + self.assertEqual(detail, 4401) + + def test_task_save_broadcasts_progress(self): + # The post_save -> group_send path runs through database_sync_to_async on + # a separate thread, which only reaches a subscriber over a cross-process + # layer, so this only runs when Redis is configured (the deployment layer). + if not os.environ.get("REDIS_HOST"): + self.skipTest("needs a cross-process channel layer (Redis)") + from asgiref.sync import async_to_sync + from channels.db import database_sync_to_async + from channels.layers import get_channel_layer + from django.utils import timezone + from freppledb.execute.models import Task + + def _make(): + Task.objects.using("default").create( + name="runplan", submitted=timezone.now(), status="33%" + ) + + async def run(): + layer = get_channel_layer() + chan = await layer.new_channel() + await layer.group_add("tasks.default", chan) + await database_sync_to_async(_make)() + msg = await asyncio.wait_for(layer.receive(chan), timeout=5) + await layer.group_discard("tasks.default", chan) + return msg + + msg = async_to_sync(run)() + self.assertEqual(msg["type"], "task.update") + self.assertEqual(msg["task"]["status"], "33%") + + +class Phase1ALogTailTest(TransactionTestCase): + """The ws/tasks//log/ live log-tail consumer (asgi.py).""" + + def test_stream_logfile_tails_and_finishes(self): + # Pure tailing logic: no DB, no channels - deterministic. + import tempfile + + from asgiref.sync import async_to_sync + from freppledb.asgi import stream_logfile + + f = tempfile.NamedTemporaryFile(suffix=".log", delete=False, mode="w") + f.write("first line\n") + f.flush() + f.close() + + async def run(): + out = [] + + async def send_json(m): + out.append(m) + + async def is_finished(): + return True + + await asyncio.wait_for( + stream_logfile(f.name, is_finished, send_json, poll=0.01), timeout=3 + ) + return out + + try: + out = async_to_sync(run)() + finally: + os.unlink(f.name) + self.assertEqual(out[0], {"log": "first line\n"}, out) + self.assertIn({"done": True}, out) + + def test_ws_log_streams_injected_path(self): + import tempfile + from types import SimpleNamespace + + from asgiref.sync import async_to_sync + from channels.testing import WebsocketCommunicator + from freppledb.asgi import TaskLogConsumer + + f = tempfile.NamedTemporaryFile(suffix=".log", delete=False, mode="w") + f.write("hello from worker\n") + f.flush() + f.close() + + active_user = SimpleNamespace( + is_active=True, is_authenticated=True, username="admin" + ) + consumer_app = TaskLogConsumer.as_asgi() + + async def auth_app(scope, receive, send): + scope = dict(scope) + scope["user"] = active_user + scope["database"] = "default" + scope["url_route"] = {"kwargs": {"taskid": "1"}} + scope["logpath_override"] = f.name + scope["finished_override"] = True + scope["logpoll_override"] = 0.01 + return await consumer_app(scope, receive, send) + + async def run(): + c = WebsocketCommunicator(auth_app, "/ws/tasks/1/log/") + connected, detail = await c.connect() + msgs = [] + if connected: + for _ in range(5): + try: + msgs.append(json.loads(await c.receive_from(timeout=3))) + except Exception: + break + if msgs[-1].get("done"): + break + await c.disconnect() + return connected, detail, msgs + + try: + connected, detail, msgs = async_to_sync(run)() + finally: + os.unlink(f.name) + self.assertTrue(connected, detail) + self.assertEqual(msgs[0].get("log"), "hello from worker\n", msgs) + self.assertTrue(any(m.get("done") for m in msgs), msgs) + + def test_ws_log_rejects_unauthenticated(self): + from asgiref.sync import async_to_sync + from channels.testing import WebsocketCommunicator + from freppledb.asgi import application + + async def run(): + c = WebsocketCommunicator(application, "/ws/tasks/1/log/") + connected, detail = await c.connect() + await c.disconnect() + return connected, detail + + connected, detail = async_to_sync(run)() + self.assertFalse(connected) + self.assertEqual(detail, 4401) diff --git a/freppledb/execute/models.py b/freppledb/execute/models.py index 982bc89783..f0c1aaaaa9 100644 --- a/freppledb/execute/models.py +++ b/freppledb/execute/models.py @@ -372,3 +372,49 @@ def save(self, *args, **kwargs): if os.sep in self.name: raise Exception("Export names can't contain %s" % os.sep) super().save(*args, **kwargs) + + +from django.db.models.signals import post_save # noqa: E402 +from django.dispatch import receiver # noqa: E402 + + +@receiver(post_save, sender=Task) +def broadcast_task_progress(sender, instance, **kwargs): + """ + Publish a task's status/progress to the scenario's websocket group so the + Execute screen updates live (Phase 1A) instead of polling every 5s. + + The worker process saves progress into Task.status ("0%"->...->"Done"); this + relays each change over the channel layer (Redis in a load-balanced deploy) + to web pods, which fan it out to connected browsers. A broadcast failure + (e.g. Redis unavailable) must never break task bookkeeping, so it is + swallowed. + """ + from asgiref.sync import async_to_sync + from channels.layers import get_channel_layer + + layer = get_channel_layer() + if layer is None: + return + database = instance._state.db or DEFAULT_DB_ALIAS + try: + async_to_sync(layer.group_send)( + "tasks.%s" % database, + { + "type": "task.update", + "task": { + "id": instance.id, + "name": instance.name, + "status": instance.status, + "message": instance.message, + "started": ( + instance.started.isoformat() if instance.started else None + ), + "finished": ( + instance.finished.isoformat() if instance.finished else None + ), + }, + }, + ) + except Exception: + logger.warning("Could not broadcast task progress", exc_info=True) diff --git a/freppledb/forecast/urls.py b/freppledb/forecast/urls.py index d709f8283d..21a0ba60b7 100644 --- a/freppledb/forecast/urls.py +++ b/freppledb/forecast/urls.py @@ -40,8 +40,17 @@ else: from . import views from . import serializers + from freppledb.common.api.output import ForecastJSONStreamView urlpatterns = [ + # JSON output API: the forecast endpoint is enriched (Phase 1B) with the + # measure order + bucket dates the editor needs; the report data is still + # the raw-SQL ?format=json path, wrapped under "data". + re_path( + r"^api/output/forecast/$", + ForecastJSONStreamView.as_view(report_class=views.OverviewReport), + name="api_output_forecast", + ), # Forecast editor screen path("forecast/editor/", views.ForecastEditor.planning), path("forecast/editor//", views.ForecastEditor.planning), diff --git a/freppledb/output/urls.py b/freppledb/output/urls.py index 3b75632960..ca00177492 100644 --- a/freppledb/output/urls.py +++ b/freppledb/output/urls.py @@ -24,6 +24,11 @@ from django.urls import re_path from freppledb import mode +from freppledb.common.api.output import ( + JSONStreamView, + PivotJSONStreamView, + PeggingJSONView, +) # Automatically add these URLs when the application is installed autodiscover = True @@ -38,6 +43,57 @@ import freppledb.output.views.pegging urlpatterns = [ + # JSON output API (Phase 0 modernization): thin wrappers that reuse each + # report's raw-SQL ?format=json streaming path. See common/api/output.py. + re_path( + r"^api/output/inventory/$", + # Enriched (self-describing measures+buckets) for the SPA inventory + # screen; data payload stays byte-identical under "data". + PivotJSONStreamView.as_view( + report_class=freppledb.output.views.buffer.OverviewReport + ), + name="api_output_inventory", + ), + re_path( + r"^api/output/demand/$", + # Enriched (self-describing measures+buckets) for the SPA demand + # screen; data payload stays byte-identical under "data". + PivotJSONStreamView.as_view( + report_class=freppledb.output.views.demand.OverviewReport + ), + name="api_output_demand", + ), + re_path( + r"^api/output/resource/$", + # Enriched for the SPA resource screen (utilization/load over buckets). + PivotJSONStreamView.as_view( + report_class=freppledb.output.views.resource.OverviewReport + ), + name="api_output_resource", + ), + re_path( + r"^api/output/pegging/(.+)/$", + # Enriched (absolute time window + due/current markers) for the SPA + # pegging Gantt; the tree/bar data stays byte-identical under "data". + PeggingJSONView.as_view( + report_class=freppledb.output.views.pegging.ReportByDemand + ), + name="api_output_pegging", + ), + re_path( + r"^api/output/problem/$", + # Flat violation list (Phase 3): the report's raw-SQL ?format=json + # stream, no enrichment needed (not a pivot). + JSONStreamView.as_view(report_class=freppledb.output.views.problem.Report), + name="api_output_problem", + ), + re_path( + r"^api/output/constraint/$", + JSONStreamView.as_view( + report_class=freppledb.output.views.constraint.BaseReport + ), + name="api_output_constraint", + ), re_path( r"^buffer/item/(.+)/$", freppledb.output.views.buffer.OverviewReport.as_view(), diff --git a/freppledb/settings.py b/freppledb/settings.py index 7e468c61cb..14552a5f61 100644 --- a/freppledb/settings.py +++ b/freppledb/settings.py @@ -28,6 +28,7 @@ Instead put all your settings in the file FREPPLE_CONFDIR/djangosettings.py. """ + import locale import os import sys @@ -39,7 +40,6 @@ import django.contrib.admindocs from django.utils.translation import gettext_lazy as _ - # FREPPLE_APP directory if "FREPPLE_APP" in os.environ: FREPPLE_APP = os.environ["FREPPLE_APP"] @@ -104,7 +104,9 @@ # will match example.com, www.example.com, and any other subdomain of example.com. # A value of '*' will match anything, effectively disabling this feature. # This option is only active when DEBUG = false. -ALLOWED_HOSTS = ["*"] +# Space-separated FREPPLE_ALLOWED_HOSTS narrows this for a deployment (the Helm +# chart sets it to the public host); defaults to '*' for dev/test. +ALLOWED_HOSTS = os.environ.get("FREPPLE_ALLOWED_HOSTS", "*").split() or ["*"] # Local time zone for this installation. Choices can be found here: # http://en.wikipedia.org/wiki/List_of_tz_zones_by_name @@ -169,6 +171,29 @@ ASGI_APPLICATION = "freppledb.asgi.application" WSGI_APPLICATION = "freppledb.wsgi.application" + +# Channel layer for websockets (Phase 1A live task progress / log tail). +# Redis is used when REDIS_HOST is set - the load-balanced deployment target, +# where the worker process publishes progress that web pods relay to browsers. +# Without REDIS_HOST an in-memory layer keeps single-process dev and the test +# suite working without extra infrastructure (it does NOT cross processes, so +# multi-process deployments must configure Redis). +if os.environ.get("REDIS_HOST"): + CHANNEL_LAYERS = { + "default": { + "BACKEND": "channels_redis.core.RedisChannelLayer", + "CONFIG": { + "hosts": [ + ( + os.environ["REDIS_HOST"], + int(os.environ.get("REDIS_PORT", "6379")), + ) + ] + }, + } + } +else: + CHANNEL_LAYERS = {"default": {"BACKEND": "channels.layers.InMemoryChannelLayer"}} ROOT_URLCONF = "freppledb.urls" if "FREPPLE_STATIC" in os.environ: STATIC_ROOT = os.environ["FREPPLE_STATIC"] @@ -443,6 +468,17 @@ "rest_framework.renderers.JSONRenderer", "freppledb.common.api.renderers.freppleBrowsableAPI", ), + # OpenAPI schema generation (drf-spectacular). Inert at request time - + # only used when generating the schema, so the existing API is unaffected. + "DEFAULT_SCHEMA_CLASS": "drf_spectacular.openapi.AutoSchema", +} + +# OpenAPI / Swagger settings for the modernization REST API (Phase 0). +SPECTACULAR_SETTINGS = { + "TITLE": "frePPLe API", + "DESCRIPTION": "REST API for the frePPLe planning application.", + "VERSION": "1.0.0", + "SERVE_INCLUDE_SCHEMA": False, } # Bootstrap diff --git a/freppledb/urls.py b/freppledb/urls.py index c3feb71362..bc700f121c 100644 --- a/freppledb/urls.py +++ b/freppledb/urls.py @@ -27,8 +27,14 @@ from django.conf import settings from django.views.generic.base import RedirectView from django.views.i18n import JavaScriptCatalog +from drf_spectacular.views import ( + SpectacularAPIView, + SpectacularRedocView, + SpectacularSwaggerView, +) from freppledb.admin import data_site +from freppledb.common.api.views import APITokenView urlpatterns = [ # Redirect admin index page /data/ to / @@ -71,4 +77,18 @@ ), re_path(r"^data/", data_site.urls), re_path(r"^api-auth/", include("rest_framework.urls", namespace="rest_framework")), + # Short-lived JWT for the same-origin SPA (session -> token). + re_path(r"^api/token/$", APITokenView, name="api_token"), + # OpenAPI schema + interactive docs (Phase 0 modernization API). + re_path(r"^api/schema/$", SpectacularAPIView.as_view(), name="schema"), + re_path( + r"^api/doc/$", + SpectacularSwaggerView.as_view(url_name="schema"), + name="swagger-ui", + ), + re_path( + r"^api/redoc/$", + SpectacularRedocView.as_view(url_name="schema"), + name="redoc", + ), ] diff --git a/frontend/.gitignore b/frontend/.gitignore new file mode 100644 index 0000000000..8358fb8b01 --- /dev/null +++ b/frontend/.gitignore @@ -0,0 +1,7 @@ +/node_modules +/.next +/out +/build +*.tsbuildinfo +next-env.d.ts +.env*.local diff --git a/frontend/app/demand/page.tsx b/frontend/app/demand/page.tsx new file mode 100644 index 0000000000..4659e76df3 --- /dev/null +++ b/frontend/app/demand/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +import PivotScreen from "@/components/PivotScreen"; +import { DEMAND } from "@/lib/demand"; + +// Phase 3 — Demand report. Read-only pivot of items x time buckets +// (/api/output/demand/, enriched PivotJSONStreamView). +export default function DemandPage() { + return ; +} diff --git a/frontend/app/execute/page.tsx b/frontend/app/execute/page.tsx new file mode 100644 index 0000000000..c9f83a8402 --- /dev/null +++ b/frontend/app/execute/page.tsx @@ -0,0 +1,224 @@ +"use client"; + +import { useState } from "react"; +import { authedFetch } from "@/lib/api"; +import { isAuthError } from "@/lib/errors"; +import { loginUrl } from "@/lib/session"; +import { useTaskProgress, type TaskUpdate } from "@/lib/useTaskProgress"; +import { useTaskLog } from "@/lib/useTaskLog"; +import { parseStatus, type TaskState } from "@/lib/ws"; +import { useToast } from "@/components/Toast"; + +// Execute / plan-run console (Phase 1A). Live task progress streams over +// ws/tasks/ and the log tail over ws/tasks/{id}/log/ — no polling. Launching a +// plan POSTs to the Django (WSGI) endpoint; failures (no session, CSRF, server +// error) now surface as toasts + an inline notice instead of doing nothing. +export default function ExecutePage() { + const { tasks, connected, authError } = useTaskProgress(); + const [selected, setSelected] = useState(null); + const [launching, setLaunching] = useState(false); + const toast = useToast(); + + const running = tasks.filter( + (t) => parseStatus(t.status).state === "running", + ).length; + + async function launchPlan() { + setLaunching(true); + try { + // authedFetch adds the Bearer JWT + CSRF header and throws AuthError on a + // 401/403. The launch view 302-redirects (fetch follows it) to /execute/ + // on success, or to /data/login/ when the session/CSRF is rejected. + const res = await authedFetch("/execute/launch/runplan/", { method: "POST" }); + let path = res.url; + try { + path = new URL(res.url).pathname; + } catch { + /* res.url may be relative in tests; fall back to the raw string */ + } + if (path.includes("/data/login")) { + toast("error", "Sign-in required", "Your session expired — sign in again."); + } else if (res.ok) { + toast("ok", "Plan launched", "Watch live progress below."); + } else { + toast("error", "Launch failed", `Server returned ${res.status}.`); + } + } catch (e) { + if (isAuthError(e)) { + toast("error", "Sign-in required", "Sign in to launch a plan."); + } else { + toast("error", "Launch failed", e instanceof Error ? e.message : String(e)); + } + } finally { + setLaunching(false); + } + } + + return ( +
+
+
+
Planning engine
+

Execute

+

+ Run the C++ planning engine and watch each task stream to completion + over the live websocket. +

+
+ +
+ + {authError && ( +
+ + + No active session — the live feed can't connect.{" "} + Sign in to continue. + +
+ )} + +
+
+
+ System status +
+
+
+
{tasks.length}
+
Tasks
+
+
+
+ {running} +
+
Running
+
+
+
+ {connected ? "LIVE" : "—"} +
+
Feed
+
+
+
+
+
+ Primary action +
+ + + Computes demand & supply for the default scenario. + +
+
+ +
+ Task feed +
+
+ {tasks.length === 0 && ( +
+ {connected ? "NO TASKS YET — RUN A PLAN TO BEGIN" : "AWAITING FEED…"} +
+ )} + {tasks.map((t, i) => ( + setSelected(selected === t.id ? null : t.id)} + /> + ))} +
+ + {selected != null && } +
+ ); +} + +const STATE_TAG: Record = { + waiting: { cls: "", label: "Queued" }, + running: { cls: "tag--run", label: "Running" }, + done: { cls: "tag--done", label: "Done" }, + failed: { cls: "tag--fail", label: "Failed" }, + canceled: { cls: "tag--fail", label: "Canceled" }, +}; + +function TaskRow({ + task, + index, + selected, + onSelect, +}: { + task: TaskUpdate; + index: number; + selected: boolean; + onSelect: () => void; +}) { + const { percent, state } = parseStatus(task.status); + const tag = STATE_TAG[state]; + const fillCls = + state === "failed" + ? "bar-fill bar-fill--fail" + : state === "running" + ? "bar-fill bar-fill--run" + : "bar-fill"; + return ( + + ); +} + +function LogPanel({ taskId }: { taskId: number }) { + const { log, done, connected } = useTaskLog(taskId); + return ( +
+
+ Log — task #{taskId} + + + {done ? "finished" : connected ? "streaming" : "connecting"} + +
+
{log || "(no output yet)"}
+
+ ); +} + +function ConnDot({ connected }: { connected: boolean }) { + return ( + + + {connected ? "live" : "offline"} + + ); +} diff --git a/frontend/app/forecast/ForecastChart.tsx b/frontend/app/forecast/ForecastChart.tsx new file mode 100644 index 0000000000..3b2757c447 --- /dev/null +++ b/frontend/app/forecast/ForecastChart.tsx @@ -0,0 +1,80 @@ +"use client"; + +import { + LineChart, + Line, + XAxis, + YAxis, + Tooltip, + Legend, + ResponsiveContainer, + CartesianGrid, +} from "recharts"; +import { toChartRows, type ForecastSeries } from "@/lib/forecast"; + +// Orders / baseline / net over time for one series (Phase 1B-3). +export function ForecastChart({ + series, + buckets, +}: { + series: ForecastSeries; + buckets: { name: string }[]; +}) { + const data = toChartRows(series, buckets); + return ( +
+
+ Series chart — orders · baseline · net +
+
+ + + + + + + + + + + + +
+
+ ); +} diff --git a/frontend/app/forecast/page.tsx b/frontend/app/forecast/page.tsx new file mode 100644 index 0000000000..23ef725127 --- /dev/null +++ b/frontend/app/forecast/page.tsx @@ -0,0 +1,325 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { useForecast } from "@/lib/useForecast"; +import { saveBulkOverrides } from "@/lib/forecastSave"; +import { applyFill, applyPercent, detectOutliers } from "@/lib/forecastEdit"; +import { loginUrl } from "@/lib/session"; +import { + type ForecastSeries, + type ForecastBucketMeta, + type Measure, +} from "@/lib/forecast"; +import { ForecastChart } from "./ForecastChart"; + +// Phase 1B Forecast Editor: a pivot of series x time buckets showing orders / +// baseline / override (editable) / net. Override edits persist to the engine +// (/forecast/detail/) which re-nets. Bulk fill / +-% across a row, outlier +// highlighting on the orders row. No top-300 cap. +const SHOWN: { measure: Measure; label: string; editable?: boolean }[] = [ + { measure: "orderstotal", label: "Orders" }, + { measure: "forecastbaseline", label: "Baseline" }, + { measure: "forecastoverride", label: "Override", editable: true }, + { measure: "forecastnet", label: "Net" }, +]; + +const fmt = (v: number | null | undefined) => + v == null ? "" : Number.isInteger(v) ? String(v) : v.toFixed(1); + +export default function ForecastPage() { + const { series, buckets, loading, error, authError, reload } = useForecast(); + const [draft, setDraft] = useState>({}); + const [saving, setSaving] = useState(false); + const [saveError, setSaveError] = useState(null); + const [charted, setCharted] = useState(null); + const chartedSeries = series.find((s) => s.key === charted) ?? null; + + const key = (s: string, b: string) => `${s} ${b}`; + + function setRowDraft(s: ForecastSeries, values: (number | null)[]) { + setDraft((d) => { + const next = { ...d }; + buckets.forEach((b, i) => { + next[key(s.key, b.name)] = values[i] == null ? "" : String(values[i]); + }); + return next; + }); + } + + function currentOverrides(s: ForecastSeries): (number | null)[] { + return buckets.map((b) => { + const k = key(s.key, b.name); + if (k in draft) { + const raw = draft[k].trim(); + return raw === "" ? null : Number(raw); + } + const v = s.buckets[b.name]?.forecastoverride; + return v == null ? null : v; + }); + } + + async function saveRow(s: ForecastSeries) { + const edits = buckets + .map((b) => ({ bucket: b, k: key(s.key, b.name) })) + .filter((e) => e.k in draft) + .map((e) => { + const raw = draft[e.k].trim(); + return { bucket: e.bucket, value: raw === "" ? null : Number(raw) }; + }) + .filter((e) => e.value === null || !Number.isNaN(e.value)); + if (edits.length === 0) return; + setSaving(true); + setSaveError(null); + try { + await saveBulkOverrides(s, edits); + setDraft((d) => { + const next = { ...d }; + for (const b of buckets) delete next[key(s.key, b.name)]; + return next; + }); + reload(); + } catch (e) { + setSaveError(e instanceof Error ? e.message : String(e)); + } finally { + setSaving(false); + } + } + + return ( +
+
+
+
Demand planning
+

Forecast

+

+ Item / location / customer demand across time buckets. Edit the + override row; the engine re-nets the forecast. +

+
+ {saving && ( + + saving + + )} +
+ + {authError && ( +
+ + + No active session. Sign in to + load forecast data. + +
+ )} + {loading &&
LOADING FORECAST…
} + {error && !authError && ( +
+ {error} +
+ )} + {saveError && ( +
+ {saveError} +
+ )} + {!loading && !error && series.length === 0 && ( +
No forecast series.
+ )} + + {series.length > 0 && ( +
+ + + + + + + {buckets.map((b) => ( + + ))} + + + + {series.map((s) => ( + setRowDraft(s, applyFill(v, buckets.length))} + onBulkPercent={(p) => + setRowDraft(s, applyPercent(currentOverrides(s), p)) + } + onSaveRow={() => saveRow(s)} + onChart={() => setCharted((c) => (c === s.key ? null : s.key))} + charted={charted === s.key} + /> + ))} + +
+ Forecast by item / location / customer over {buckets.length} time + buckets +
SeriesMeasure + {b.name} +
+
+ )} + {chartedSeries && } +
+ ); +} + +function SeriesRows({ + s, + buckets, + draft, + setDraft, + cellKey, + onBulkFill, + onBulkPercent, + onSaveRow, + onChart, + charted, +}: { + s: ForecastSeries; + buckets: ForecastBucketMeta[]; + draft: Record; + setDraft: (fn: (d: Record) => Record) => void; + cellKey: (s: string, b: string) => string; + onBulkFill: (v: number | null) => void; + onBulkPercent: (pct: number) => void; + onSaveRow: () => void; + onChart: () => void; + charted: boolean; +}) { + const title = useMemo( + () => + [s.fields.item, s.fields.location, s.fields.customer] + .filter(Boolean) + .join(" / "), + [s], + ); + const outliers = useMemo( + () => + detectOutliers(buckets.map((b) => s.buckets[b.name]?.orderstotal ?? null)), + [s, buckets], + ); + const [bulk, setBulk] = useState(""); + + return ( + <> + {SHOWN.map((row, ri) => ( + + {ri === 0 && ( + +
+ {title} + +
+ onBulkFill(bulk.trim() === "" ? null : Number(bulk))} + onPercent={() => onBulkPercent(Number(bulk) || 0)} + onSave={onSaveRow} + /> + + )} + + {row.label} + + {buckets.map((b, bi) => { + const v = s.buckets[b.name]?.[row.measure]; + const flagged = row.measure === "orderstotal" && outliers[bi]; + if (row.editable) { + const k = cellKey(s.key, b.name); + const shown = k in draft ? draft[k] : v == null ? "" : String(v); + return ( + + + setDraft((d) => ({ ...d, [k]: e.target.value })) + } + onKeyDown={(e) => { + if (e.key === "Enter") onSaveRow(); + }} + /> + + ); + } + return ( + + {fmt(v)} + + ); + })} + + ))} + + ); +} + +function BulkControls({ + value, + setValue, + onFill, + onPercent, + onSave, +}: { + value: string; + setValue: (v: string) => void; + onFill: () => void; + onPercent: () => void; + onSave: () => void; +}) { + return ( +
+ setValue(e.target.value)} + inputMode="decimal" + aria-label="bulk value or percent" + placeholder="value / %" + /> + + + +
+ ); +} diff --git a/frontend/app/globals.css b/frontend/app/globals.css new file mode 100644 index 0000000000..9a491dfb9b --- /dev/null +++ b/frontend/app/globals.css @@ -0,0 +1,1342 @@ +/* ─────────────────────────────────────────────────────────────────────────── + frePPLe — "planning instrument" design system + Industrial operations console: near-black base, amber signal, mono data, + hairline panels, blueprint grid. Fonts come from next/font (IBM Plex + Mono + Sans), exposed as --font-mono / --font-sans. + ─────────────────────────────────────────────────────────────────────────── */ + +:root { + /* surfaces */ + --bg: #0a0b0e; + --bg-2: #0d0f13; + --panel: #111419; + --panel-2: #161a20; + --raise: #1b2027; + + /* lines */ + --line: #22262e; + --line-bright: #2e333d; + --grid: rgba(245, 183, 51, 0.045); + + /* ink */ + --text: #e9eaed; + --muted: #8b919c; + --faint: #5b616d; + + /* signal */ + --signal: #f5b733; /* amber — brand + running/attention */ + --signal-ink: #1a1407; + --signal-dim: rgba(245, 183, 51, 0.12); + --ok: #74e08a; /* lime — done */ + --ok-dim: rgba(116, 224, 138, 0.14); + --fail: #ff5f5a; /* red — failed */ + --fail-dim: rgba(255, 95, 90, 0.14); + + /* legacy aliases (kept so any stray var() still resolves) */ + --accent: var(--signal); + --border: var(--line); + + /* metrics */ + --radius: 4px; + --radius-lg: 8px; + --rail: 232px; + --shadow: 0 1px 0 rgba(255, 255, 255, 0.03) inset, + 0 12px 36px -18px rgba(0, 0, 0, 0.9); + /* shared entrance easing — confident, slightly overshooting */ + --ease: cubic-bezier(0.16, 1, 0.3, 1); +} + +* { + box-sizing: border-box; +} + +html, +body { + margin: 0; + padding: 0; + min-height: 100%; +} + +body { + background-color: var(--bg); + /* blueprint dot-grid + faint amber glow for depth */ + background-image: + radial-gradient(var(--grid) 1px, transparent 1.4px), + radial-gradient( + 1100px 700px at 78% -8%, + rgba(245, 183, 51, 0.05), + transparent 60% + ), + linear-gradient(180deg, var(--bg-2), var(--bg)); + background-size: + 22px 22px, + 100% 100%, + 100% 100%; + background-attachment: fixed; + color: var(--text); + font-family: var(--font-sans, ui-sans-serif), system-ui, sans-serif; + font-size: 14px; + line-height: 1.55; + -webkit-font-smoothing: antialiased; + text-rendering: optimizeLegibility; +} + +/* fine film-grain over the whole canvas: breaks up the flat near-black and + gives the dark surfaces a tactile, printed-instrument depth. Fixed + behind + content, never intercepts pointer/much paint. */ +body::before { + content: ""; + position: fixed; + inset: 0; + z-index: 0; + pointer-events: none; + opacity: 0.035; + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='160' height='160'%3E%3Cfilter id='n'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='0.85' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23n)'/%3E%3C/svg%3E"); +} +.shell { + position: relative; + z-index: 1; +} + +/* one crisp, consistent keyboard-focus ring everywhere (refinement + a11y). + An OUTLINE, not a box-shadow: outlines aren't clipped by overflow:hidden + ancestors (the launch console, the gantt, the table/picker scrollers all clip) + and they follow each element's own border-radius - so the ring stays visible + and correctly shaped on every control. */ +:focus-visible { + outline: 2px solid var(--signal); + outline-offset: 2px; +} + +/* tabular figures everywhere numbers matter */ +.mono, +input, +table, +.metric, +.eyebrow, +.nav-item, +.btn { + font-family: var(--font-mono, ui-monospace), monospace; + font-variant-numeric: tabular-nums; + font-feature-settings: "tnum" 1; +} + +a { + color: var(--signal); + text-decoration: none; +} +a:hover { + text-decoration: underline; +} + +::selection { + background: var(--signal); + color: var(--signal-ink); +} + +*::-webkit-scrollbar { + width: 10px; + height: 10px; +} +*::-webkit-scrollbar-thumb { + background: var(--line-bright); + border-radius: 6px; + border: 2px solid transparent; + background-clip: padding-box; +} +*::-webkit-scrollbar-track { + background: transparent; +} + +/* ── app shell ──────────────────────────────────────────────────────────── */ +.shell { + display: grid; + grid-template-columns: var(--rail) 1fr; + min-height: 100vh; +} + +.sidebar { + position: sticky; + top: 0; + align-self: start; + height: 100vh; + display: flex; + flex-direction: column; + gap: 4px; + padding: 18px 14px; + border-right: 1px solid var(--line); + background: linear-gradient(180deg, var(--panel), var(--bg-2)); +} + +.brand { + display: flex; + align-items: center; + gap: 10px; + padding: 6px 8px 18px; +} +.brand-mark { + width: 28px; + height: 28px; + display: grid; + place-items: center; + border-radius: var(--radius); + background: var(--signal); + color: var(--signal-ink); + font-family: var(--font-mono), monospace; + font-weight: 700; + font-size: 15px; + box-shadow: 0 0 0 1px rgba(245, 183, 51, 0.4), + 0 8px 22px -8px var(--signal); +} +.brand-name { + font-family: var(--font-mono), monospace; + font-size: 15px; + letter-spacing: 0.14em; + text-transform: uppercase; +} +.brand-sub { + display: block; + font-size: 10px; + letter-spacing: 0.22em; + color: var(--faint); +} + +.nav { + display: flex; + flex-direction: column; + gap: 2px; + margin-top: 4px; +} +.nav-label { + font-size: 10px; + letter-spacing: 0.2em; + text-transform: uppercase; + color: var(--faint); + padding: 14px 8px 6px; +} +.nav-item { + position: relative; + display: flex; + align-items: center; + gap: 10px; + padding: 9px 10px; + border-radius: var(--radius); + color: var(--muted); + font-size: 12.5px; + letter-spacing: 0.08em; + text-transform: uppercase; + border: 1px solid transparent; + transition: + background 0.15s ease, + color 0.15s ease, + border-color 0.15s ease, + transform 0.15s var(--ease); +} +/* a left rail-bar that grows in on the active route — the instrument-panel tell */ +.nav-item::before { + content: ""; + position: absolute; + left: -14px; + top: 50%; + height: 0; + width: 3px; + border-radius: 0 2px 2px 0; + background: var(--signal); + box-shadow: 0 0 10px var(--signal); + transform: translateY(-50%); + transition: height 0.2s var(--ease); +} +.nav-item:hover { + color: var(--text); + background: var(--panel-2); + text-decoration: none; + transform: translateX(2px); +} +.nav-item[aria-current="page"] { + color: var(--text); + background: var(--signal-dim); + border-color: rgba(245, 183, 51, 0.3); +} +.nav-item[aria-current="page"]::before { + height: 22px; +} +.nav-item[aria-current="page"] .nav-tick { + background: var(--signal); + box-shadow: 0 0 10px var(--signal); +} +.nav-tick { + width: 6px; + height: 6px; + border-radius: 50%; + background: var(--line-bright); + flex: none; +} + +.sidebar-foot { + margin-top: auto; + display: flex; + flex-direction: column; + gap: 10px; + padding-top: 16px; + border-top: 1px solid var(--line); +} + +/* ── top status rail ────────────────────────────────────────────────────── */ +.main { + min-width: 0; + display: flex; + flex-direction: column; +} +.statusrail { + position: sticky; + top: 0; + z-index: 5; + display: flex; + align-items: center; + gap: 18px; + padding: 0 26px; + height: 46px; + border-bottom: 1px solid var(--line); + background: rgba(10, 11, 14, 0.72); + backdrop-filter: blur(8px); +} +/* a hairline amber underglow on the rail — a thin line of "power". */ +.statusrail::after { + content: ""; + position: absolute; + left: 0; + right: 0; + bottom: -1px; + height: 1px; + background: linear-gradient( + 90deg, + transparent, + rgba(245, 183, 51, 0.35) 18%, + rgba(245, 183, 51, 0.12) 60%, + transparent + ); +} +.stat { + position: relative; + display: flex; + align-items: center; + gap: 7px; + font-family: var(--font-mono), monospace; + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted); +} +/* hairline divider between adjacent rail stats */ +.statusrail .stat + .stat::before { + content: ""; + position: absolute; + left: -9px; + top: 50%; + height: 12px; + width: 1px; + background: var(--line-bright); + transform: translateY(-50%); +} +.rail-clock { + color: var(--text); + font-variant-numeric: tabular-nums; +} +.rail-clock .stat-key { + color: var(--faint); +} +.stat b { + color: var(--text); + font-weight: 600; +} +.stat-key { + color: var(--faint); +} +.rail-spacer { + flex: 1; +} + +.content { + padding: 30px 26px 80px; + max-width: 1180px; + width: 100%; +} + +/* ── page header ────────────────────────────────────────────────────────── */ +.pagehead { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 20px; + margin-bottom: 26px; + /* entrance handled by the .content > main > * stagger (pagehead is child 1) */ +} +.eyebrow { + font-size: 10.5px; + letter-spacing: 0.28em; + text-transform: uppercase; + color: var(--signal); +} +.h1 { + margin: 4px 0 0; + font-family: var(--font-mono), monospace; + font-size: 27px; + font-weight: 600; + letter-spacing: -0.01em; +} +.subtle { + color: var(--muted); + font-size: 13px; + margin: 6px 0 0; + max-width: 60ch; +} + +/* ── entrance orchestration ───────────────────────────────────────────────── + Each screen assembles top-to-bottom: the page sections rise + fade in a quick + stagger, so a load reads as the console powering up rather than a flat paint. + (Disabled wholesale under prefers-reduced-motion, see the media query below.) */ +.content > main > * { + animation: reveal 0.5s var(--ease) both; +} +.content > main > *:nth-child(1) { + animation-delay: 0.02s; +} +.content > main > *:nth-child(2) { + animation-delay: 0.09s; +} +.content > main > *:nth-child(3) { + animation-delay: 0.16s; +} +.content > main > *:nth-child(4) { + animation-delay: 0.23s; +} +.content > main > *:nth-child(n + 5) { + animation-delay: 0.3s; +} +/* the nav itself cascades in on first paint. `backwards` (not `both`) so the + finished animation doesn't lock `transform` and block the :hover nudge. */ +.nav-item { + animation: navin 0.5s var(--ease) backwards; +} +.nav-item:nth-child(2) { + animation-delay: 0.04s; +} +.nav-item:nth-child(3) { + animation-delay: 0.08s; +} +.nav-item:nth-child(4) { + animation-delay: 0.12s; +} +.nav-item:nth-child(5) { + animation-delay: 0.16s; +} +.nav-item:nth-child(6) { + animation-delay: 0.2s; +} +.nav-item:nth-child(n + 7) { + animation-delay: 0.24s; +} +@keyframes navin { + from { + opacity: 0; + transform: translateX(-6px); + } + to { + opacity: 1; + transform: none; + } +} + +/* ── dot / connection indicator ─────────────────────────────────────────── */ +.dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--faint); + flex: none; +} +.dot--live { + background: var(--ok); + box-shadow: 0 0 0 0 rgba(116, 224, 138, 0.5); + animation: pulse 2s infinite; +} +.dot--run { + background: var(--signal); +} +.dot--fail { + background: var(--fail); +} + +/* ── buttons ────────────────────────────────────────────────────────────── */ +.btn { + display: inline-flex; + align-items: center; + gap: 8px; + border: 1px solid var(--line-bright); + background: var(--panel-2); + color: var(--text); + border-radius: var(--radius); + padding: 8px 14px; + font-size: 12px; + letter-spacing: 0.08em; + text-transform: uppercase; + cursor: pointer; + transition: + transform 0.06s ease, + background 0.15s ease, + border-color 0.15s ease, + box-shadow 0.15s ease; +} +.btn:hover:not(:disabled) { + border-color: var(--faint); + background: var(--raise); +} +.btn:active:not(:disabled) { + transform: translateY(1px); +} +.btn:disabled { + opacity: 0.55; + cursor: not-allowed; +} +.btn-primary { + border-color: rgba(245, 183, 51, 0.5); + background: var(--signal); + color: var(--signal-ink); + font-weight: 700; + box-shadow: 0 10px 26px -12px var(--signal); +} +.btn-primary:hover:not(:disabled) { + background: #ffc649; + border-color: var(--signal); +} +.btn-primary.is-armed { + animation: armed 1.1s ease-in-out infinite; +} +.btn-mini { + padding: 4px 9px; + font-size: 11px; + letter-spacing: 0.04em; + text-transform: none; +} + +/* ── panels & cards ─────────────────────────────────────────────────────── */ +.panel { + position: relative; + background: linear-gradient(180deg, var(--panel), var(--panel-2)); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + box-shadow: var(--shadow); +} +/* a 1px top highlight so panels catch the light off the dark canvas */ +.panel::before { + content: ""; + position: absolute; + inset: 0 0 auto 0; + height: 1px; + border-radius: var(--radius-lg) var(--radius-lg) 0 0; + background: linear-gradient( + 90deg, + transparent, + rgba(255, 255, 255, 0.07) 30%, + rgba(255, 255, 255, 0.07) 70%, + transparent + ); +} +.panel-head { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; + padding: 13px 16px; + border-bottom: 1px solid var(--line); +} +.panel-title { + display: inline-flex; + align-items: center; + gap: 9px; + font-family: var(--font-mono), monospace; + font-size: 12px; + letter-spacing: 0.12em; + text-transform: uppercase; + color: var(--muted); +} +/* leading amber tick — the section marker */ +.panel-title::before { + content: ""; + width: 5px; + height: 5px; + border-radius: 1px; + background: var(--signal); + box-shadow: 0 0 8px var(--signal); + flex: none; + transform: rotate(45deg); +} + +/* launch console hero */ +.console { + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: 1px; + background: var(--line); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + overflow: hidden; + margin-bottom: 26px; + box-shadow: var(--shadow); + /* entrance handled by the .content > main > * stagger */ +} +.console-cell { + background: linear-gradient(180deg, var(--panel), var(--panel-2)); + padding: 22px 24px; +} +.console-cell.is-action { + display: flex; + flex-direction: column; + justify-content: center; + gap: 14px; + background: + radial-gradient( + 420px 180px at 100% 0%, + var(--signal-dim), + transparent 70% + ), + linear-gradient(180deg, var(--panel-2), var(--panel)); +} +.metric { + font-size: 30px; + font-weight: 600; + letter-spacing: -0.02em; +} +.metric-row { + display: flex; + gap: 30px; + flex-wrap: wrap; + margin-top: 6px; +} +.metric-block .eyebrow { + color: var(--faint); +} + +/* ── task feed ──────────────────────────────────────────────────────────── */ +.feed { + display: flex; + flex-direction: column; + gap: 10px; +} +.task { + width: 100%; + text-align: left; + background: linear-gradient(180deg, var(--panel), var(--panel-2)); + border: 1px solid var(--line); + border-radius: var(--radius-lg); + padding: 14px 16px; + cursor: pointer; + transition: + border-color 0.15s ease, + transform 0.1s ease; + animation: reveal 0.4s ease both; +} +.task:hover { + border-color: var(--line-bright); +} +.task.is-selected { + border-color: rgba(245, 183, 51, 0.45); + box-shadow: 0 0 0 1px rgba(245, 183, 51, 0.25); +} +.task-top { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} +.task-name { + font-family: var(--font-mono), monospace; + font-size: 13px; + letter-spacing: 0.02em; +} +.task-id { + color: var(--faint); +} +.tag { + font-family: var(--font-mono), monospace; + font-size: 10.5px; + letter-spacing: 0.1em; + text-transform: uppercase; + padding: 3px 8px; + border-radius: 100px; + border: 1px solid var(--line-bright); + color: var(--muted); + white-space: nowrap; +} +.tag--done { + color: var(--ok); + border-color: rgba(116, 224, 138, 0.4); + background: var(--ok-dim); +} +.tag--run { + color: var(--signal); + border-color: rgba(245, 183, 51, 0.4); + background: var(--signal-dim); +} +.tag--fail { + color: var(--fail); + border-color: rgba(255, 95, 90, 0.4); + background: var(--fail-dim); +} + +.bar { + height: 6px; + margin-top: 12px; + border-radius: 100px; + background: var(--bg); + border: 1px solid var(--line); + overflow: hidden; +} +.bar-fill { + height: 100%; + border-radius: 100px; + transition: width 0.4s cubic-bezier(0.2, 0.8, 0.2, 1); + background: var(--ok); +} +.bar-fill--run { + background: linear-gradient(90deg, var(--signal), #ffd373, var(--signal)); + background-size: 28px 100%; + animation: stripe 0.9s linear infinite; +} +.bar-fill--fail { + background: var(--fail); +} +.task-msg { + margin-top: 9px; + color: var(--muted); + font-size: 12px; +} + +/* ── log panel ──────────────────────────────────────────────────────────── */ +.log { + margin: 0; + padding: 16px; + max-height: 340px; + overflow: auto; + font-family: var(--font-mono), monospace; + font-size: 12px; + line-height: 1.65; + color: #c9cdd6; + white-space: pre-wrap; +} + +/* ── notices / empty states ─────────────────────────────────────────────── */ +.notice { + display: flex; + align-items: center; + gap: 12px; + padding: 14px 16px; + border-radius: var(--radius-lg); + border: 1px solid var(--line); + background: var(--panel); + color: var(--muted); + font-size: 13px; +} +.notice--error { + border-color: rgba(255, 95, 90, 0.4); + background: var(--fail-dim); + color: #ffd0ce; +} +.notice--auth { + border-color: rgba(245, 183, 51, 0.4); + background: var(--signal-dim); + color: #ffe6ad; +} +.empty { + padding: 40px 16px; + text-align: center; + color: var(--faint); + border: 1px dashed var(--line-bright); + border-radius: var(--radius-lg); + font-family: var(--font-mono), monospace; + font-size: 12.5px; + letter-spacing: 0.06em; +} + +/* ── toasts ─────────────────────────────────────────────────────────────── */ +.toaststack { + position: fixed; + right: 22px; + bottom: 22px; + z-index: 50; + display: flex; + flex-direction: column; + gap: 10px; + max-width: 360px; +} +.toast { + display: flex; + align-items: flex-start; + gap: 10px; + padding: 12px 14px; + border-radius: var(--radius-lg); + border: 1px solid var(--line-bright); + background: var(--raise); + box-shadow: 0 18px 50px -18px rgba(0, 0, 0, 0.95); + animation: toastin 0.32s cubic-bezier(0.2, 0.9, 0.2, 1) both; + font-size: 13px; +} +.toast::before { + content: ""; + width: 3px; + align-self: stretch; + border-radius: 3px; + background: var(--signal); + flex: none; +} +.toast--ok::before { + background: var(--ok); +} +.toast--error::before { + background: var(--fail); +} +.toast-title { + font-family: var(--font-mono), monospace; + font-size: 11px; + letter-spacing: 0.1em; + text-transform: uppercase; + color: var(--muted); +} + +/* ── instrument table (forecast) ────────────────────────────────────────── */ +/* ── tab bar (problems / orders list screens) ───────────────────────────── */ +.tabbar { + display: inline-flex; + gap: 2px; + margin-bottom: 16px; + padding: 3px; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + background: var(--panel); +} +.tab { + border: 1px solid transparent; + background: transparent; + color: var(--muted); + font-family: var(--font-mono), monospace; + font-size: 12px; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 7px 14px; + border-radius: var(--radius); + cursor: pointer; + transition: + background 0.15s ease, + color 0.15s ease; +} +.tab:hover { + color: var(--text); + background: var(--panel-2); +} +.tab.is-active { + color: var(--signal); + background: var(--signal-dim); + border-color: rgba(245, 183, 51, 0.3); +} + +/* ── record toolbar + status pills + editable grid (orders) ─────────────── */ +.record-toolbar { + display: flex; + align-items: center; + gap: 12px; + margin-bottom: 12px; +} + +/* status pill: a scannable chip — amber proposed, lime firm, muted done */ +.pill { + display: inline-block; + font-family: var(--font-mono), monospace; + font-size: 10px; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + padding: 2px 8px; + border-radius: 999px; + color: var(--faint); + background: var(--raise); + border: 1px solid var(--line-bright); +} +.pill--run { + color: var(--signal); + background: var(--signal-dim); + border-color: rgba(245, 183, 51, 0.35); +} +.pill--ok { + color: var(--ok); + background: var(--ok-dim); + border-color: rgba(116, 224, 138, 0.35); +} +.pill--done { + color: var(--muted); +} + +/* editable grid: a row in edit mode gets a live amber rail; saving pulses. */ +.grid-row--editing td { + background: var(--signal-dim); + box-shadow: inset 3px 0 0 var(--signal); +} +.grid-row--busy { + animation: barpending 0.9s ease-in-out infinite; +} +.row-actions { + white-space: nowrap; + text-align: right; +} +/* actions reveal on row hover (always shown for the row being edited) */ +.grid tbody tr:not(.grid-row--editing) .row-actions .rowbtn { + opacity: 0; + transition: opacity 0.12s ease; +} +.grid tbody tr:hover .row-actions .rowbtn, +.grid tbody tr:focus-within .row-actions .rowbtn { + opacity: 1; +} +.rowbtn { + font-family: var(--font-mono), monospace; + font-size: 11px; + text-transform: uppercase; + letter-spacing: 0.04em; + padding: 3px 8px; + margin-left: 4px; + border-radius: var(--radius); + border: 1px solid var(--line-bright); + background: var(--panel-2); + color: var(--muted); + cursor: pointer; +} +.rowbtn:hover:not(:disabled) { + color: var(--text); + background: var(--raise); +} +.rowbtn:disabled { + opacity: 0.5; + cursor: not-allowed; +} +.rowbtn--ok { + color: var(--signal-ink); + background: var(--signal); + border-color: rgba(245, 183, 51, 0.5); + font-weight: 700; +} +.rowbtn--ok:hover:not(:disabled) { + background: #ffc649; + color: var(--signal-ink); +} +.rowbtn--danger:hover:not(:disabled) { + color: var(--fail); + border-color: var(--fail); +} +.row-confirm { + font-family: var(--font-mono), monospace; + font-size: 11px; + text-transform: uppercase; + color: var(--fail); + margin-right: 4px; +} +.row-locked { + font-family: var(--font-mono), monospace; + font-size: 10px; + text-transform: uppercase; + letter-spacing: 0.06em; + color: var(--faint); +} + +.tablewrap { + overflow: auto; + border: 1px solid var(--line); + border-radius: var(--radius-lg); + background: var(--panel); + box-shadow: var(--shadow); + animation: reveal 0.5s 0.05s ease both; +} +table.grid { + border-collapse: separate; + border-spacing: 0; + font-size: 12.5px; + width: 100%; +} +table.grid caption { + text-align: left; + padding: 12px 14px; + color: var(--muted); + font-family: var(--font-mono), monospace; + font-size: 11px; + letter-spacing: 0.08em; + text-transform: uppercase; + border-bottom: 1px solid var(--line); +} +table.grid th, +table.grid td { + padding: 6px 10px; + border-bottom: 1px solid var(--line); + white-space: nowrap; +} +table.grid thead th { + position: sticky; + top: 0; + z-index: 2; + background: var(--panel-2); + color: var(--muted); + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + font-size: 11px; + border-bottom: 1px solid var(--line-bright); +} +.num { + text-align: right; + font-variant-numeric: tabular-nums; +} +.measure { + color: var(--faint); + text-transform: uppercase; + font-size: 11px; + letter-spacing: 0.04em; +} +.measure--override { + color: var(--signal); +} +.series-cell { + vertical-align: top; + background: var(--bg-2); + border-right: 1px solid var(--line); + min-width: 200px; +} +.series-name { + font-family: var(--font-mono), monospace; + font-size: 12.5px; + display: flex; + align-items: center; + gap: 6px; +} +.cell--outlier { + background: var(--fail-dim); + color: #ffd0ce; + font-weight: 600; +} +.cell-input { + width: 70px; + text-align: right; + background: var(--bg); + color: var(--text); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 3px 6px; + font-size: 12px; + transition: border-color 0.12s ease, box-shadow 0.12s ease; +} +.cell-input:focus { + outline: none; + border-color: var(--signal); + box-shadow: 0 0 0 2px var(--signal-dim); +} +.bulk { + display: flex; + gap: 5px; + margin-top: 9px; + flex-wrap: wrap; + align-items: center; +} +.bulk input { + width: 74px; + background: var(--bg); + color: var(--text); + border: 1px solid var(--line); + border-radius: var(--radius); + padding: 4px 6px; + font-size: 12px; +} + +/* ── keyframes ──────────────────────────────────────────────────────────── */ +@keyframes reveal { + from { + opacity: 0; + transform: translateY(8px); + } + to { + opacity: 1; + transform: none; + } +} +@keyframes pulse { + 0% { + box-shadow: 0 0 0 0 rgba(116, 224, 138, 0.5); + } + 70% { + box-shadow: 0 0 0 7px rgba(116, 224, 138, 0); + } + 100% { + box-shadow: 0 0 0 0 rgba(116, 224, 138, 0); + } +} +@keyframes stripe { + to { + background-position: 28px 0; + } +} +@keyframes armed { + 0%, + 100% { + box-shadow: 0 10px 26px -12px var(--signal); + } + 50% { + box-shadow: 0 0 0 4px var(--signal-dim), 0 10px 26px -12px var(--signal); + } +} +@keyframes toastin { + from { + opacity: 0; + transform: translateX(16px) scale(0.98); + } + to { + opacity: 1; + transform: none; + } +} + +@media (prefers-reduced-motion: reduce) { + * { + animation-duration: 0.001ms !important; + animation-iteration-count: 1 !important; + } +} + +/* ── demand pegging: picker + gantt ─────────────────────────────────────── */ +.picker { + display: flex; + flex-direction: column; + gap: 2px; + max-height: 220px; + overflow-y: auto; +} +.picker-item { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 12px; + text-align: left; + padding: 7px 10px; + border: 1px solid transparent; + border-radius: 6px; + background: transparent; + color: var(--text); + cursor: pointer; + font: inherit; +} +.picker-item:hover { + background: var(--raise); + border-color: var(--line); +} +.picker-item.is-selected { + background: var(--signal-dim); + border-color: var(--signal); +} +.picker-name { + font-family: var(--font-mono); + font-size: 13px; +} +.picker-meta { + font-family: var(--font-mono); + font-size: 11px; + color: var(--faint); + text-transform: uppercase; + letter-spacing: 0.04em; +} + +.gantt { + border: 1px solid var(--line); + border-radius: 10px; + overflow: hidden; + background: var(--panel); +} +.gantt-head, +.gantt-row { + display: grid; + grid-template-columns: 280px 1fr; + align-items: stretch; +} +.gantt-row { + border-top: 1px solid var(--line); +} +.gantt-row:hover { + background: var(--bg-2); +} +/* D3: a downstream step whose timing may shift after a reschedule, until re-plan */ +.gantt-row--affected { + background: var(--signal-dim); + box-shadow: inset 3px 0 0 var(--signal); +} +.gantt-row--affected:hover { + background: var(--signal-dim); +} +.gantt-head { + background: var(--panel-2); +} +.gantt-label { + display: flex; + align-items: center; + gap: 8px; + padding: 8px 10px; + border-right: 1px solid var(--line); + min-width: 0; +} +.gantt-label--head, +.gantt-lane--head { + font-family: var(--font-mono); + font-size: 10.5px; + letter-spacing: 0.18em; + text-transform: uppercase; + color: var(--faint); +} +.gantt-op { + font-family: var(--font-mono); + font-size: 12.5px; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} +.gantt-type { + flex: none; + font-family: var(--font-mono); + font-size: 9.5px; + font-weight: 700; + letter-spacing: 0.06em; + padding: 2px 5px; + border-radius: 4px; + color: var(--muted); + background: var(--raise); + border: 1px solid var(--line-bright); +} +.gantt-type--dlvr { + color: var(--signal); + background: var(--signal-dim); + border-color: var(--signal); +} +.gantt-lane { + position: relative; + height: 30px; + margin: 6px 0; +} +.gantt-lane--head { + height: 22px; + margin: 0; +} +.gantt-tick { + position: absolute; + top: 4px; + transform: translateX(-50%); + white-space: nowrap; +} +.gantt-tick::before { + content: ""; + position: absolute; + top: 18px; + left: 50%; + width: 1px; + height: 9999px; + background: var(--line); + opacity: 0.5; +} +.gantt-bar { + position: absolute; + top: 4px; + height: 22px; + display: flex; + align-items: center; + justify-content: flex-end; + padding: 0 5px; + border-radius: 4px; + box-sizing: border-box; + overflow: hidden; + font-family: var(--font-mono); + font-size: 10px; +} +.gantt-bar-q { + color: var(--text); + opacity: 0.85; +} +.gantt-bar--proposed { + background: var(--signal-dim); + border: 1px solid var(--signal); +} +.gantt-bar--firm { + background: var(--ok-dim); + border: 1px solid var(--ok); +} +.gantt-bar--done { + background: var(--raise); + border: 1px solid var(--line-bright); +} +/* D2 reschedule states */ +.gantt-bar--editable { + cursor: grab; + touch-action: none; /* let pointer-drag own horizontal gestures */ +} +.gantt-bar--editable:hover { + filter: brightness(1.15); +} +.gantt-bar--dragging { + cursor: grabbing; + z-index: 3; + box-shadow: 0 0 0 1px var(--signal), 0 6px 16px -6px rgba(0, 0, 0, 0.8); +} +.gantt-bar--pending { + animation: barpending 0.9s ease-in-out infinite; +} +@keyframes barpending { + 0%, + 100% { + opacity: 1; + } + 50% { + opacity: 0.45; + } +} +.gantt-marker { + position: absolute; + top: -3px; + bottom: -3px; + width: 2px; + z-index: 2; +} +.gantt-marker--due { + background: var(--signal); + box-shadow: 0 0 6px var(--signal); +} +.gantt-marker--now { + background: var(--line-bright); +} + +/* ── responsive ─────────────────────────────────────────────────────────── */ +@media (max-width: 860px) { + .shell { + grid-template-columns: 1fr; + } + .sidebar { + position: static; + height: auto; + flex-direction: row; + align-items: center; + flex-wrap: wrap; + gap: 10px; + } + .sidebar-foot { + margin: 0; + border: none; + padding: 0; + } + .nav { + flex-direction: row; + } + .nav-label { + display: none; + } + .console { + grid-template-columns: 1fr; + } +} diff --git a/frontend/app/inventory/page.tsx b/frontend/app/inventory/page.tsx new file mode 100644 index 0000000000..03b5b82002 --- /dev/null +++ b/frontend/app/inventory/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +import PivotScreen from "@/components/PivotScreen"; +import { INVENTORY } from "@/lib/inventory"; + +// Phase 3 — Inventory/Buffer report. Read-only pivot of buffers x time buckets +// from the real plan (/api/output/inventory/, enriched PivotJSONStreamView). +export default function InventoryPage() { + return ; +} diff --git a/frontend/app/layout.tsx b/frontend/app/layout.tsx new file mode 100644 index 0000000000..f84cebe504 --- /dev/null +++ b/frontend/app/layout.tsx @@ -0,0 +1,41 @@ +import type { Metadata } from "next"; +import { IBM_Plex_Mono, IBM_Plex_Sans } from "next/font/google"; +import "./globals.css"; +import AppShell from "@/components/AppShell"; +import { ToastProvider } from "@/components/Toast"; + +// Industrial-console type system: IBM Plex Mono carries the data, labels and +// headings (engineering pedigree, tabular figures); IBM Plex Sans the prose. +const mono = IBM_Plex_Mono({ + subsets: ["latin"], + weight: ["400", "500", "600", "700"], + variable: "--font-mono", + display: "swap", +}); +const sans = IBM_Plex_Sans({ + subsets: ["latin"], + weight: ["400", "500", "600"], + variable: "--font-sans", + display: "swap", +}); + +export const metadata: Metadata = { + title: "frePPLe — Planning Console", + description: "frePPLe supply-chain planning & forecasting console", +}; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + + {children} + + + + ); +} diff --git a/frontend/app/orders/page.tsx b/frontend/app/orders/page.tsx new file mode 100644 index 0000000000..40cd611182 --- /dev/null +++ b/frontend/app/orders/page.tsx @@ -0,0 +1,31 @@ +"use client"; + +import TabListScreen from "@/components/TabListScreen"; +import { ORDER_TABS, canEditOrder, deleteOrder, patchOrder } from "@/lib/orders"; +import type { RecordRow } from "@/lib/records"; + +// Orders screen (Phase 3): manufacturing / purchase / distribution order summaries +// from the input REST API. One tab per order type, per-type columns, with inline +// edit (quantity / dates / status) + delete; executed orders are locked. +// (Create needs an operation/item picker — a documented follow-on.) +export default function OrdersPage() { + return ( + + patchOrder(endpoint, String(row.reference), changes), + remove: (endpoint, row: RecordRow) => + deleteOrder(endpoint, String(row.reference)), + }} + emptyText="NO ORDERS — NONE PLANNED YET" + /> + ); +} diff --git a/frontend/app/page.tsx b/frontend/app/page.tsx new file mode 100644 index 0000000000..501b5e109f --- /dev/null +++ b/frontend/app/page.tsx @@ -0,0 +1,5 @@ +import { redirect } from "next/navigation"; + +export default function Home() { + redirect("/execute"); +} diff --git a/frontend/app/pegging/PeggingGantt.tsx b/frontend/app/pegging/PeggingGantt.tsx new file mode 100644 index 0000000000..e4e1489cb9 --- /dev/null +++ b/frontend/app/pegging/PeggingGantt.tsx @@ -0,0 +1,223 @@ +"use client"; + +import { useRef, useState } from "react"; +import { + axisTicks, + fractionOf, + parseEngineDate, + type Pegging, + type PeggingBar, +} from "@/lib/pegging"; +import { isReschedulable, shiftEngineDate } from "@/lib/reschedule"; + +// Demand-pegging Gantt. D1 rendered it read-only; D2 adds pointer-drag +// rescheduling: drag a bar horizontally to shift its start/end by the dragged +// time delta, then the page PATCHes the operationplan's dates. Bars are +// absolutely positioned by date fraction (HTML/CSS, not SVG) so the drag is a +// straight px->fraction->time mapping over the lane width. + +const STATUS_CLASS: Record = { + proposed: "gantt-bar--proposed", + approved: "gantt-bar--firm", + confirmed: "gantt-bar--firm", + completed: "gantt-bar--done", + closed: "gantt-bar--done", +}; + +// Below this drag distance (fraction of the lane) we treat the gesture as a +// click, not a reschedule. +const DRAG_THRESHOLD = 0.004; + +function barTooltip(b: PeggingBar, editable: boolean): string { + const dates = + b.start && b.end && b.start !== b.end + ? `${b.start} → ${b.end}` + : b.start || b.end || "no dates"; + const risk = b.criticality > 0 ? `\ncriticality ${b.criticality}` : ""; + const hint = editable ? "\n(drag to reschedule)" : ""; + return `${b.type} ${b.reference}\n${b.operation}\nqty ${b.quantity}\n${b.status} · ${dates}${risk}${hint}`; +} + +export default function PeggingGantt({ + pegging, + onReschedule, + affected, +}: { + pegging: Pegging; + // Provided => bars are draggable; resolves once the PATCH persisted (the page + // then reloads), rejects on failure (the bar snaps back to server state). The + // `rowId` lets the page flag the affected downstream chain (D3). + onReschedule?: ( + bar: PeggingBar, + rowId: string, + startdate: string, + enddate: string, + ) => Promise; + // Row ids whose timing depends on a just-rescheduled op (D3) — highlighted as + // "impact pending" until a re-plan recomputes the peg. + affected?: Set; +}) { + const { window, rows } = pegging; + const startMs = parseEngineDate(window.start); + const endMs = parseEngineDate(window.end); + const spanMs = endMs - startMs; + const ticks = axisTicks(window); + const dueFrac = fractionOf(window.due, startMs, endMs); + const nowFrac = fractionOf(window.current, startMs, endMs); + + // Live drag offset for the bar being dragged (keyed by operationplan ref), and + // the ref currently mid-PATCH (rendered pending). dragInfo holds the immutable + // gesture context so move/up don't depend on render state. + const [drag, setDrag] = useState<{ ref: string; deltaFrac: number } | null>(null); + const [pendingRef, setPendingRef] = useState(null); + const dragInfo = useRef<{ + startX: number; + laneW: number; + baseLeft: number; + bar: PeggingBar; + rowId: string; + } | null>(null); + + if (!rows.length) { + return ( +
+ NO PEGGING — THIS DEMAND IS UNPLANNED. RUN A PLAN FIRST. +
+ ); + } + + function onPointerDown( + e: React.PointerEvent, + bar: PeggingBar, + baseLeft: number, + rowId: string, + ) { + const lane = e.currentTarget.parentElement; + if (!lane) return; + dragInfo.current = { + startX: e.clientX, + laneW: lane.getBoundingClientRect().width || 1, + baseLeft, + bar, + rowId, + }; + e.currentTarget.setPointerCapture(e.pointerId); + setDrag({ ref: bar.reference, deltaFrac: 0 }); + } + + function onPointerMove(e: React.PointerEvent) { + const info = dragInfo.current; + if (!info || !drag) return; + const raw = (e.clientX - info.startX) / info.laneW; + // keep the bar's left edge on-canvas + const deltaFrac = Math.max(-info.baseLeft, Math.min(0.99 - info.baseLeft, raw)); + setDrag({ ref: info.bar.reference, deltaFrac }); + } + + function onPointerUp(e: React.PointerEvent) { + const info = dragInfo.current; + const d = drag; + dragInfo.current = null; + setDrag(null); + if (!info || !d) return; + e.currentTarget.releasePointerCapture(e.pointerId); + if (Math.abs(d.deltaFrac) < DRAG_THRESHOLD || !onReschedule) return; // a click + const deltaMs = d.deltaFrac * spanMs; + const ns = shiftEngineDate(info.bar.start || info.bar.end, deltaMs); + const ne = shiftEngineDate(info.bar.end || info.bar.start, deltaMs); + if (!ns || !ne) return; + setPendingRef(info.bar.reference); + onReschedule(info.bar, info.rowId, ns, ne).finally(() => setPendingRef(null)); + } + + return ( +
+
+
+ Supply step +
+
+ {ticks.map((t, i) => ( + + {t.label} + + ))} +
+
+ + {rows.map((row) => ( +
+
+ + {row.type || "—"} + + {row.operation} +
+
+ {nowFrac != null && ( + + )} + {dueFrac != null && ( + + )} + {row.bars.map((b, i) => { + const baseLeft = fractionOf(b.start || b.end, startMs, endMs); + if (baseLeft == null) return null; + const endF = fractionOf(b.end || b.start, startMs, endMs) ?? baseLeft; + const dragging = drag?.ref === b.reference ? drag.deltaFrac : 0; + const left = baseLeft + dragging; + const widthPct = Math.max(0.8, (endF - baseLeft) * 100); + const editable = !!onReschedule && isReschedulable(b.type, b.status); + const cls = STATUS_CLASS[b.status] ?? "gantt-bar--proposed"; + return ( + onPointerDown(e, b, baseLeft, row.id) + : undefined + } + onPointerMove={editable ? onPointerMove : undefined} + onPointerUp={editable ? onPointerUp : undefined} + > + {b.quantity} + + ); + })} +
+
+ ))} +
+ ); +} diff --git a/frontend/app/pegging/page.tsx b/frontend/app/pegging/page.tsx new file mode 100644 index 0000000000..c9ec7b1143 --- /dev/null +++ b/frontend/app/pegging/page.tsx @@ -0,0 +1,230 @@ +"use client"; + +import { Suspense, useEffect, useMemo, useState } from "react"; +import { useRouter, useSearchParams } from "next/navigation"; +import { isAuthError } from "@/lib/errors"; +import { loginUrl } from "@/lib/session"; +import { useDemandList } from "@/lib/useDemandList"; +import { usePegging } from "@/lib/usePegging"; +import { useReplan } from "@/lib/useReplan"; +import { patchReschedule } from "@/lib/reschedule"; +import { useToast } from "@/components/Toast"; +import { downstreamChain, type PeggingBar } from "@/lib/pegging"; +import PeggingGantt from "./PeggingGantt"; + +// Demand Pegging Gantt screen (Phase 3-D1 read / D2 reschedule / D3 re-plan loop). +// Pick a demand; trace the supply chain on a dated Gantt; drag a bar to +// reschedule the operationplan; re-plan in place to recompute the peg. +function PeggingScreen() { + const router = useRouter(); + const params = useSearchParams(); + const selected = params.get("demand") ?? ""; + const [q, setQ] = useState(""); + // After a reschedule the peg is stale; `affected` are the downstream rows whose + // timing may shift (highlighted until a re-plan recomputes the real result). + const [stale, setStale] = useState(false); + const [affected, setAffected] = useState>(new Set()); + const toast = useToast(); + + const { demands, authError: listAuth } = useDemandList(); + const { + pegging, + loading, + error, + authError: pegAuth, + reload, + } = usePegging(selected); + const { replan, running: replanning } = useReplan(); + const authError = listAuth || pegAuth; + + // A reschedule persists dates but does NOT recompute the peg — only a re-plan + // does. Clear the stale hint + affected highlight whenever the demand changes. + useEffect(() => { + setStale(false); + setAffected(new Set()); + }, [selected]); + + // Drag-drop reschedule: PATCH the operationplan's dates, flag the downstream + // chain (D3), then reload so the Gantt reflects the persisted state. On failure + // reload too, snapping the bar back; rethrow so the bar clears its pending UI. + async function handleReschedule( + bar: PeggingBar, + rowId: string, + startdate: string, + enddate: string, + ) { + try { + await patchReschedule({ + type: bar.type, + reference: bar.reference, + startdate, + enddate, + }); + toast("ok", "Rescheduled", `${bar.type} ${bar.reference} → ${startdate.slice(0, 10)}.`); + const rows = pegging?.rows ?? []; + setAffected(downstreamChain(rows, rows.findIndex((r) => r.id === rowId))); + setStale(true); + reload(); + } catch (e) { + if (isAuthError(e)) { + toast("error", "Sign-in required", "Sign in to reschedule."); + } else { + toast("error", "Reschedule failed", e instanceof Error ? e.message : String(e)); + } + reload(); + throw e; + } + } + + // The re-plan loop: run the engine, then re-fetch the (now authoritative) + // pegging and clear the stale/affected hints. + async function handleReplan() { + try { + await replan(); + toast("ok", "Re-planned", "Pegging refreshed from the engine."); + setStale(false); + setAffected(new Set()); + reload(); + } catch (e) { + if (isAuthError(e)) { + toast("error", "Sign-in required", "Sign in to re-plan."); + } else { + toast("error", "Re-plan failed", e instanceof Error ? e.message : String(e)); + } + } + } + + const matches = useMemo(() => { + const needle = q.trim().toLowerCase(); + const list = needle + ? demands.filter( + (d) => + d.name.toLowerCase().includes(needle) || + (d.item ?? "").toLowerCase().includes(needle), + ) + : demands; + return list.slice(0, 50); + }, [demands, q]); + + function pick(name: string) { + const sp = new URLSearchParams(Array.from(params.entries())); + if (name) sp.set("demand", name); + else sp.delete("demand"); + router.replace(`/pegging?${sp.toString()}`); + } + + return ( +
+
+
+
Plan analysis
+

Demand pegging

+

+ Trace the supply chain that pegs to a sales order — every + operationplan feeding the delivery, on one dated timeline. +

+
+
+ + {authError && ( +
+ + + No active session.{" "} + Sign in to load pegging. + +
+ )} + +
+
+ Demand + {selected && {selected}} +
+
+ setQ(e.target.value)} + aria-label="Filter demands" + /> +
+ {matches.map((d) => ( + + ))} + {!matches.length && ( + + NO DEMANDS MATCH + + )} +
+
+
+ + {!selected && ( +
SELECT A DEMAND TO TRACE ITS PEGGING
+ )} + {selected && loading &&
LOADING PEGGING…
} + {selected && error && !authError && ( +
+ + Could not load pegging: {error} +
+ )} + {stale && ( +
+ + + Dates saved. Highlighted steps may shift — the peg recomputes when you + re-plan. + + +
+ )} + {selected && !loading && !error && pegging && ( + + )} +
+ ); +} + +export default function PeggingPage() { + // useSearchParams requires a Suspense boundary in the App Router. + return ( + LOADING…}> + + + ); +} diff --git a/frontend/app/problems/page.tsx b/frontend/app/problems/page.tsx new file mode 100644 index 0000000000..fe6423c361 --- /dev/null +++ b/frontend/app/problems/page.tsx @@ -0,0 +1,20 @@ +"use client"; + +import TabListScreen from "@/components/TabListScreen"; +import { PROBLEM_COLUMNS, PROBLEM_TABS } from "@/lib/problems"; + +// Problems / Constraints screen (Phase 3): the violation lists the engine flags — +// late demands, capacity overloads, material shortages. Two tabs, shared columns. +export default function ProblemsPage() { + return ( + + ); +} diff --git a/frontend/app/resource/page.tsx b/frontend/app/resource/page.tsx new file mode 100644 index 0000000000..61b791c3f8 --- /dev/null +++ b/frontend/app/resource/page.tsx @@ -0,0 +1,10 @@ +"use client"; + +import PivotScreen from "@/components/PivotScreen"; +import { RESOURCE } from "@/lib/resource"; + +// Phase 3 — Resource report. Read-only pivot of resources x time buckets +// (/api/output/resource/, enriched PivotJSONStreamView). +export default function ResourcePage() { + return ; +} diff --git a/frontend/components/AppShell.tsx b/frontend/components/AppShell.tsx new file mode 100644 index 0000000000..1b633abb16 --- /dev/null +++ b/frontend/components/AppShell.tsx @@ -0,0 +1,176 @@ +"use client"; + +import Link from "next/link"; +import { usePathname } from "next/navigation"; +import { useEffect, useState, type ReactNode } from "react"; +import { useSession } from "@/lib/useSession"; +import { loginUrl, logoutUrl } from "@/lib/session"; + +const NAV = [ + { href: "/execute", label: "Execute", hint: "Plan runs" }, + { href: "/forecast", label: "Forecast", hint: "Demand editor" }, + { href: "/demand", label: "Demand", hint: "Sales orders" }, + { href: "/pegging", label: "Pegging", hint: "Supply trace" }, + { href: "/inventory", label: "Inventory", hint: "On-hand & supply" }, + { href: "/orders", label: "Orders", hint: "MO / PO / DO" }, + { href: "/resource", label: "Resource", hint: "Capacity & load" }, + { href: "/problems", label: "Problems", hint: "Violations" }, +]; + +// The persistent console chrome: a left rail (brand + nav + session) and a top +// status bar (scenario / environment / who's signed in). Wraps every screen. +export default function AppShell({ children }: { children: ReactNode }) { + const pathname = usePathname() || "/"; + const { session, status } = useSession(); + + return ( +
+ + +
+
+ + scenario default + + + env staging + + + + +
+
{children}
+
+
+ ); +} + +// A live UTC mission-clock in the status rail — the small heartbeat that makes +// the console feel "on". Renders nothing until mounted so SSR and the first +// client paint match (no hydration mismatch from a server-vs-client time). +function RailClock() { + const [now, setNow] = useState(null); + useEffect(() => { + const tick = () => + setNow( + new Date().toLocaleTimeString("en-GB", { + hour: "2-digit", + minute: "2-digit", + second: "2-digit", + timeZone: "UTC", + }), + ); + tick(); + const id = setInterval(tick, 1000); + return () => clearInterval(id); + }, []); + if (!now) return null; + return ( + + + utc {now} + + ); +} + +function SessionStat({ + session, + status, + path, +}: { + session: ReturnType["session"]; + status: ReturnType["status"]; + path: string; +}) { + if (status === "authed" && session) { + return ( + + + {session.user} + + ); + } + if (status === "loading") { + return ( + + checking… + + ); + } + return ( + + sign in + + ); +} + +function SessionBlock({ + session, + status, + path, +}: { + session: ReturnType["session"]; + status: ReturnType["status"]; + path: string; +}) { + if (status === "authed" && session) { + return ( + <> + + signed in as{" "} + {session.user} + + + Sign out + + + ); + } + if (status === "loading") { + return ( + + connecting… + + ); + } + return ( + <> + + {status === "offline" ? "server unreachable" : "no active session"} + + + Sign in + + + ); +} diff --git a/frontend/components/PivotScreen.tsx b/frontend/components/PivotScreen.tsx new file mode 100644 index 0000000000..94050386f0 --- /dev/null +++ b/frontend/components/PivotScreen.tsx @@ -0,0 +1,124 @@ +"use client"; + +import { usePivotReport } from "@/lib/usePivotReport"; +import { loginUrl } from "@/lib/session"; +import type { PivotSeries, BucketMeta } from "@/lib/pivot"; + +// Generic read-only instrument grid for an enriched GridPivot OUTPUT report +// (inventory / demand / resource). A screen is just a config object; the table +// renders series x shown-measures x buckets. Reuses the design-system classes. +export type PivotScreenConfig = { + endpoint: string; // e.g. "/api/output/demand/" + keyField: string; // series identity row-field (e.g. "item", "resource") + eyebrow: string; + title: string; // h1 + accessible heading name + subtitle: string; + emptyText: string; // shown when the report has no series + shown: { measure: string; label: string }[]; + titleOf: (s: PivotSeries) => string; +}; + +const fmt = (v: number | null | undefined) => + v == null ? "" : Number.isInteger(v) ? String(v) : v.toFixed(1); + +export default function PivotScreen(cfg: PivotScreenConfig) { + const { series, buckets, loading, error, authError } = usePivotReport( + cfg.endpoint, + cfg.keyField, + ); + + return ( +
+
+
+
{cfg.eyebrow}
+

{cfg.title}

+

{cfg.subtitle}

+
+
+ + {authError && ( +
+ + + No active session.{" "} + Sign in to load data. + +
+ )} + {loading &&
LOADING…
} + {error && !authError && ( +
+ {error} +
+ )} + {!loading && !error && series.length === 0 && ( +
{cfg.emptyText}
+ )} + + {series.length > 0 && ( +
+ + + + + + + {buckets.map((b) => ( + + ))} + + + + {series.map((s) => ( + + ))} + +
+ {cfg.title} by series over {buckets.length} time buckets +
SeriesMeasure + {b.name} +
+
+ )} +
+ ); +} + +function SeriesRows({ + s, + buckets, + shown, + title, +}: { + s: PivotSeries; + buckets: BucketMeta[]; + shown: { measure: string; label: string }[]; + title: string; +}) { + return ( + <> + {shown.map((row, ri) => ( + + {ri === 0 && ( + +
{title}
+ + )} + {row.label} + {buckets.map((b) => ( + + {fmt(s.buckets[b.name]?.[row.measure])} + + ))} + + ))} + + ); +} diff --git a/frontend/components/RecordTable.tsx b/frontend/components/RecordTable.tsx new file mode 100644 index 0000000000..94bd0a33ec --- /dev/null +++ b/frontend/components/RecordTable.tsx @@ -0,0 +1,247 @@ +"use client"; + +import { useMemo, useState } from "react"; +import { cellText, type Column, type RecordRow } from "@/lib/records"; + +// Editing hooks the table to a persistence layer. The page provides save/delete; +// the table owns the per-row edit/delete UI + optimistic state. +export type EditConfig = { + rowKey: string; // the field identifying a row (e.g. "reference") + canEdit?: (row: RecordRow) => boolean; // locked rows are read-only + onSave: (row: RecordRow, changes: Record) => Promise; + onDelete: (row: RecordRow) => Promise; +}; + +function pillClass(status: string): string { + const s = status.toLowerCase(); + if (s === "confirmed" || s === "approved") return "pill pill--ok"; + if (s === "completed" || s === "closed") return "pill pill--done"; + if (s === "proposed") return "pill pill--run"; + return "pill"; +} + +// A generic flat record table (problems/constraints, order summaries). Client-side +// filter; optional inline edit/delete when `edit` is supplied. Reuses the +// instrument-table design-system classes. +export default function RecordTable({ + columns, + records, + filterKeys, + edit, + emptyText = "NO RECORDS", +}: { + columns: Column[]; + records: RecordRow[]; + filterKeys?: string[]; + edit?: EditConfig; + emptyText?: string; +}) { + const [q, setQ] = useState(""); + const [editing, setEditing] = useState(null); + const [draft, setDraft] = useState>({}); + const [pending, setPending] = useState(null); + const [confirmDel, setConfirmDel] = useState(null); + const keys = filterKeys ?? columns.map((c) => c.key); + + const rows = useMemo(() => { + const needle = q.trim().toLowerCase(); + if (!needle) return records; + return records.filter((r) => + keys.some((k) => String(r[k] ?? "").toLowerCase().includes(needle)), + ); + }, [records, q, keys]); + + function startEdit(id: string) { + setConfirmDel(null); + setDraft({}); + setEditing(id); + } + function cancelEdit() { + setEditing(null); + setDraft({}); + } + async function save(row: RecordRow, id: string) { + if (!edit || !Object.keys(draft).length) return cancelEdit(); + setPending(id); + try { + await edit.onSave(row, draft); // page reloads on success -> fresh records + cancelEdit(); + } catch { + /* page surfaces the error; keep the row in edit mode to retry */ + } finally { + setPending(null); + } + } + async function del(row: RecordRow, id: string) { + if (!edit) return; + setPending(id); + try { + await edit.onDelete(row); + setConfirmDel(null); + } catch { + /* page surfaces the error; keep the confirm to retry */ + } finally { + setPending(null); + } + } + + function editCell(c: Column, row: RecordRow) { + const cur = draft[c.key] ?? row[c.key]; + const set = (v: unknown) => setDraft((d) => ({ ...d, [c.key]: v })); + if (c.edit === "select") { + return ( + + ); + } + return ( + set(e.target.value)} + aria-label={c.label} + /> + ); + } + + return ( + <> +
+ setQ(e.target.value)} + aria-label="Filter records" + /> + + {rows.length} + {rows.length !== records.length ? ` / ${records.length}` : ""} rows + +
+ + {!rows.length ? ( +
{q ? "NO MATCHES" : emptyText}
+ ) : ( +
+ + + + {columns.map((c) => ( + + ))} + {edit && + + + {rows.map((r, i) => { + const id = String(r[edit?.rowKey ?? "reference"] ?? r.id ?? i); + const isEditing = editing === id; + const editable = edit ? (edit.canEdit?.(r) ?? true) : false; + const busy = pending === id; + return ( + + {columns.map((c) => ( + + ))} + {edit && ( + + )} + + ); + })} + +
+ {c.label} + } +
+ {isEditing && c.edit ? ( + editCell(c, r) + ) : c.pill ? ( + + {cellText(c, r)} + + ) : ( + cellText(c, r) + )} + + {isEditing ? ( + <> + + + + ) : confirmDel === id ? ( + <> + Delete? + + + + ) : editable ? ( + <> + + + + ) : ( + + locked + + )} +
+
+ )} + + ); +} diff --git a/frontend/components/TabListScreen.tsx b/frontend/components/TabListScreen.tsx new file mode 100644 index 0000000000..b0ec0a8da9 --- /dev/null +++ b/frontend/components/TabListScreen.tsx @@ -0,0 +1,164 @@ +"use client"; + +import { useId, useState } from "react"; +import { isAuthError } from "@/lib/errors"; +import { loginUrl } from "@/lib/session"; +import { useRecordList } from "@/lib/useRecordList"; +import { useToast } from "@/components/Toast"; +import type { Column, RecordRow } from "@/lib/records"; +import RecordTable from "@/components/RecordTable"; + +// Inline-edit wiring for an editable list (orders): persist a change / a delete +// against the active tab's endpoint. The screen adds toast + reload around it. +export type EditableConfig = { + rowKey: string; + canEdit?: (row: RecordRow) => boolean; + save: (endpoint: string, row: RecordRow, changes: Record) => Promise; + remove: (endpoint: string, row: RecordRow) => Promise; +}; + +export type ListTab = { + key: string; + label: string; + endpoint: string; + // Per-tab columns (orders); falls back to the screen-level `columns` (problems). + columns?: Column[]; +}; + +// Shared chrome for the Phase 3 flat-list screens (Problems/Constraints, Orders): +// page header + a tabbed record table over one endpoint per tab. Owns the +// tab/tabpanel a11y wiring (roles, aria-controls, arrow-key nav) and the +// auth/loading/error states, so the screens stay thin config. +export default function TabListScreen({ + eyebrow, + title, + subtitle, + path, + tabs, + columns, + filterKeys, + editable, + emptyText, +}: { + eyebrow: string; + title: string; + subtitle: string; + path: string; // for the sign-in redirect + tabs: ListTab[]; + columns?: Column[]; // default columns when a tab doesn't carry its own + filterKeys?: string[]; + editable?: EditableConfig; // present => inline edit/delete + emptyText?: string; +}) { + const [tabKey, setTabKey] = useState(tabs[0].key); + const tab = tabs.find((t) => t.key === tabKey) ?? tabs[0]; + const cols = tab.columns ?? columns ?? []; + const baseId = useId(); + const toast = useToast(); + + const { records, loading, error, authError, reload } = useRecordList(tab.endpoint); + + // Wrap the page's persist functions with toast + reload. Rethrow so the table + // keeps the row open for a retry on failure. + const edit = editable + ? { + rowKey: editable.rowKey, + canEdit: editable.canEdit, + onSave: async (row: RecordRow, changes: Record) => { + try { + await editable.save(tab.endpoint, row, changes); + toast("ok", "Saved", `${row[editable.rowKey]} updated.`); + reload(); + } catch (e) { + errToast(e); + throw e; + } + }, + onDelete: async (row: RecordRow) => { + try { + await editable.remove(tab.endpoint, row); + toast("ok", "Deleted", `${row[editable.rowKey]} removed.`); + reload(); + } catch (e) { + errToast(e); + throw e; + } + }, + } + : undefined; + + function errToast(e: unknown) { + if (isAuthError(e)) toast("error", "Sign-in required", "Sign in to edit."); + else toast("error", "Save failed", e instanceof Error ? e.message : String(e)); + } + + // Arrow-key tab navigation (the ARIA tabs pattern). + function onKey(e: React.KeyboardEvent, i: number) { + if (e.key !== "ArrowRight" && e.key !== "ArrowLeft") return; + e.preventDefault(); + const next = (i + (e.key === "ArrowRight" ? 1 : tabs.length - 1)) % tabs.length; + setTabKey(tabs[next].key); + } + + return ( +
+
+
+
{eyebrow}
+

{title}

+

{subtitle}

+
+
+ + {authError && ( +
+ + + No active session. Sign in to load data. + +
+ )} + +
+ {tabs.map((t, i) => ( + + ))} +
+ +
+ {loading &&
LOADING…
} + {error && !authError && ( +
+ + Could not load: {error} +
+ )} + {!loading && !error && ( + + )} +
+
+ ); +} diff --git a/frontend/components/Toast.tsx b/frontend/components/Toast.tsx new file mode 100644 index 0000000000..3f11c89066 --- /dev/null +++ b/frontend/components/Toast.tsx @@ -0,0 +1,55 @@ +"use client"; + +import { + createContext, + useCallback, + useContext, + useRef, + useState, + type ReactNode, +} from "react"; + +type ToastKind = "info" | "ok" | "error"; +type Toast = { id: number; kind: ToastKind; title: string; body?: string }; + +type ToastApi = (kind: ToastKind, title: string, body?: string) => void; + +const ToastCtx = createContext(() => {}); + +// Minimal toast system: a provider that renders a fixed stack, plus useToast() +// for any client component to push a transient message. Replaces the app's +// previous silent failures (a click with no visible result). +export function ToastProvider({ children }: { children: ReactNode }) { + const [toasts, setToasts] = useState([]); + // Stable monotonic counter for unique keys, even for toasts pushed in the same + // millisecond (a useRef survives re-renders; a render-local `let` would not). + const seq = useRef(0); + + const push = useCallback((kind, title, body) => { + const id = Date.now() + seq.current++; + setToasts((t) => [...t, { id, kind, title, body }]); + setTimeout(() => { + setToasts((t) => t.filter((x) => x.id !== id)); + }, 5200); + }, []); + + return ( + + {children} +
+ {toasts.map((t) => ( +
+
+
{t.title}
+ {t.body &&
{t.body}
} +
+
+ ))} +
+
+ ); +} + +export function useToast(): ToastApi { + return useContext(ToastCtx); +} diff --git a/frontend/lib/api-types.ts b/frontend/lib/api-types.ts new file mode 100644 index 0000000000..f2c5697f35 --- /dev/null +++ b/frontend/lib/api-types.ts @@ -0,0 +1,12455 @@ +/** + * This file was auto-generated by openapi-typescript. + * Do not make direct changes to the file. + */ + +export interface paths { + "/api/common/attribute/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_attribute_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_attribute_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["common_attribute_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_attribute_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_attribute_partial_update"]; + trace?: never; + }; + "/api/common/attribute/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_attribute_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_attribute_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_attribute_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_attribute_partial_update_2"]; + trace?: never; + }; + "/api/common/bucket/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_bucket_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_bucket_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["common_bucket_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_bucket_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_bucket_partial_update"]; + trace?: never; + }; + "/api/common/bucket/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_bucket_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_bucket_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_bucket_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_bucket_partial_update_2"]; + trace?: never; + }; + "/api/common/bucketdetail/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_bucketdetail_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_bucketdetail_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["common_bucketdetail_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_bucketdetail_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_bucketdetail_partial_update"]; + trace?: never; + }; + "/api/common/bucketdetail/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_bucketdetail_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_bucketdetail_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_bucketdetail_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_bucketdetail_partial_update_2"]; + trace?: never; + }; + "/api/common/comment/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_comment_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_comment_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["common_comment_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_comment_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_comment_partial_update"]; + trace?: never; + }; + "/api/common/comment/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_comment_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_comment_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_comment_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_comment_partial_update_2"]; + trace?: never; + }; + "/api/common/parameter/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_parameter_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_parameter_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["common_parameter_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_parameter_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_parameter_partial_update"]; + trace?: never; + }; + "/api/common/parameter/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["common_parameter_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["common_parameter_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["common_parameter_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["common_parameter_partial_update_2"]; + trace?: never; + }; + "/api/forecast/forecast/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["forecast_forecast_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["forecast_forecast_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["forecast_forecast_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["forecast_forecast_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["forecast_forecast_partial_update"]; + trace?: never; + }; + "/api/forecast/forecast/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["forecast_forecast_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["forecast_forecast_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["forecast_forecast_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["forecast_forecast_partial_update_2"]; + trace?: never; + }; + "/api/forecast/forecastplan/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["forecast_forecastplan_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["forecast_forecastplan_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["forecast_forecastplan_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["forecast_forecastplan_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["forecast_forecastplan_partial_update"]; + trace?: never; + }; + "/api/forecast/measure/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["forecast_measure_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["forecast_measure_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["forecast_measure_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["forecast_measure_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["forecast_measure_partial_update"]; + trace?: never; + }; + "/api/forecast/measure/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["forecast_measure_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["forecast_measure_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["forecast_measure_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["forecast_measure_partial_update_2"]; + trace?: never; + }; + "/api/input/buffer/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_buffer_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_buffer_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_buffer_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_buffer_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_buffer_partial_update"]; + trace?: never; + }; + "/api/input/buffer/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_buffer_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_buffer_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_buffer_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_buffer_partial_update_2"]; + trace?: never; + }; + "/api/input/calendar/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_calendar_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_calendar_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_calendar_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_calendar_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_calendar_partial_update"]; + trace?: never; + }; + "/api/input/calendar/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_calendar_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_calendar_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_calendar_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_calendar_partial_update_2"]; + trace?: never; + }; + "/api/input/calendarbucket/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_calendarbucket_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_calendarbucket_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_calendarbucket_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_calendarbucket_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_calendarbucket_partial_update"]; + trace?: never; + }; + "/api/input/calendarbucket/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_calendarbucket_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_calendarbucket_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_calendarbucket_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_calendarbucket_partial_update_2"]; + trace?: never; + }; + "/api/input/customer/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_customer_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_customer_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_customer_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_customer_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_customer_partial_update"]; + trace?: never; + }; + "/api/input/customer/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_customer_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_customer_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_customer_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_customer_partial_update_2"]; + trace?: never; + }; + "/api/input/deliveryorder/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_deliveryorder_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_deliveryorder_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_deliveryorder_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_deliveryorder_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_deliveryorder_partial_update"]; + trace?: never; + }; + "/api/input/deliveryorder/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_deliveryorder_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_deliveryorder_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_deliveryorder_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_deliveryorder_partial_update_2"]; + trace?: never; + }; + "/api/input/demand/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_demand_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_demand_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_demand_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_demand_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_demand_partial_update"]; + trace?: never; + }; + "/api/input/demand/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_demand_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_demand_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_demand_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_demand_partial_update_2"]; + trace?: never; + }; + "/api/input/distributionorder/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_distributionorder_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_distributionorder_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_distributionorder_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_distributionorder_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_distributionorder_partial_update"]; + trace?: never; + }; + "/api/input/distributionorder/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_distributionorder_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_distributionorder_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_distributionorder_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_distributionorder_partial_update_2"]; + trace?: never; + }; + "/api/input/item/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_item_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_item_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_item_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_item_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_item_partial_update"]; + trace?: never; + }; + "/api/input/item/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_item_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_item_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_item_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_item_partial_update_2"]; + trace?: never; + }; + "/api/input/itemdistribution/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_itemdistribution_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_itemdistribution_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_itemdistribution_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_itemdistribution_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_itemdistribution_partial_update"]; + trace?: never; + }; + "/api/input/itemdistribution/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_itemdistribution_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_itemdistribution_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_itemdistribution_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_itemdistribution_partial_update_2"]; + trace?: never; + }; + "/api/input/itemsupplier/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_itemsupplier_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_itemsupplier_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_itemsupplier_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_itemsupplier_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_itemsupplier_partial_update"]; + trace?: never; + }; + "/api/input/itemsupplier/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_itemsupplier_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_itemsupplier_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_itemsupplier_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_itemsupplier_partial_update_2"]; + trace?: never; + }; + "/api/input/location/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_location_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_location_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_location_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_location_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_location_partial_update"]; + trace?: never; + }; + "/api/input/location/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_location_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_location_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_location_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_location_partial_update_2"]; + trace?: never; + }; + "/api/input/manufacturingorder/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_manufacturingorder_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_manufacturingorder_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_manufacturingorder_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_manufacturingorder_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_manufacturingorder_partial_update"]; + trace?: never; + }; + "/api/input/manufacturingorder/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_manufacturingorder_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_manufacturingorder_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_manufacturingorder_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_manufacturingorder_partial_update_2"]; + trace?: never; + }; + "/api/input/operation/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operation_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operation_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_operation_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operation_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operation_partial_update"]; + trace?: never; + }; + "/api/input/operation/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operation_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operation_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operation_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operation_partial_update_2"]; + trace?: never; + }; + "/api/input/operationdependency/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationdependency_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationdependency_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_operationdependency_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationdependency_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationdependency_partial_update"]; + trace?: never; + }; + "/api/input/operationdependency/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationdependency_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationdependency_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationdependency_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationdependency_partial_update_2"]; + trace?: never; + }; + "/api/input/operationmaterial/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationmaterial_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationmaterial_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_operationmaterial_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationmaterial_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationmaterial_partial_update"]; + trace?: never; + }; + "/api/input/operationmaterial/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationmaterial_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationmaterial_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationmaterial_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationmaterial_partial_update_2"]; + trace?: never; + }; + "/api/input/operationplanmaterial/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationplanmaterial_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationplanmaterial_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_operationplanmaterial_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationplanmaterial_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationplanmaterial_partial_update"]; + trace?: never; + }; + "/api/input/operationplanmaterial/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationplanmaterial_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationplanmaterial_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationplanmaterial_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationplanmaterial_partial_update_2"]; + trace?: never; + }; + "/api/input/operationplanresource/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationplanresource_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationplanresource_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_operationplanresource_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationplanresource_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationplanresource_partial_update"]; + trace?: never; + }; + "/api/input/operationplanresource/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationplanresource_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationplanresource_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationplanresource_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationplanresource_partial_update_2"]; + trace?: never; + }; + "/api/input/operationresource/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationresource_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationresource_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_operationresource_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationresource_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationresource_partial_update"]; + trace?: never; + }; + "/api/input/operationresource/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_operationresource_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_operationresource_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_operationresource_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_operationresource_partial_update_2"]; + trace?: never; + }; + "/api/input/purchaseorder/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_purchaseorder_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_purchaseorder_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_purchaseorder_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_purchaseorder_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_purchaseorder_partial_update"]; + trace?: never; + }; + "/api/input/purchaseorder/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_purchaseorder_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_purchaseorder_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_purchaseorder_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_purchaseorder_partial_update_2"]; + trace?: never; + }; + "/api/input/resource/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_resource_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_resource_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_resource_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_resource_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_resource_partial_update"]; + trace?: never; + }; + "/api/input/resource/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_resource_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_resource_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_resource_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_resource_partial_update_2"]; + trace?: never; + }; + "/api/input/resourceskill/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_resourceskill_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_resourceskill_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_resourceskill_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_resourceskill_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_resourceskill_partial_update"]; + trace?: never; + }; + "/api/input/resourceskill/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_resourceskill_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_resourceskill_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_resourceskill_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_resourceskill_partial_update_2"]; + trace?: never; + }; + "/api/input/setupmatrix/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_setupmatrix_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_setupmatrix_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_setupmatrix_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_setupmatrix_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_setupmatrix_partial_update"]; + trace?: never; + }; + "/api/input/setupmatrix/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_setupmatrix_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_setupmatrix_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_setupmatrix_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_setupmatrix_partial_update_2"]; + trace?: never; + }; + "/api/input/setuprule/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_setuprule_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_setuprule_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_setuprule_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_setuprule_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_setuprule_partial_update"]; + trace?: never; + }; + "/api/input/setuprule/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_setuprule_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_setuprule_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_setuprule_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_setuprule_partial_update_2"]; + trace?: never; + }; + "/api/input/skill/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_skill_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_skill_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_skill_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_skill_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_skill_partial_update"]; + trace?: never; + }; + "/api/input/skill/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_skill_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_skill_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_skill_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_skill_partial_update_2"]; + trace?: never; + }; + "/api/input/suboperation/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_suboperation_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_suboperation_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_suboperation_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_suboperation_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_suboperation_partial_update"]; + trace?: never; + }; + "/api/input/suboperation/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_suboperation_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_suboperation_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_suboperation_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_suboperation_partial_update_2"]; + trace?: never; + }; + "/api/input/supplier/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_supplier_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_supplier_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_supplier_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_supplier_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_supplier_partial_update"]; + trace?: never; + }; + "/api/input/supplier/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_supplier_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_supplier_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_supplier_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_supplier_partial_update_2"]; + trace?: never; + }; + "/api/input/workorder/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_workorder_list"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_workorder_update"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + post: operations["input_workorder_create"]; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_workorder_destroy"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework.: + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_workorder_partial_update"]; + trace?: never; + }; + "/api/input/workorder/{id}/": { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + get: operations["input_workorder_retrieve"]; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + put: operations["input_workorder_update_2"]; + post?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + delete: operations["input_workorder_destroy_2"]; + options?: never; + head?: never; + /** @description Customized API view for the REST framework. + * - support for request-specific scenario database + * - add 'title' to the context of the html view */ + patch: operations["input_workorder_partial_update_2"]; + trace?: never; + }; +} +export type webhooks = Record; +export interface components { + schemas: { + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Attribute: { + model?: components["schemas"]["ModelEnum"]; + name?: string; + label: string; + editable?: boolean; + initially_hidden?: boolean; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @enum {unknown} */ + BlankEnum: ""; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Bucket: { + name?: string; + description?: string | null; + /** @description Higher values indicate more granular time buckets */ + level: number; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + BucketDetail: { + bucket?: string; + name: string; + /** + * Start date + * Format: date-time + */ + startdate?: string; + /** + * End date + * Format: date-time + */ + enddate: string; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Buffer: { + /** Identifier */ + readonly id: number; + description?: string | null; + category?: string | null; + subcategory?: string | null; + type?: (components["schemas"]["BufferTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Unique identifier */ + location?: string; + /** @description Unique identifier */ + item?: string; + /** @default */ + batch: string | null; + /** + * Format: decimal + * @description current inventory + */ + onhand?: string | null; + /** + * Format: decimal + * @description safety stock + */ + minimum?: string | null; + /** @description Calendar storing a time-dependent safety stock profile */ + minimum_calendar?: string | null; + /** + * Format: decimal + * @description maximum stock + */ + maximum?: string | null; + /** @description Calendar storing a time-dependent maximum stock profile */ + maximum_calendar?: string | null; + /** @description Batching window for grouping replenishments in batches */ + min_interval?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** + * @description * `default` - default + * * `infinite` - infinite + * @enum {string} + */ + BufferTypeEnum: "default" | "infinite"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Calendar: { + name?: string; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** + * Default value + * Format: decimal + * @description Value to be used when no entry is effective + */ + defaultvalue?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + CalendarBucket: { + /** Identifier */ + readonly id: number; + calendar?: string; + /** + * Start date + * Format: date-time + * @default 1971-01-01T00:00:00 + */ + startdate: string; + /** + * End date + * Format: date-time + * @default 2030-12-31T00:00:00 + */ + enddate: string; + /** Format: decimal */ + value?: string; + /** @default 0 */ + priority: number | null; + monday?: boolean; + tuesday?: boolean; + wednesday?: boolean; + thursday?: boolean; + friday?: boolean; + saturday?: boolean; + sunday?: boolean; + /** + * Start time + * Format: time + */ + starttime?: string | null; + /** + * End time + * Format: time + */ + endtime?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Comment: { + /** Identifier */ + readonly id: number; + /** Object id */ + object_pk: string; + /** Message */ + comment: string; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + content_type: number; + readonly user: number | null; + type?: components["schemas"]["CommentTypeEnum"]; + }; + /** + * @description * `add` - Add + * * `change` - Change + * * `delete` - Delete + * * `comment` - comment + * * `follower` - follower + * @enum {string} + */ + CommentTypeEnum: "add" | "change" | "delete" | "comment" | "follower"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Customer: { + /** @description Unique identifier */ + name?: string; + /** @description Hierarchical parent */ + owner?: string | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + DeliveryOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Unique identifier */ + demand?: string | null; + /** @description Unique identifier */ + item?: string | null; + /** @description Unique identifier */ + location?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** + * Format: date-time + * @description Due date of the demand/forecast + */ + readonly due: string | null; + /** @description MTO batch name */ + batch?: string | null; + readonly delay: string | null; + readonly plan: unknown; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + readonly forecast: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Demand: { + /** @description Unique identifier */ + name?: string; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description Unique identifier */ + item: string; + /** @description Unique identifier */ + customer: string; + /** @description Unique identifier */ + location: string; + /** + * Format: date-time + * @description Due date of the sales order + */ + due: string; + /** @description Status of the demand. Only "open" and "quote" demands are planned + * + * * `inquiry` - inquiry + * * `quote` - quote + * * `open` - open + * * `closed` - closed + * * `canceled` - canceled */ + status?: (components["schemas"]["DemandStatusEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * Delivery operation + * @description Operation used to satisfy this demand + */ + operation?: string | null; + /** Format: decimal */ + quantity: string; + /** @description Priority of the demand (lower numbers indicate more important demands) */ + priority?: number; + /** @description MTO batch name */ + batch?: string | null; + readonly delay: string | null; + /** + * Planned quantity + * Format: decimal + * @description Quantity planned for delivery + */ + readonly plannedquantity: string | null; + /** + * Delivery date + * Format: date-time + * @description Delivery date of the demand + */ + readonly deliverydate: string | null; + readonly plan: unknown; + /** + * Minimum shipment + * Format: decimal + * @description Minimum shipment quantity when planning this demand + */ + minshipment?: string | null; + /** + * Maximum lateness + * @description Maximum lateness allowed when planning this demand + */ + maxlateness?: string | null; + owner?: string | null; + /** @description Defines how sales orders are shipped together + * + * * `independent` - independent + * * `alltogether` - all together + * * `inratio` - in ratio */ + policy?: (components["schemas"]["PolicyEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + }; + /** + * @description * `inquiry` - inquiry + * * `quote` - quote + * * `open` - open + * * `closed` - closed + * * `canceled` - canceled + * @enum {string} + */ + DemandStatusEnum: "inquiry" | "quote" | "open" | "closed" | "canceled"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + DistributionOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Unique identifier */ + item?: string | null; + /** @description Unique identifier */ + origin?: string | null; + /** @description Unique identifier */ + destination?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** Format: decimal */ + readonly criticality: string | null; + readonly delay: string | null; + readonly plan: unknown; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + readonly forecast: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Forecast: { + name?: string; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** @description Unique identifier */ + customer: string; + /** @description Unique identifier */ + item: string; + /** @description Unique identifier */ + location: string; + /** + * Delivery operation + * @description Operation used to satisfy this demand + */ + operation?: string | null; + /** + * Forecast method + * @description Method used to generate a base forecast + * + * * `automatic` - Automatic + * * `constant` - Constant + * * `trend` - Trend + * * `seasonal` - Seasonal + * * `intermittent` - Intermittent + * * `moving average` - Moving average + * * `manual` - Manual + * * `aggregate` - Aggregate + */ + method?: (components["schemas"]["MethodEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Priority of the demand (lower numbers indicate more important demands) */ + priority?: number; + /** + * Minimum shipment + * Format: decimal + * @description Minimum shipment quantity when planning this demand + */ + minshipment?: string | null; + /** + * Maximum lateness + * @description Maximum lateness allowed when planning this demand + */ + maxlateness?: string | null; + /** @description Round forecast numbers to integers */ + discrete?: boolean; + /** + * Estimated forecast error + * Format: decimal + */ + out_smape?: string | null; + /** Calculated forecast method */ + out_method?: string | null; + /** + * Calculated standard deviation + * Format: decimal + */ + out_deviation?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Item: { + /** @description Unique identifier */ + name?: string; + /** @description Hierarchical parent */ + owner?: string | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** + * Format: decimal + * @description Cost of the item + */ + cost?: string | null; + /** + * Format: decimal + * @description Volume of the item + */ + volume?: string | null; + /** + * Format: decimal + * @description Weight of the item + */ + weight?: string | null; + /** + * Period of cover + * @description Period of cover in days + */ + readonly periodofcover: number | null; + /** Unit of measure */ + uom?: string | null; + type?: (components["schemas"]["ItemTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + /** Count of late demands */ + readonly latedemandcount: number | null; + /** + * Quantity of late demands + * Format: decimal + */ + readonly latedemandquantity: string | null; + /** + * Value of late demand + * Format: decimal + */ + readonly latedemandvalue: string | null; + /** Count of unplanned demands */ + readonly unplanneddemandcount: number | null; + /** + * Quantity of unplanned demands + * Format: decimal + */ + readonly unplanneddemandquantity: string | null; + /** + * Value of unplanned demands + * Format: decimal + */ + readonly unplanneddemandvalue: string | null; + readonly demand_pattern: string | null; + /** Format: decimal */ + readonly adi: string | null; + /** Format: decimal */ + readonly cv2: string | null; + /** + * Outliers last bucket + * Format: decimal + */ + readonly outlier_1b: string | null; + /** + * Outliers last 6 buckets + * Format: decimal + */ + readonly outlier_6b: string | null; + /** + * Outliers last 12 buckets + * Format: decimal + */ + readonly outlier_12b: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + ItemDistribution: { + /** Identifier */ + readonly id: number; + /** @description Unique identifier */ + item?: string; + /** @description Destination location to be replenished */ + location?: string; + /** @description Source location shipping the item */ + origin?: string; + /** + * Lead time + * @description Transport lead time + */ + leadtime?: string | null; + /** + * Size minimum + * Format: decimal + * @description A minimum shipping quantity + */ + sizeminimum?: string | null; + /** + * Size multiple + * Format: decimal + * @description A multiple shipping quantity + */ + sizemultiple?: string | null; + /** + * Size maximum + * Format: decimal + * @description A maximum shipping quantity + */ + sizemaximum?: string | null; + /** + * Batching window + * @description Proposed distribution orders within this window will be grouped together + */ + batchwindow?: string | null; + /** + * Format: decimal + * @description Shipping cost per unit + */ + cost?: string | null; + /** @description Priority among all alternates */ + priority?: number | null; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + ItemSupplier: { + /** Identifier */ + readonly id: number; + /** @description Unique identifier */ + item?: string; + /** @description Unique identifier */ + location?: string | null; + /** @description Unique identifier */ + supplier?: string; + /** + * Lead time + * @description Purchasing lead time + */ + leadtime?: string | null; + /** + * Size minimum + * Format: decimal + * @description A minimum purchasing quantity + */ + sizeminimum?: string | null; + /** + * Size multiple + * Format: decimal + * @description A multiple purchasing quantity + */ + sizemultiple?: string | null; + /** + * Size maximum + * Format: decimal + * @description A maximum purchasing quantity + */ + sizemaximum?: string | null; + /** + * Batching window + * @description Proposed purchase orders within this window will be grouped together + */ + batchwindow?: string | null; + /** + * Format: decimal + * @description Purchasing cost per unit + */ + cost?: string | null; + /** @description Priority among all alternates */ + priority?: number | null; + /** + * Hard safety lead time + * @description hard safety lead time + */ + hard_safety_leadtime?: string | null; + /** + * Soft safety lead time + * @description soft safety lead time + */ + extra_safety_leadtime?: string | null; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** + * @description * `make to stock` - make to stock + * * `make to order` - make to order + * @enum {string} + */ + ItemTypeEnum: "make to stock" | "make to order"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Location: { + /** @description Unique identifier */ + name?: string; + /** @description Hierarchical parent */ + owner?: string | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description Calendar defining the working hours and holidays */ + available?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + ManufacturingOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + operation?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Completed quantity + * Format: decimal + */ + quantity_completed?: string | null; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** Format: decimal */ + readonly criticality: string | null; + readonly delay: string | null; + readonly plan: unknown; + /** @description Hierarchical parent */ + owner?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + resources?: components["schemas"]["OperationPlanResourceNested"][]; + materials?: components["schemas"]["OperationPlanMaterialNested"][]; + /** @description Unique identifier */ + demand?: string | null; + readonly forecast: string | null; + }; + /** + * @description * `aggregate` - aggregate + * * `local` - local + * * `computed` - computed + * @enum {string} + */ + MeasureTypeEnum: "aggregate" | "local" | "computed"; + /** + * @description * `automatic` - Automatic + * * `constant` - Constant + * * `trend` - Trend + * * `seasonal` - Seasonal + * * `intermittent` - Intermittent + * * `moving average` - Moving average + * * `manual` - Manual + * * `aggregate` - Aggregate + * @enum {string} + */ + MethodEnum: "automatic" | "constant" | "trend" | "seasonal" | "intermittent" | "moving average" | "manual" | "aggregate"; + /** + * @description * `edit` - edit + * * `view` - view + * * `hide` - hide + * @enum {string} + */ + ModeFutureEnum: "edit" | "view" | "hide"; + /** + * @description * `edit` - edit + * * `view` - view + * * `hide` - hide + * @enum {string} + */ + ModePastEnum: "edit" | "view" | "hide"; + /** + * @description * `calendar` - calendar + * * `supplier` - supplier + * * `item` - item + * * `location` - location + * * `customer` - customer + * * `demand` - demand + * * `operation` - operation + * * `operationplan` - operationplan + * * `operationplanmaterial` - operationplanmaterial + * * `setupmatrix` - setupmatrix + * * `skill` - skill + * * `resource` - resource + * * `operationplanresource` - operationplanresource + * * `suboperation` - suboperation + * * `setuprule` - setuprule + * * `resourceskill` - resourceskill + * * `operationresource` - operationresource + * * `operationmaterial` - operationmaterial + * * `itemsupplier` - itemsupplier + * * `itemdistribution` - itemdistribution + * * `calendarbucket` - calendarbucket + * * `buffer` - buffer + * * `operationdependency` - operationdependency + * * `workorder` - workorder + * * `forecast` - forecast + * * `constraint` - constraint + * * `problem` - problem + * * `user` - user + * * `bucket` - bucket + * * `bucketdetail` - bucketdetail + * * `apikey` - apikey + * @enum {string} + */ + ModelEnum: "calendar" | "supplier" | "item" | "location" | "customer" | "demand" | "operation" | "operationplan" | "operationplanmaterial" | "setupmatrix" | "skill" | "resource" | "operationplanresource" | "suboperation" | "setuprule" | "resourceskill" | "operationresource" | "operationmaterial" | "itemsupplier" | "itemdistribution" | "calendarbucket" | "buffer" | "operationdependency" | "workorder" | "forecast" | "constraint" | "problem" | "user" | "bucket" | "bucketdetail" | "apikey"; + /** @enum {unknown} */ + NullEnum: null; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Operation: { + name?: string; + type?: (components["schemas"]["OperationTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description Item produced by this operation */ + item?: string | null; + /** @description Unique identifier */ + location: string; + /** + * Release fence + * @description Operationplans within this time window from the current day are expected to be released to production ERP + */ + fence?: string | null; + /** + * Batching window + * @description The solver algorithm will scan for opportunities to create batches within this time window before and after the requirement date + */ + batchwindow?: string | null; + /** + * Post-op time + * @description A delay time to be respected as a soft constraint after ending the operation + */ + posttime?: string | null; + /** + * Size minimum + * Format: decimal + * @description Minimum production quantity + */ + sizeminimum?: string | null; + /** + * Size multiple + * Format: decimal + * @description Multiple production quantity + */ + sizemultiple?: string | null; + /** + * Size maximum + * Format: decimal + * @description Maximum production quantity + */ + sizemaximum?: string | null; + /** @description Parent operation (which must be of type routing, alternate or split) */ + owner?: string | null; + /** @description Priority among all alternates */ + priority?: number | null; + /** + * Format: date-time + * @description Validity start date + */ + effective_start?: string | null; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + /** @description Calendar defining the working hours and holidays */ + available?: string | null; + /** + * Format: decimal + * @description Cost per produced unit + */ + cost?: string | null; + /** @description Fixed production time for setup and overhead */ + duration?: string | null; + /** + * Duration per unit + * @description Production time per produced piece + */ + duration_per?: string | null; + /** + * Search mode + * @description Method to select preferred alternate + * + * * `PRIORITY` - priority + * * `MINCOST` - minimum cost + * * `MINPENALTY` - minimum penalty + * * `MINCOSTPENALTY` - minimum cost plus penalty + */ + search?: (components["schemas"]["SearchEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + OperationDependency: { + /** Identifier */ + readonly id: number; + /** @description operation */ + operation?: string; + /** + * Blocked by operation + * @description blocked by operation + */ + blockedby?: string; + /** + * Format: decimal + * @description Quantity relation between the operations + */ + quantity?: string | null; + /** + * Soft safety lead time + * @description soft safety lead time + */ + safety_leadtime?: string | null; + /** + * Hard safety lead time + * @description hard safety lead time + */ + hard_safety_leadtime?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + OperationMaterial: { + /** Identifier */ + readonly id: number; + operation?: string; + /** @description Unique identifier */ + item?: string; + /** @description Unique identifier */ + location?: string | null; + /** + * Format: decimal + * @description Quantity to consume or produce per piece + */ + quantity?: string | null; + /** + * Fixed quantity + * Format: decimal + * @description Fixed quantity to consume or produce + */ + quantity_fixed?: string | null; + /** + * Transfer batch quantity + * Format: decimal + * @description Batch size by in which material is produced or consumed + */ + transferbatch?: string | null; + /** @description Time offset from the start or end to consume or produce material */ + offset?: string | null; + /** @description Consume/produce material at the start or the end of the operationplan + * + * * `start` - Start + * * `end` - End + * * `transfer_batch` - Batch transfer */ + type?: (components["schemas"]["OperationMaterialTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + /** @description Name of this operation material to identify alternates */ + name?: string | null; + /** @description Priority of this operation material in a group of alternates */ + priority?: number | null; + /** + * Search mode + * @description Method to select preferred alternate + * + * * `PRIORITY` - priority + * * `MINCOST` - minimum cost + * * `MINPENALTY` - minimum penalty + * * `MINCOSTPENALTY` - minimum cost plus penalty + */ + search?: (components["schemas"]["SearchEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** + * @description * `start` - Start + * * `end` - End + * * `transfer_batch` - Batch transfer + * @enum {string} + */ + OperationMaterialTypeEnum: "start" | "end" | "transfer_batch"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + OperationPlanMaterial: { + /** Identifier */ + readonly id: number; + /** + * Reference + * @description Unique identifier + */ + operationplan: string; + /** @description Unique identifier */ + item: string; + /** @description Unique identifier */ + location: string; + /** Format: decimal */ + quantity: string; + /** Format: decimal */ + onhand?: string | null; + /** + * Date + * Format: date-time + */ + flowdate: string; + /** + * Material status + * @description status of the material production or consumption + * + * * `proposed` - proposed + * * `confirmed` - confirmed + * * `closed` - closed + */ + status?: (components["schemas"]["Status9c1Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + OperationPlanMaterialNested: { + /** @description Unique identifier */ + item: string; + /** Format: decimal */ + quantity: string; + /** + * Date + * Format: date-time + */ + flowdate: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + OperationPlanResource: { + /** Identifier */ + readonly id: number; + /** + * Reference + * @description Unique identifier + */ + operationplan?: string; + /** @description Unique identifier */ + resource?: string; + /** Format: decimal */ + quantity?: string | null; + readonly startdate: string; + readonly enddate: string; + /** + * Load status + * @description Status of the resource assignment + * + * * `proposed` - proposed + * * `confirmed` - confirmed + * * `closed` - closed + */ + status?: (components["schemas"]["Status9c1Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + setup?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + OperationPlanResourceNested: { + /** @description Unique identifier */ + resource?: string; + /** Format: decimal */ + quantity?: string | null; + setup?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + OperationResource: { + /** Identifier */ + readonly id: number; + operation?: string; + /** @description Unique identifier */ + resource?: string; + /** @description Required skill to perform the operation */ + skill?: string | null; + /** + * Format: decimal + * @description Required quantity of the resource + */ + quantity?: string | null; + /** + * Format: decimal + * @description Constant part of the capacity consumption (bucketized resources only) + */ + quantity_fixed?: string | null; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + /** @description Name of this operation resource to identify alternates */ + name?: string | null; + /** @description Priority of this operation resource in a group of alternates */ + priority?: number | null; + /** @description Setup required on the resource for this operation */ + setup?: string | null; + /** + * Search mode + * @description Method to select preferred alternate + * + * * `PRIORITY` - priority + * * `MINCOST` - minimum cost + * * `MINPENALTY` - minimum penalty + * * `MINCOSTPENALTY` - minimum cost plus penalty + */ + search?: (components["schemas"]["SearchEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** + * @description * `fixed_time` - fixed_time + * * `time_per` - time_per + * * `routing` - routing + * * `alternate` - alternate + * * `split` - split + * @enum {string} + */ + OperationTypeEnum: "fixed_time" | "time_per" | "routing" | "alternate" | "split"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Parameter: { + name?: string; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + value?: string | null; + description?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedAttribute: { + model?: components["schemas"]["ModelEnum"]; + name?: string; + label?: string; + editable?: boolean; + initially_hidden?: boolean; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedBucket: { + name?: string; + description?: string | null; + /** @description Higher values indicate more granular time buckets */ + level?: number; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedBucketDetail: { + bucket?: string; + name?: string; + /** + * Start date + * Format: date-time + */ + startdate?: string; + /** + * End date + * Format: date-time + */ + enddate?: string; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedBuffer: { + /** Identifier */ + readonly id?: number; + description?: string | null; + category?: string | null; + subcategory?: string | null; + type?: (components["schemas"]["BufferTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Unique identifier */ + location?: string; + /** @description Unique identifier */ + item?: string; + /** @default */ + batch: string | null; + /** + * Format: decimal + * @description current inventory + */ + onhand?: string | null; + /** + * Format: decimal + * @description safety stock + */ + minimum?: string | null; + /** @description Calendar storing a time-dependent safety stock profile */ + minimum_calendar?: string | null; + /** + * Format: decimal + * @description maximum stock + */ + maximum?: string | null; + /** @description Calendar storing a time-dependent maximum stock profile */ + maximum_calendar?: string | null; + /** @description Batching window for grouping replenishments in batches */ + min_interval?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedCalendar: { + name?: string; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** + * Default value + * Format: decimal + * @description Value to be used when no entry is effective + */ + defaultvalue?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedCalendarBucket: { + /** Identifier */ + readonly id?: number; + calendar?: string; + /** + * Start date + * Format: date-time + * @default 1971-01-01T00:00:00 + */ + startdate: string; + /** + * End date + * Format: date-time + * @default 2030-12-31T00:00:00 + */ + enddate: string; + /** Format: decimal */ + value?: string; + /** @default 0 */ + priority: number | null; + monday?: boolean; + tuesday?: boolean; + wednesday?: boolean; + thursday?: boolean; + friday?: boolean; + saturday?: boolean; + sunday?: boolean; + /** + * Start time + * Format: time + */ + starttime?: string | null; + /** + * End time + * Format: time + */ + endtime?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedComment: { + /** Identifier */ + readonly id?: number; + /** Object id */ + object_pk?: string; + /** Message */ + comment?: string; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + content_type?: number; + readonly user?: number | null; + type?: components["schemas"]["CommentTypeEnum"]; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedCustomer: { + /** @description Unique identifier */ + name?: string; + /** @description Hierarchical parent */ + owner?: string | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedDeliveryOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Unique identifier */ + demand?: string | null; + /** @description Unique identifier */ + item?: string | null; + /** @description Unique identifier */ + location?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** + * Format: date-time + * @description Due date of the demand/forecast + */ + readonly due?: string | null; + /** @description MTO batch name */ + batch?: string | null; + readonly delay?: string | null; + readonly plan?: unknown; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + readonly forecast?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedDemand: { + /** @description Unique identifier */ + name?: string; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description Unique identifier */ + item?: string; + /** @description Unique identifier */ + customer?: string; + /** @description Unique identifier */ + location?: string; + /** + * Format: date-time + * @description Due date of the sales order + */ + due?: string; + /** @description Status of the demand. Only "open" and "quote" demands are planned + * + * * `inquiry` - inquiry + * * `quote` - quote + * * `open` - open + * * `closed` - closed + * * `canceled` - canceled */ + status?: (components["schemas"]["DemandStatusEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * Delivery operation + * @description Operation used to satisfy this demand + */ + operation?: string | null; + /** Format: decimal */ + quantity?: string; + /** @description Priority of the demand (lower numbers indicate more important demands) */ + priority?: number; + /** @description MTO batch name */ + batch?: string | null; + readonly delay?: string | null; + /** + * Planned quantity + * Format: decimal + * @description Quantity planned for delivery + */ + readonly plannedquantity?: string | null; + /** + * Delivery date + * Format: date-time + * @description Delivery date of the demand + */ + readonly deliverydate?: string | null; + readonly plan?: unknown; + /** + * Minimum shipment + * Format: decimal + * @description Minimum shipment quantity when planning this demand + */ + minshipment?: string | null; + /** + * Maximum lateness + * @description Maximum lateness allowed when planning this demand + */ + maxlateness?: string | null; + owner?: string | null; + /** @description Defines how sales orders are shipped together + * + * * `independent` - independent + * * `alltogether` - all together + * * `inratio` - in ratio */ + policy?: (components["schemas"]["PolicyEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedDistributionOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Unique identifier */ + item?: string | null; + /** @description Unique identifier */ + origin?: string | null; + /** @description Unique identifier */ + destination?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** Format: decimal */ + readonly criticality?: string | null; + readonly delay?: string | null; + readonly plan?: unknown; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + readonly forecast?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedForecast: { + name?: string; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** @description Unique identifier */ + customer?: string; + /** @description Unique identifier */ + item?: string; + /** @description Unique identifier */ + location?: string; + /** + * Delivery operation + * @description Operation used to satisfy this demand + */ + operation?: string | null; + /** + * Forecast method + * @description Method used to generate a base forecast + * + * * `automatic` - Automatic + * * `constant` - Constant + * * `trend` - Trend + * * `seasonal` - Seasonal + * * `intermittent` - Intermittent + * * `moving average` - Moving average + * * `manual` - Manual + * * `aggregate` - Aggregate + */ + method?: (components["schemas"]["MethodEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Priority of the demand (lower numbers indicate more important demands) */ + priority?: number; + /** + * Minimum shipment + * Format: decimal + * @description Minimum shipment quantity when planning this demand + */ + minshipment?: string | null; + /** + * Maximum lateness + * @description Maximum lateness allowed when planning this demand + */ + maxlateness?: string | null; + /** @description Round forecast numbers to integers */ + discrete?: boolean; + /** + * Estimated forecast error + * Format: decimal + */ + out_smape?: string | null; + /** Calculated forecast method */ + out_method?: string | null; + /** + * Calculated standard deviation + * Format: decimal + */ + out_deviation?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedForecastPlan: { + /** @description Unique identifier */ + item?: string; + /** @description Unique identifier */ + location?: string; + /** @description Unique identifier */ + customer?: string; + /** + * Start date + * Format: date-time + */ + startdate?: string; + /** + * End date + * Format: date-time + */ + enddate?: string; + value?: unknown; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedItem: { + /** @description Unique identifier */ + name?: string; + /** @description Hierarchical parent */ + owner?: string | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** + * Format: decimal + * @description Cost of the item + */ + cost?: string | null; + /** + * Format: decimal + * @description Volume of the item + */ + volume?: string | null; + /** + * Format: decimal + * @description Weight of the item + */ + weight?: string | null; + /** + * Period of cover + * @description Period of cover in days + */ + readonly periodofcover?: number | null; + /** Unit of measure */ + uom?: string | null; + type?: (components["schemas"]["ItemTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + /** Count of late demands */ + readonly latedemandcount?: number | null; + /** + * Quantity of late demands + * Format: decimal + */ + readonly latedemandquantity?: string | null; + /** + * Value of late demand + * Format: decimal + */ + readonly latedemandvalue?: string | null; + /** Count of unplanned demands */ + readonly unplanneddemandcount?: number | null; + /** + * Quantity of unplanned demands + * Format: decimal + */ + readonly unplanneddemandquantity?: string | null; + /** + * Value of unplanned demands + * Format: decimal + */ + readonly unplanneddemandvalue?: string | null; + readonly demand_pattern?: string | null; + /** Format: decimal */ + readonly adi?: string | null; + /** Format: decimal */ + readonly cv2?: string | null; + /** + * Outliers last bucket + * Format: decimal + */ + readonly outlier_1b?: string | null; + /** + * Outliers last 6 buckets + * Format: decimal + */ + readonly outlier_6b?: string | null; + /** + * Outliers last 12 buckets + * Format: decimal + */ + readonly outlier_12b?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedItemDistribution: { + /** Identifier */ + readonly id?: number; + /** @description Unique identifier */ + item?: string; + /** @description Destination location to be replenished */ + location?: string; + /** @description Source location shipping the item */ + origin?: string; + /** + * Lead time + * @description Transport lead time + */ + leadtime?: string | null; + /** + * Size minimum + * Format: decimal + * @description A minimum shipping quantity + */ + sizeminimum?: string | null; + /** + * Size multiple + * Format: decimal + * @description A multiple shipping quantity + */ + sizemultiple?: string | null; + /** + * Size maximum + * Format: decimal + * @description A maximum shipping quantity + */ + sizemaximum?: string | null; + /** + * Batching window + * @description Proposed distribution orders within this window will be grouped together + */ + batchwindow?: string | null; + /** + * Format: decimal + * @description Shipping cost per unit + */ + cost?: string | null; + /** @description Priority among all alternates */ + priority?: number | null; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedItemSupplier: { + /** Identifier */ + readonly id?: number; + /** @description Unique identifier */ + item?: string; + /** @description Unique identifier */ + location?: string | null; + /** @description Unique identifier */ + supplier?: string; + /** + * Lead time + * @description Purchasing lead time + */ + leadtime?: string | null; + /** + * Size minimum + * Format: decimal + * @description A minimum purchasing quantity + */ + sizeminimum?: string | null; + /** + * Size multiple + * Format: decimal + * @description A multiple purchasing quantity + */ + sizemultiple?: string | null; + /** + * Size maximum + * Format: decimal + * @description A maximum purchasing quantity + */ + sizemaximum?: string | null; + /** + * Batching window + * @description Proposed purchase orders within this window will be grouped together + */ + batchwindow?: string | null; + /** + * Format: decimal + * @description Purchasing cost per unit + */ + cost?: string | null; + /** @description Priority among all alternates */ + priority?: number | null; + /** + * Hard safety lead time + * @description hard safety lead time + */ + hard_safety_leadtime?: string | null; + /** + * Soft safety lead time + * @description soft safety lead time + */ + extra_safety_leadtime?: string | null; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedLocation: { + /** @description Unique identifier */ + name?: string; + /** @description Hierarchical parent */ + owner?: string | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description Calendar defining the working hours and holidays */ + available?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedManufacturingOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + operation?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Completed quantity + * Format: decimal + */ + quantity_completed?: string | null; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** Format: decimal */ + readonly criticality?: string | null; + readonly delay?: string | null; + readonly plan?: unknown; + /** @description Hierarchical parent */ + owner?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + resources?: components["schemas"]["OperationPlanResourceNested"][]; + materials?: components["schemas"]["OperationPlanMaterialNested"][]; + /** @description Unique identifier */ + demand?: string | null; + readonly forecast?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedMeasure: { + /** @description Unique identifier */ + name?: string; + /** @description Label to be displayed in the user interface */ + label?: string | null; + description?: string | null; + type?: (components["schemas"]["MeasureTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + discrete?: boolean | null; + /** + * Default value + * Format: decimal + */ + defaultvalue?: string | null; + /** Mode in future periods */ + mode_future?: (components["schemas"]["ModeFutureEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** Mode in past periods */ + mode_past?: (components["schemas"]["ModePastEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Formula to compute values */ + compute_expression?: string | null; + /** @description Formula executed when updating this field */ + update_expression?: string | null; + /** Override measure */ + overrides?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedOperation: { + name?: string; + type?: (components["schemas"]["OperationTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description Item produced by this operation */ + item?: string | null; + /** @description Unique identifier */ + location?: string; + /** + * Release fence + * @description Operationplans within this time window from the current day are expected to be released to production ERP + */ + fence?: string | null; + /** + * Batching window + * @description The solver algorithm will scan for opportunities to create batches within this time window before and after the requirement date + */ + batchwindow?: string | null; + /** + * Post-op time + * @description A delay time to be respected as a soft constraint after ending the operation + */ + posttime?: string | null; + /** + * Size minimum + * Format: decimal + * @description Minimum production quantity + */ + sizeminimum?: string | null; + /** + * Size multiple + * Format: decimal + * @description Multiple production quantity + */ + sizemultiple?: string | null; + /** + * Size maximum + * Format: decimal + * @description Maximum production quantity + */ + sizemaximum?: string | null; + /** @description Parent operation (which must be of type routing, alternate or split) */ + owner?: string | null; + /** @description Priority among all alternates */ + priority?: number | null; + /** + * Format: date-time + * @description Validity start date + */ + effective_start?: string | null; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + /** @description Calendar defining the working hours and holidays */ + available?: string | null; + /** + * Format: decimal + * @description Cost per produced unit + */ + cost?: string | null; + /** @description Fixed production time for setup and overhead */ + duration?: string | null; + /** + * Duration per unit + * @description Production time per produced piece + */ + duration_per?: string | null; + /** + * Search mode + * @description Method to select preferred alternate + * + * * `PRIORITY` - priority + * * `MINCOST` - minimum cost + * * `MINPENALTY` - minimum penalty + * * `MINCOSTPENALTY` - minimum cost plus penalty + */ + search?: (components["schemas"]["SearchEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedOperationDependency: { + /** Identifier */ + readonly id?: number; + /** @description operation */ + operation?: string; + /** + * Blocked by operation + * @description blocked by operation + */ + blockedby?: string; + /** + * Format: decimal + * @description Quantity relation between the operations + */ + quantity?: string | null; + /** + * Soft safety lead time + * @description soft safety lead time + */ + safety_leadtime?: string | null; + /** + * Hard safety lead time + * @description hard safety lead time + */ + hard_safety_leadtime?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedOperationMaterial: { + /** Identifier */ + readonly id?: number; + operation?: string; + /** @description Unique identifier */ + item?: string; + /** @description Unique identifier */ + location?: string | null; + /** + * Format: decimal + * @description Quantity to consume or produce per piece + */ + quantity?: string | null; + /** + * Fixed quantity + * Format: decimal + * @description Fixed quantity to consume or produce + */ + quantity_fixed?: string | null; + /** + * Transfer batch quantity + * Format: decimal + * @description Batch size by in which material is produced or consumed + */ + transferbatch?: string | null; + /** @description Time offset from the start or end to consume or produce material */ + offset?: string | null; + /** @description Consume/produce material at the start or the end of the operationplan + * + * * `start` - Start + * * `end` - End + * * `transfer_batch` - Batch transfer */ + type?: (components["schemas"]["OperationMaterialTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + /** @description Name of this operation material to identify alternates */ + name?: string | null; + /** @description Priority of this operation material in a group of alternates */ + priority?: number | null; + /** + * Search mode + * @description Method to select preferred alternate + * + * * `PRIORITY` - priority + * * `MINCOST` - minimum cost + * * `MINPENALTY` - minimum penalty + * * `MINCOSTPENALTY` - minimum cost plus penalty + */ + search?: (components["schemas"]["SearchEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedOperationPlanMaterial: { + /** Identifier */ + readonly id?: number; + /** + * Reference + * @description Unique identifier + */ + operationplan?: string; + /** @description Unique identifier */ + item?: string; + /** @description Unique identifier */ + location?: string; + /** Format: decimal */ + quantity?: string; + /** Format: decimal */ + onhand?: string | null; + /** + * Date + * Format: date-time + */ + flowdate?: string; + /** + * Material status + * @description status of the material production or consumption + * + * * `proposed` - proposed + * * `confirmed` - confirmed + * * `closed` - closed + */ + status?: (components["schemas"]["Status9c1Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedOperationPlanResource: { + /** Identifier */ + readonly id?: number; + /** + * Reference + * @description Unique identifier + */ + operationplan?: string; + /** @description Unique identifier */ + resource?: string; + /** Format: decimal */ + quantity?: string | null; + readonly startdate?: string; + readonly enddate?: string; + /** + * Load status + * @description Status of the resource assignment + * + * * `proposed` - proposed + * * `confirmed` - confirmed + * * `closed` - closed + */ + status?: (components["schemas"]["Status9c1Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + setup?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedOperationResource: { + /** Identifier */ + readonly id?: number; + operation?: string; + /** @description Unique identifier */ + resource?: string; + /** @description Required skill to perform the operation */ + skill?: string | null; + /** + * Format: decimal + * @description Required quantity of the resource + */ + quantity?: string | null; + /** + * Format: decimal + * @description Constant part of the capacity consumption (bucketized resources only) + */ + quantity_fixed?: string | null; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + /** @description Name of this operation resource to identify alternates */ + name?: string | null; + /** @description Priority of this operation resource in a group of alternates */ + priority?: number | null; + /** @description Setup required on the resource for this operation */ + setup?: string | null; + /** + * Search mode + * @description Method to select preferred alternate + * + * * `PRIORITY` - priority + * * `MINCOST` - minimum cost + * * `MINPENALTY` - minimum penalty + * * `MINCOSTPENALTY` - minimum cost plus penalty + */ + search?: (components["schemas"]["SearchEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedParameter: { + name?: string; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + value?: string | null; + description?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedPurchaseOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Unique identifier */ + item?: string | null; + /** @description Unique identifier */ + supplier?: string | null; + /** @description Unique identifier */ + location?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** Format: decimal */ + readonly criticality?: string | null; + readonly delay?: string | null; + readonly plan?: unknown; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + readonly forecast?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedResource: { + /** @description Unique identifier */ + name?: string; + description?: string | null; + category?: string | null; + subcategory?: string | null; + type?: (components["schemas"]["ResourceTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * Format: decimal + * @description Size of the resource + */ + maximum?: string | null; + /** @description Calendar defining the resource size varying over time */ + maximum_calendar?: string | null; + /** @description Calendar defining the working hours and holidays */ + available?: string | null; + /** @description Unique identifier */ + location?: string | null; + /** @description Hierarchical parent */ + owner?: string | null; + /** + * Format: decimal + * @description Cost for using 1 unit of the resource for 1 hour + */ + cost?: string | null; + /** + * Max early + * @description Time window before the ask date where we look for available capacity + */ + maxearly?: string | null; + /** + * Setup matrix + * @description Setup matrix defining the conversion time and cost + */ + setupmatrix?: string | null; + /** @description Setup of the resource at the start of the plan */ + setup?: string | null; + /** + * Efficiency % + * Format: decimal + * @description Efficiency percentage. Operations will take longer on resources with efficiency less than 100%. + */ + efficiency?: string | null; + /** + * Efficiency % calendar + * @description Calendar defining the efficiency percentage varying over time + */ + efficiency_calendar?: string | null; + /** @description controls whether or not this resource is planned in finite capacity mode */ + constrained?: boolean | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + /** Count of capacity overload problems */ + readonly overloadcount?: number | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedResourceSkill: { + /** Identifier */ + readonly id?: number; + /** @description Unique identifier */ + resource?: string; + /** @description Unique identifier */ + skill?: string; + /** + * Format: date-time + * @description Validity start date + */ + effective_start?: string | null; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + /** @description Priority of this skill in a group of alternates */ + priority?: number | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedSetupMatrix: { + name?: string; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedSetupRule: { + /** Identifier */ + readonly id?: number; + /** Setup matrix */ + setupmatrix?: string; + /** + * From setup + * @description Name of the old setup (wildcard characters are supported) + */ + fromsetup?: string | null; + /** + * To setup + * @description Name of the new setup (wildcard characters are supported) + */ + tosetup?: string | null; + priority?: number; + /** @description Duration of the changeover */ + duration?: string | null; + /** + * Format: decimal + * @description Cost of the conversion + */ + cost?: string | null; + /** @description Extra resource used during this changeover */ + resource?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedSkill: { + /** @description Unique identifier */ + name?: string; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedSubOperation: { + /** Identifier */ + readonly id?: number; + /** @description Parent operation */ + operation?: string; + /** @description Sequence of this operation among the suboperations. Negative values are ignored. */ + priority?: number; + /** @description Child operation */ + suboperation?: string; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedSupplier: { + /** @description Unique identifier */ + name?: string; + /** @description Hierarchical parent */ + owner?: string | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description Calendar defining the working hours and holidays */ + available?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PatchedWorkOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + operation?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Completed quantity + * Format: decimal + */ + quantity_completed?: string | null; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** Format: decimal */ + readonly criticality?: string | null; + readonly delay?: string | null; + readonly plan?: unknown; + /** @description Hierarchical parent */ + owner?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified?: string; + resources?: components["schemas"]["OperationPlanResourceNested"][]; + materials?: components["schemas"]["OperationPlanMaterialNested"][]; + /** @description Unique identifier */ + demand?: string | null; + readonly forecast?: string | null; + }; + /** + * @description * `independent` - independent + * * `alltogether` - all together + * * `inratio` - in ratio + * @enum {string} + */ + PolicyEnum: "independent" | "alltogether" | "inratio"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + PurchaseOrder: { + /** @description Unique identifier */ + reference?: string; + /** @description Status of the order + * + * * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed */ + status?: (components["schemas"]["StatusCa7Enum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** @description Unique identifier */ + item?: string | null; + /** @description Unique identifier */ + supplier?: string | null; + /** @description Unique identifier */ + location?: string | null; + /** Format: decimal */ + quantity?: string; + /** + * Start date + * Format: date-time + * @description start date + */ + startdate?: string | null; + /** + * End date + * Format: date-time + * @description end date + */ + enddate?: string | null; + /** @description MTO batch name */ + batch?: string | null; + /** Format: decimal */ + readonly criticality: string | null; + readonly delay: string | null; + readonly plan: unknown; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + readonly forecast: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Resource: { + /** @description Unique identifier */ + name?: string; + description?: string | null; + category?: string | null; + subcategory?: string | null; + type?: (components["schemas"]["ResourceTypeEnum"] | components["schemas"]["BlankEnum"] | components["schemas"]["NullEnum"]) | null; + /** + * Format: decimal + * @description Size of the resource + */ + maximum?: string | null; + /** @description Calendar defining the resource size varying over time */ + maximum_calendar?: string | null; + /** @description Calendar defining the working hours and holidays */ + available?: string | null; + /** @description Unique identifier */ + location?: string | null; + /** @description Hierarchical parent */ + owner?: string | null; + /** + * Format: decimal + * @description Cost for using 1 unit of the resource for 1 hour + */ + cost?: string | null; + /** + * Max early + * @description Time window before the ask date where we look for available capacity + */ + maxearly?: string | null; + /** + * Setup matrix + * @description Setup matrix defining the conversion time and cost + */ + setupmatrix?: string | null; + /** @description Setup of the resource at the start of the plan */ + setup?: string | null; + /** + * Efficiency % + * Format: decimal + * @description Efficiency percentage. Operations will take longer on resources with efficiency less than 100%. + */ + efficiency?: string | null; + /** + * Efficiency % calendar + * @description Calendar defining the efficiency percentage varying over time + */ + efficiency_calendar?: string | null; + /** @description controls whether or not this resource is planned in finite capacity mode */ + constrained?: boolean | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + /** Count of capacity overload problems */ + readonly overloadcount: number | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + ResourceSkill: { + /** Identifier */ + readonly id: number; + /** @description Unique identifier */ + resource?: string; + /** @description Unique identifier */ + skill?: string; + /** + * Format: date-time + * @description Validity start date + */ + effective_start?: string | null; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + /** @description Priority of this skill in a group of alternates */ + priority?: number | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** + * @description * `default` - default + * * `buckets` - buckets + * * `buckets_day` - buckets_day + * * `buckets_week` - buckets_week + * * `buckets_month` - buckets_month + * * `infinite` - infinite + * @enum {string} + */ + ResourceTypeEnum: "default" | "buckets" | "buckets_day" | "buckets_week" | "buckets_month" | "infinite"; + /** + * @description * `PRIORITY` - priority + * * `MINCOST` - minimum cost + * * `MINPENALTY` - minimum penalty + * * `MINCOSTPENALTY` - minimum cost plus penalty + * @enum {string} + */ + SearchEnum: "PRIORITY" | "MINCOST" | "MINPENALTY" | "MINCOSTPENALTY"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + SetupMatrix: { + name?: string; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + SetupRule: { + /** Identifier */ + readonly id: number; + /** Setup matrix */ + setupmatrix?: string; + /** + * From setup + * @description Name of the old setup (wildcard characters are supported) + */ + fromsetup?: string | null; + /** + * To setup + * @description Name of the new setup (wildcard characters are supported) + */ + tosetup?: string | null; + priority?: number; + /** @description Duration of the changeover */ + duration?: string | null; + /** + * Format: decimal + * @description Cost of the conversion + */ + cost?: string | null; + /** @description Extra resource used during this changeover */ + resource?: string | null; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Skill: { + /** @description Unique identifier */ + name?: string; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** + * @description * `proposed` - proposed + * * `confirmed` - confirmed + * * `closed` - closed + * @enum {string} + */ + Status9c1Enum: "proposed" | "confirmed" | "closed"; + /** + * @description * `proposed` - proposed + * * `approved` - approved + * * `confirmed` - confirmed + * * `completed` - completed + * * `closed` - closed + * @enum {string} + */ + StatusCa7Enum: "proposed" | "approved" | "confirmed" | "completed" | "closed"; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + SubOperation: { + /** Identifier */ + readonly id: number; + /** @description Parent operation */ + operation?: string; + /** @description Sequence of this operation among the suboperations. Negative values are ignored. */ + priority?: number; + /** @description Child operation */ + suboperation?: string; + /** + * Format: date-time + * @description Validity start date + * @default 1971-01-01T00:00:00 + */ + effective_start: string; + /** + * Format: date-time + * @description Validity end date + */ + effective_end?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + /** @description The django model serializer extends the default implementation with the + * following capabilities: + * - Ability to work with natural keys. + * - Support for create-or-update on the POST method. + * The default POST method only supports create. + * The PUT method remains update-only. + * - Enable partial updates by default */ + Supplier: { + /** @description Unique identifier */ + name?: string; + /** @description Hierarchical parent */ + owner?: string | null; + description?: string | null; + category?: string | null; + subcategory?: string | null; + /** @description Calendar defining the working hours and holidays */ + available?: string | null; + source?: string | null; + /** + * Last modified + * Format: date-time + */ + readonly lastmodified: string; + }; + }; + responses: never; + parameters: never; + requestBodies: never; + headers: never; + pathItems: never; +} +export type $defs = Record; +export interface operations { + common_attribute_list: { + parameters: { + query?: { + editable?: boolean; + initially_hidden?: boolean; + lastmodified?: string; + lastmodified__gt?: string; + lastmodified__gte?: string; + /** @description Multiple values may be separated by commas. */ + lastmodified__in?: string[]; + lastmodified__lt?: string; + lastmodified__lte?: string; + /** @description * `calendar` - calendar + * * `supplier` - supplier + * * `item` - item + * * `location` - location + * * `customer` - customer + * * `demand` - demand + * * `operation` - operation + * * `operationplan` - operationplan + * * `operationplanmaterial` - operationplanmaterial + * * `setupmatrix` - setupmatrix + * * `skill` - skill + * * `resource` - resource + * * `operationplanresource` - operationplanresource + * * `suboperation` - suboperation + * * `setuprule` - setuprule + * * `resourceskill` - resourceskill + * * `operationresource` - operationresource + * * `operationmaterial` - operationmaterial + * * `itemsupplier` - itemsupplier + * * `itemdistribution` - itemdistribution + * * `calendarbucket` - calendarbucket + * * `buffer` - buffer + * * `operationdependency` - operationdependency + * * `workorder` - workorder + * * `forecast` - forecast + * * `constraint` - constraint + * * `problem` - problem + * * `user` - user + * * `bucket` - bucket + * * `bucketdetail` - bucketdetail + * * `apikey` - apikey */ + model?: "apikey" | "bucket" | "bucketdetail" | "buffer" | "calendar" | "calendarbucket" | "constraint" | "customer" | "demand" | "forecast" | "item" | "itemdistribution" | "itemsupplier" | "location" | "operation" | "operationdependency" | "operationmaterial" | "operationplan" | "operationplanmaterial" | "operationplanresource" | "operationresource" | "problem" | "resource" | "resourceskill" | "setupmatrix" | "setuprule" | "skill" | "suboperation" | "supplier" | "user" | "workorder"; + /** @description Multiple values may be separated by commas. */ + model__in?: string[]; + name?: string; + name__contains?: string; + /** @description Multiple values may be separated by commas. */ + name__in?: string[]; + source?: string; + /** @description Multiple values may be separated by commas. */ + source__in?: string[]; + }; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedAttribute"][]; + }; + }; + }; + }; + common_attribute_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedAttribute"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedAttribute"]; + "multipart/form-data": components["schemas"]["PatchedAttribute"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedAttribute"]; + }; + }; + }; + }; + common_attribute_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedAttribute"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedAttribute"]; + "multipart/form-data": components["schemas"]["PatchedAttribute"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedAttribute"]; + }; + }; + }; + }; + common_attribute_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_attribute_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedAttribute"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedAttribute"]; + "multipart/form-data": components["schemas"]["PatchedAttribute"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedAttribute"]; + }; + }; + }; + }; + common_attribute_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Attribute"]; + }; + }; + }; + }; + common_attribute_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Attribute"]; + "application/x-www-form-urlencoded": components["schemas"]["Attribute"]; + "multipart/form-data": components["schemas"]["Attribute"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Attribute"]; + }; + }; + }; + }; + common_attribute_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_attribute_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedAttribute"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedAttribute"]; + "multipart/form-data": components["schemas"]["PatchedAttribute"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Attribute"]; + }; + }; + }; + }; + common_bucket_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBucket"][]; + }; + }; + }; + }; + common_bucket_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBucket"]; + "multipart/form-data": components["schemas"]["PatchedBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBucket"]; + }; + }; + }; + }; + common_bucket_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBucket"]; + "multipart/form-data": components["schemas"]["PatchedBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBucket"]; + }; + }; + }; + }; + common_bucket_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_bucket_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBucket"]; + "multipart/form-data": components["schemas"]["PatchedBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBucket"]; + }; + }; + }; + }; + common_bucket_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Bucket"]; + }; + }; + }; + }; + common_bucket_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Bucket"]; + "application/x-www-form-urlencoded": components["schemas"]["Bucket"]; + "multipart/form-data": components["schemas"]["Bucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Bucket"]; + }; + }; + }; + }; + common_bucket_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_bucket_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBucket"]; + "multipart/form-data": components["schemas"]["PatchedBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Bucket"]; + }; + }; + }; + }; + common_bucketdetail_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBucketDetail"][]; + }; + }; + }; + }; + common_bucketdetail_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBucketDetail"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBucketDetail"]; + "multipart/form-data": components["schemas"]["PatchedBucketDetail"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBucketDetail"]; + }; + }; + }; + }; + common_bucketdetail_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBucketDetail"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBucketDetail"]; + "multipart/form-data": components["schemas"]["PatchedBucketDetail"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBucketDetail"]; + }; + }; + }; + }; + common_bucketdetail_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_bucketdetail_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBucketDetail"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBucketDetail"]; + "multipart/form-data": components["schemas"]["PatchedBucketDetail"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBucketDetail"]; + }; + }; + }; + }; + common_bucketdetail_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BucketDetail"]; + }; + }; + }; + }; + common_bucketdetail_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["BucketDetail"]; + "application/x-www-form-urlencoded": components["schemas"]["BucketDetail"]; + "multipart/form-data": components["schemas"]["BucketDetail"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BucketDetail"]; + }; + }; + }; + }; + common_bucketdetail_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_bucketdetail_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBucketDetail"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBucketDetail"]; + "multipart/form-data": components["schemas"]["PatchedBucketDetail"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["BucketDetail"]; + }; + }; + }; + }; + common_comment_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedComment"][]; + }; + }; + }; + }; + common_comment_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedComment"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedComment"]; + "multipart/form-data": components["schemas"]["PatchedComment"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedComment"]; + }; + }; + }; + }; + common_comment_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedComment"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedComment"]; + "multipart/form-data": components["schemas"]["PatchedComment"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedComment"]; + }; + }; + }; + }; + common_comment_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_comment_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedComment"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedComment"]; + "multipart/form-data": components["schemas"]["PatchedComment"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedComment"]; + }; + }; + }; + }; + common_comment_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Comment"]; + }; + }; + }; + }; + common_comment_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Comment"]; + "application/x-www-form-urlencoded": components["schemas"]["Comment"]; + "multipart/form-data": components["schemas"]["Comment"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Comment"]; + }; + }; + }; + }; + common_comment_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_comment_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedComment"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedComment"]; + "multipart/form-data": components["schemas"]["PatchedComment"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Comment"]; + }; + }; + }; + }; + common_parameter_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedParameter"][]; + }; + }; + }; + }; + common_parameter_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedParameter"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedParameter"]; + "multipart/form-data": components["schemas"]["PatchedParameter"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedParameter"]; + }; + }; + }; + }; + common_parameter_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedParameter"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedParameter"]; + "multipart/form-data": components["schemas"]["PatchedParameter"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedParameter"]; + }; + }; + }; + }; + common_parameter_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_parameter_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedParameter"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedParameter"]; + "multipart/form-data": components["schemas"]["PatchedParameter"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedParameter"]; + }; + }; + }; + }; + common_parameter_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Parameter"]; + }; + }; + }; + }; + common_parameter_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Parameter"]; + "application/x-www-form-urlencoded": components["schemas"]["Parameter"]; + "multipart/form-data": components["schemas"]["Parameter"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Parameter"]; + }; + }; + }; + }; + common_parameter_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + common_parameter_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedParameter"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedParameter"]; + "multipart/form-data": components["schemas"]["PatchedParameter"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Parameter"]; + }; + }; + }; + }; + forecast_forecast_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedForecast"][]; + }; + }; + }; + }; + forecast_forecast_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedForecast"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedForecast"]; + "multipart/form-data": components["schemas"]["PatchedForecast"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedForecast"]; + }; + }; + }; + }; + forecast_forecast_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedForecast"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedForecast"]; + "multipart/form-data": components["schemas"]["PatchedForecast"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedForecast"]; + }; + }; + }; + }; + forecast_forecast_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + forecast_forecast_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedForecast"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedForecast"]; + "multipart/form-data": components["schemas"]["PatchedForecast"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedForecast"]; + }; + }; + }; + }; + forecast_forecast_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Forecast"]; + }; + }; + }; + }; + forecast_forecast_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Forecast"]; + "application/x-www-form-urlencoded": components["schemas"]["Forecast"]; + "multipart/form-data": components["schemas"]["Forecast"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Forecast"]; + }; + }; + }; + }; + forecast_forecast_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + forecast_forecast_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedForecast"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedForecast"]; + "multipart/form-data": components["schemas"]["PatchedForecast"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Forecast"]; + }; + }; + }; + }; + forecast_forecastplan_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedForecastPlan"][]; + }; + }; + }; + }; + forecast_forecastplan_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedForecastPlan"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedForecastPlan"]; + "multipart/form-data": components["schemas"]["PatchedForecastPlan"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedForecastPlan"]; + }; + }; + }; + }; + forecast_forecastplan_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedForecastPlan"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedForecastPlan"]; + "multipart/form-data": components["schemas"]["PatchedForecastPlan"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedForecastPlan"]; + }; + }; + }; + }; + forecast_forecastplan_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + forecast_forecastplan_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedForecastPlan"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedForecastPlan"]; + "multipart/form-data": components["schemas"]["PatchedForecastPlan"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedForecastPlan"]; + }; + }; + }; + }; + forecast_measure_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedMeasure"][]; + }; + }; + }; + }; + forecast_measure_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedMeasure"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedMeasure"]; + "multipart/form-data": components["schemas"]["PatchedMeasure"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedMeasure"]; + }; + }; + }; + }; + forecast_measure_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedMeasure"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedMeasure"]; + "multipart/form-data": components["schemas"]["PatchedMeasure"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedMeasure"]; + }; + }; + }; + }; + forecast_measure_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + forecast_measure_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedMeasure"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedMeasure"]; + "multipart/form-data": components["schemas"]["PatchedMeasure"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedMeasure"]; + }; + }; + }; + }; + forecast_measure_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + forecast_measure_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + forecast_measure_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + forecast_measure_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 200: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_buffer_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBuffer"][]; + }; + }; + }; + }; + input_buffer_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBuffer"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBuffer"]; + "multipart/form-data": components["schemas"]["PatchedBuffer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBuffer"]; + }; + }; + }; + }; + input_buffer_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBuffer"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBuffer"]; + "multipart/form-data": components["schemas"]["PatchedBuffer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBuffer"]; + }; + }; + }; + }; + input_buffer_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_buffer_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBuffer"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBuffer"]; + "multipart/form-data": components["schemas"]["PatchedBuffer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedBuffer"]; + }; + }; + }; + }; + input_buffer_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Buffer"]; + }; + }; + }; + }; + input_buffer_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Buffer"]; + "application/x-www-form-urlencoded": components["schemas"]["Buffer"]; + "multipart/form-data": components["schemas"]["Buffer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Buffer"]; + }; + }; + }; + }; + input_buffer_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_buffer_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedBuffer"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedBuffer"]; + "multipart/form-data": components["schemas"]["PatchedBuffer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Buffer"]; + }; + }; + }; + }; + input_calendar_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCalendar"][]; + }; + }; + }; + }; + input_calendar_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCalendar"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCalendar"]; + "multipart/form-data": components["schemas"]["PatchedCalendar"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCalendar"]; + }; + }; + }; + }; + input_calendar_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCalendar"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCalendar"]; + "multipart/form-data": components["schemas"]["PatchedCalendar"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCalendar"]; + }; + }; + }; + }; + input_calendar_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_calendar_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCalendar"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCalendar"]; + "multipart/form-data": components["schemas"]["PatchedCalendar"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCalendar"]; + }; + }; + }; + }; + input_calendar_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Calendar"]; + }; + }; + }; + }; + input_calendar_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Calendar"]; + "application/x-www-form-urlencoded": components["schemas"]["Calendar"]; + "multipart/form-data": components["schemas"]["Calendar"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Calendar"]; + }; + }; + }; + }; + input_calendar_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_calendar_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCalendar"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCalendar"]; + "multipart/form-data": components["schemas"]["PatchedCalendar"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Calendar"]; + }; + }; + }; + }; + input_calendarbucket_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCalendarBucket"][]; + }; + }; + }; + }; + input_calendarbucket_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCalendarBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCalendarBucket"]; + "multipart/form-data": components["schemas"]["PatchedCalendarBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCalendarBucket"]; + }; + }; + }; + }; + input_calendarbucket_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCalendarBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCalendarBucket"]; + "multipart/form-data": components["schemas"]["PatchedCalendarBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCalendarBucket"]; + }; + }; + }; + }; + input_calendarbucket_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_calendarbucket_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCalendarBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCalendarBucket"]; + "multipart/form-data": components["schemas"]["PatchedCalendarBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCalendarBucket"]; + }; + }; + }; + }; + input_calendarbucket_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CalendarBucket"]; + }; + }; + }; + }; + input_calendarbucket_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["CalendarBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["CalendarBucket"]; + "multipart/form-data": components["schemas"]["CalendarBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CalendarBucket"]; + }; + }; + }; + }; + input_calendarbucket_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_calendarbucket_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCalendarBucket"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCalendarBucket"]; + "multipart/form-data": components["schemas"]["PatchedCalendarBucket"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["CalendarBucket"]; + }; + }; + }; + }; + input_customer_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCustomer"][]; + }; + }; + }; + }; + input_customer_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCustomer"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCustomer"]; + "multipart/form-data": components["schemas"]["PatchedCustomer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCustomer"]; + }; + }; + }; + }; + input_customer_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCustomer"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCustomer"]; + "multipart/form-data": components["schemas"]["PatchedCustomer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCustomer"]; + }; + }; + }; + }; + input_customer_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_customer_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCustomer"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCustomer"]; + "multipart/form-data": components["schemas"]["PatchedCustomer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedCustomer"]; + }; + }; + }; + }; + input_customer_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Customer"]; + }; + }; + }; + }; + input_customer_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Customer"]; + "application/x-www-form-urlencoded": components["schemas"]["Customer"]; + "multipart/form-data": components["schemas"]["Customer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Customer"]; + }; + }; + }; + }; + input_customer_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_customer_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedCustomer"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedCustomer"]; + "multipart/form-data": components["schemas"]["PatchedCustomer"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Customer"]; + }; + }; + }; + }; + input_deliveryorder_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDeliveryOrder"][]; + }; + }; + }; + }; + input_deliveryorder_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDeliveryOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDeliveryOrder"]; + "multipart/form-data": components["schemas"]["PatchedDeliveryOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDeliveryOrder"]; + }; + }; + }; + }; + input_deliveryorder_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDeliveryOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDeliveryOrder"]; + "multipart/form-data": components["schemas"]["PatchedDeliveryOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDeliveryOrder"]; + }; + }; + }; + }; + input_deliveryorder_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_deliveryorder_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDeliveryOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDeliveryOrder"]; + "multipart/form-data": components["schemas"]["PatchedDeliveryOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDeliveryOrder"]; + }; + }; + }; + }; + input_deliveryorder_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeliveryOrder"]; + }; + }; + }; + }; + input_deliveryorder_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["DeliveryOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["DeliveryOrder"]; + "multipart/form-data": components["schemas"]["DeliveryOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeliveryOrder"]; + }; + }; + }; + }; + input_deliveryorder_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_deliveryorder_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDeliveryOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDeliveryOrder"]; + "multipart/form-data": components["schemas"]["PatchedDeliveryOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DeliveryOrder"]; + }; + }; + }; + }; + input_demand_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDemand"][]; + }; + }; + }; + }; + input_demand_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDemand"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDemand"]; + "multipart/form-data": components["schemas"]["PatchedDemand"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDemand"]; + }; + }; + }; + }; + input_demand_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDemand"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDemand"]; + "multipart/form-data": components["schemas"]["PatchedDemand"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDemand"]; + }; + }; + }; + }; + input_demand_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_demand_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDemand"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDemand"]; + "multipart/form-data": components["schemas"]["PatchedDemand"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDemand"]; + }; + }; + }; + }; + input_demand_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Demand"]; + }; + }; + }; + }; + input_demand_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Demand"]; + "application/x-www-form-urlencoded": components["schemas"]["Demand"]; + "multipart/form-data": components["schemas"]["Demand"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Demand"]; + }; + }; + }; + }; + input_demand_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_demand_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDemand"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDemand"]; + "multipart/form-data": components["schemas"]["PatchedDemand"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Demand"]; + }; + }; + }; + }; + input_distributionorder_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDistributionOrder"][]; + }; + }; + }; + }; + input_distributionorder_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDistributionOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDistributionOrder"]; + "multipart/form-data": components["schemas"]["PatchedDistributionOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDistributionOrder"]; + }; + }; + }; + }; + input_distributionorder_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDistributionOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDistributionOrder"]; + "multipart/form-data": components["schemas"]["PatchedDistributionOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDistributionOrder"]; + }; + }; + }; + }; + input_distributionorder_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_distributionorder_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDistributionOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDistributionOrder"]; + "multipart/form-data": components["schemas"]["PatchedDistributionOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedDistributionOrder"]; + }; + }; + }; + }; + input_distributionorder_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DistributionOrder"]; + }; + }; + }; + }; + input_distributionorder_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["DistributionOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["DistributionOrder"]; + "multipart/form-data": components["schemas"]["DistributionOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DistributionOrder"]; + }; + }; + }; + }; + input_distributionorder_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_distributionorder_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedDistributionOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedDistributionOrder"]; + "multipart/form-data": components["schemas"]["PatchedDistributionOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["DistributionOrder"]; + }; + }; + }; + }; + input_item_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItem"][]; + }; + }; + }; + }; + input_item_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItem"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItem"]; + "multipart/form-data": components["schemas"]["PatchedItem"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItem"]; + }; + }; + }; + }; + input_item_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItem"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItem"]; + "multipart/form-data": components["schemas"]["PatchedItem"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItem"]; + }; + }; + }; + }; + input_item_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_item_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItem"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItem"]; + "multipart/form-data": components["schemas"]["PatchedItem"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItem"]; + }; + }; + }; + }; + input_item_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Item"]; + }; + }; + }; + }; + input_item_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Item"]; + "application/x-www-form-urlencoded": components["schemas"]["Item"]; + "multipart/form-data": components["schemas"]["Item"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Item"]; + }; + }; + }; + }; + input_item_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_item_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItem"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItem"]; + "multipart/form-data": components["schemas"]["PatchedItem"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Item"]; + }; + }; + }; + }; + input_itemdistribution_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItemDistribution"][]; + }; + }; + }; + }; + input_itemdistribution_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItemDistribution"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItemDistribution"]; + "multipart/form-data": components["schemas"]["PatchedItemDistribution"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItemDistribution"]; + }; + }; + }; + }; + input_itemdistribution_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItemDistribution"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItemDistribution"]; + "multipart/form-data": components["schemas"]["PatchedItemDistribution"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItemDistribution"]; + }; + }; + }; + }; + input_itemdistribution_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_itemdistribution_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItemDistribution"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItemDistribution"]; + "multipart/form-data": components["schemas"]["PatchedItemDistribution"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItemDistribution"]; + }; + }; + }; + }; + input_itemdistribution_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ItemDistribution"]; + }; + }; + }; + }; + input_itemdistribution_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ItemDistribution"]; + "application/x-www-form-urlencoded": components["schemas"]["ItemDistribution"]; + "multipart/form-data": components["schemas"]["ItemDistribution"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ItemDistribution"]; + }; + }; + }; + }; + input_itemdistribution_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_itemdistribution_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItemDistribution"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItemDistribution"]; + "multipart/form-data": components["schemas"]["PatchedItemDistribution"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ItemDistribution"]; + }; + }; + }; + }; + input_itemsupplier_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItemSupplier"][]; + }; + }; + }; + }; + input_itemsupplier_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItemSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItemSupplier"]; + "multipart/form-data": components["schemas"]["PatchedItemSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItemSupplier"]; + }; + }; + }; + }; + input_itemsupplier_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItemSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItemSupplier"]; + "multipart/form-data": components["schemas"]["PatchedItemSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItemSupplier"]; + }; + }; + }; + }; + input_itemsupplier_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_itemsupplier_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItemSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItemSupplier"]; + "multipart/form-data": components["schemas"]["PatchedItemSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedItemSupplier"]; + }; + }; + }; + }; + input_itemsupplier_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ItemSupplier"]; + }; + }; + }; + }; + input_itemsupplier_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ItemSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["ItemSupplier"]; + "multipart/form-data": components["schemas"]["ItemSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ItemSupplier"]; + }; + }; + }; + }; + input_itemsupplier_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_itemsupplier_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedItemSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedItemSupplier"]; + "multipart/form-data": components["schemas"]["PatchedItemSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ItemSupplier"]; + }; + }; + }; + }; + input_location_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedLocation"][]; + }; + }; + }; + }; + input_location_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedLocation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedLocation"]; + "multipart/form-data": components["schemas"]["PatchedLocation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedLocation"]; + }; + }; + }; + }; + input_location_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedLocation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedLocation"]; + "multipart/form-data": components["schemas"]["PatchedLocation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedLocation"]; + }; + }; + }; + }; + input_location_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_location_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedLocation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedLocation"]; + "multipart/form-data": components["schemas"]["PatchedLocation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedLocation"]; + }; + }; + }; + }; + input_location_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Location"]; + }; + }; + }; + }; + input_location_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Location"]; + "application/x-www-form-urlencoded": components["schemas"]["Location"]; + "multipart/form-data": components["schemas"]["Location"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Location"]; + }; + }; + }; + }; + input_location_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_location_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedLocation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedLocation"]; + "multipart/form-data": components["schemas"]["PatchedLocation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Location"]; + }; + }; + }; + }; + input_manufacturingorder_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"][]; + }; + }; + }; + }; + input_manufacturingorder_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedManufacturingOrder"]; + "multipart/form-data": components["schemas"]["PatchedManufacturingOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"]; + }; + }; + }; + }; + input_manufacturingorder_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedManufacturingOrder"]; + "multipart/form-data": components["schemas"]["PatchedManufacturingOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"]; + }; + }; + }; + }; + input_manufacturingorder_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_manufacturingorder_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedManufacturingOrder"]; + "multipart/form-data": components["schemas"]["PatchedManufacturingOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"]; + }; + }; + }; + }; + input_manufacturingorder_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManufacturingOrder"]; + }; + }; + }; + }; + input_manufacturingorder_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ManufacturingOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["ManufacturingOrder"]; + "multipart/form-data": components["schemas"]["ManufacturingOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManufacturingOrder"]; + }; + }; + }; + }; + input_manufacturingorder_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_manufacturingorder_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedManufacturingOrder"]; + "multipart/form-data": components["schemas"]["PatchedManufacturingOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManufacturingOrder"]; + }; + }; + }; + }; + input_operation_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperation"][]; + }; + }; + }; + }; + input_operation_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperation"]; + "multipart/form-data": components["schemas"]["PatchedOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperation"]; + }; + }; + }; + }; + input_operation_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperation"]; + "multipart/form-data": components["schemas"]["PatchedOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperation"]; + }; + }; + }; + }; + input_operation_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operation_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperation"]; + "multipart/form-data": components["schemas"]["PatchedOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperation"]; + }; + }; + }; + }; + input_operation_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Operation"]; + }; + }; + }; + }; + input_operation_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["Operation"]; + "application/x-www-form-urlencoded": components["schemas"]["Operation"]; + "multipart/form-data": components["schemas"]["Operation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Operation"]; + }; + }; + }; + }; + input_operation_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operation_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperation"]; + "multipart/form-data": components["schemas"]["PatchedOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Operation"]; + }; + }; + }; + }; + input_operationdependency_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationDependency"][]; + }; + }; + }; + }; + input_operationdependency_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationDependency"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationDependency"]; + "multipart/form-data": components["schemas"]["PatchedOperationDependency"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationDependency"]; + }; + }; + }; + }; + input_operationdependency_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationDependency"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationDependency"]; + "multipart/form-data": components["schemas"]["PatchedOperationDependency"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationDependency"]; + }; + }; + }; + }; + input_operationdependency_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationdependency_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationDependency"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationDependency"]; + "multipart/form-data": components["schemas"]["PatchedOperationDependency"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationDependency"]; + }; + }; + }; + }; + input_operationdependency_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationDependency"]; + }; + }; + }; + }; + input_operationdependency_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["OperationDependency"]; + "application/x-www-form-urlencoded": components["schemas"]["OperationDependency"]; + "multipart/form-data": components["schemas"]["OperationDependency"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationDependency"]; + }; + }; + }; + }; + input_operationdependency_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationdependency_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationDependency"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationDependency"]; + "multipart/form-data": components["schemas"]["PatchedOperationDependency"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationDependency"]; + }; + }; + }; + }; + input_operationmaterial_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationMaterial"][]; + }; + }; + }; + }; + input_operationmaterial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationMaterial"]; + "multipart/form-data": components["schemas"]["PatchedOperationMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationMaterial"]; + }; + }; + }; + }; + input_operationmaterial_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationMaterial"]; + "multipart/form-data": components["schemas"]["PatchedOperationMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationMaterial"]; + }; + }; + }; + }; + input_operationmaterial_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationmaterial_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationMaterial"]; + "multipart/form-data": components["schemas"]["PatchedOperationMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationMaterial"]; + }; + }; + }; + }; + input_operationmaterial_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationMaterial"]; + }; + }; + }; + }; + input_operationmaterial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["OperationMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["OperationMaterial"]; + "multipart/form-data": components["schemas"]["OperationMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationMaterial"]; + }; + }; + }; + }; + input_operationmaterial_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationmaterial_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationMaterial"]; + "multipart/form-data": components["schemas"]["PatchedOperationMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationMaterial"]; + }; + }; + }; + }; + input_operationplanmaterial_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationPlanMaterial"][]; + }; + }; + }; + }; + input_operationplanmaterial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationPlanMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationPlanMaterial"]; + "multipart/form-data": components["schemas"]["PatchedOperationPlanMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationPlanMaterial"]; + }; + }; + }; + }; + input_operationplanmaterial_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationPlanMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationPlanMaterial"]; + "multipart/form-data": components["schemas"]["PatchedOperationPlanMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationPlanMaterial"]; + }; + }; + }; + }; + input_operationplanmaterial_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationplanmaterial_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationPlanMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationPlanMaterial"]; + "multipart/form-data": components["schemas"]["PatchedOperationPlanMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationPlanMaterial"]; + }; + }; + }; + }; + input_operationplanmaterial_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationPlanMaterial"]; + }; + }; + }; + }; + input_operationplanmaterial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody: { + content: { + "application/json": components["schemas"]["OperationPlanMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["OperationPlanMaterial"]; + "multipart/form-data": components["schemas"]["OperationPlanMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationPlanMaterial"]; + }; + }; + }; + }; + input_operationplanmaterial_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationplanmaterial_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationPlanMaterial"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationPlanMaterial"]; + "multipart/form-data": components["schemas"]["PatchedOperationPlanMaterial"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationPlanMaterial"]; + }; + }; + }; + }; + input_operationplanresource_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationPlanResource"][]; + }; + }; + }; + }; + input_operationplanresource_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationPlanResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationPlanResource"]; + "multipart/form-data": components["schemas"]["PatchedOperationPlanResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationPlanResource"]; + }; + }; + }; + }; + input_operationplanresource_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationPlanResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationPlanResource"]; + "multipart/form-data": components["schemas"]["PatchedOperationPlanResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationPlanResource"]; + }; + }; + }; + }; + input_operationplanresource_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationplanresource_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationPlanResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationPlanResource"]; + "multipart/form-data": components["schemas"]["PatchedOperationPlanResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationPlanResource"]; + }; + }; + }; + }; + input_operationplanresource_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationPlanResource"]; + }; + }; + }; + }; + input_operationplanresource_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["OperationPlanResource"]; + "application/x-www-form-urlencoded": components["schemas"]["OperationPlanResource"]; + "multipart/form-data": components["schemas"]["OperationPlanResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationPlanResource"]; + }; + }; + }; + }; + input_operationplanresource_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationplanresource_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationPlanResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationPlanResource"]; + "multipart/form-data": components["schemas"]["PatchedOperationPlanResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationPlanResource"]; + }; + }; + }; + }; + input_operationresource_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationResource"][]; + }; + }; + }; + }; + input_operationresource_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationResource"]; + "multipart/form-data": components["schemas"]["PatchedOperationResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationResource"]; + }; + }; + }; + }; + input_operationresource_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationResource"]; + "multipart/form-data": components["schemas"]["PatchedOperationResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationResource"]; + }; + }; + }; + }; + input_operationresource_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationresource_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationResource"]; + "multipart/form-data": components["schemas"]["PatchedOperationResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedOperationResource"]; + }; + }; + }; + }; + input_operationresource_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationResource"]; + }; + }; + }; + }; + input_operationresource_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["OperationResource"]; + "application/x-www-form-urlencoded": components["schemas"]["OperationResource"]; + "multipart/form-data": components["schemas"]["OperationResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationResource"]; + }; + }; + }; + }; + input_operationresource_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_operationresource_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedOperationResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedOperationResource"]; + "multipart/form-data": components["schemas"]["PatchedOperationResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["OperationResource"]; + }; + }; + }; + }; + input_purchaseorder_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedPurchaseOrder"][]; + }; + }; + }; + }; + input_purchaseorder_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedPurchaseOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedPurchaseOrder"]; + "multipart/form-data": components["schemas"]["PatchedPurchaseOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedPurchaseOrder"]; + }; + }; + }; + }; + input_purchaseorder_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedPurchaseOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedPurchaseOrder"]; + "multipart/form-data": components["schemas"]["PatchedPurchaseOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedPurchaseOrder"]; + }; + }; + }; + }; + input_purchaseorder_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_purchaseorder_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedPurchaseOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedPurchaseOrder"]; + "multipart/form-data": components["schemas"]["PatchedPurchaseOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedPurchaseOrder"]; + }; + }; + }; + }; + input_purchaseorder_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PurchaseOrder"]; + }; + }; + }; + }; + input_purchaseorder_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PurchaseOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PurchaseOrder"]; + "multipart/form-data": components["schemas"]["PurchaseOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PurchaseOrder"]; + }; + }; + }; + }; + input_purchaseorder_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_purchaseorder_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedPurchaseOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedPurchaseOrder"]; + "multipart/form-data": components["schemas"]["PatchedPurchaseOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PurchaseOrder"]; + }; + }; + }; + }; + input_resource_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedResource"][]; + }; + }; + }; + }; + input_resource_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedResource"]; + "multipart/form-data": components["schemas"]["PatchedResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedResource"]; + }; + }; + }; + }; + input_resource_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedResource"]; + "multipart/form-data": components["schemas"]["PatchedResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedResource"]; + }; + }; + }; + }; + input_resource_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_resource_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedResource"]; + "multipart/form-data": components["schemas"]["PatchedResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedResource"]; + }; + }; + }; + }; + input_resource_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Resource"]; + }; + }; + }; + }; + input_resource_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Resource"]; + "application/x-www-form-urlencoded": components["schemas"]["Resource"]; + "multipart/form-data": components["schemas"]["Resource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Resource"]; + }; + }; + }; + }; + input_resource_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_resource_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedResource"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedResource"]; + "multipart/form-data": components["schemas"]["PatchedResource"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Resource"]; + }; + }; + }; + }; + input_resourceskill_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedResourceSkill"][]; + }; + }; + }; + }; + input_resourceskill_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedResourceSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedResourceSkill"]; + "multipart/form-data": components["schemas"]["PatchedResourceSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedResourceSkill"]; + }; + }; + }; + }; + input_resourceskill_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedResourceSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedResourceSkill"]; + "multipart/form-data": components["schemas"]["PatchedResourceSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedResourceSkill"]; + }; + }; + }; + }; + input_resourceskill_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_resourceskill_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedResourceSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedResourceSkill"]; + "multipart/form-data": components["schemas"]["PatchedResourceSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedResourceSkill"]; + }; + }; + }; + }; + input_resourceskill_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResourceSkill"]; + }; + }; + }; + }; + input_resourceskill_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ResourceSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["ResourceSkill"]; + "multipart/form-data": components["schemas"]["ResourceSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResourceSkill"]; + }; + }; + }; + }; + input_resourceskill_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_resourceskill_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedResourceSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedResourceSkill"]; + "multipart/form-data": components["schemas"]["PatchedResourceSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ResourceSkill"]; + }; + }; + }; + }; + input_setupmatrix_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSetupMatrix"][]; + }; + }; + }; + }; + input_setupmatrix_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSetupMatrix"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSetupMatrix"]; + "multipart/form-data": components["schemas"]["PatchedSetupMatrix"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSetupMatrix"]; + }; + }; + }; + }; + input_setupmatrix_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSetupMatrix"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSetupMatrix"]; + "multipart/form-data": components["schemas"]["PatchedSetupMatrix"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSetupMatrix"]; + }; + }; + }; + }; + input_setupmatrix_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_setupmatrix_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSetupMatrix"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSetupMatrix"]; + "multipart/form-data": components["schemas"]["PatchedSetupMatrix"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSetupMatrix"]; + }; + }; + }; + }; + input_setupmatrix_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetupMatrix"]; + }; + }; + }; + }; + input_setupmatrix_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["SetupMatrix"]; + "application/x-www-form-urlencoded": components["schemas"]["SetupMatrix"]; + "multipart/form-data": components["schemas"]["SetupMatrix"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetupMatrix"]; + }; + }; + }; + }; + input_setupmatrix_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_setupmatrix_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSetupMatrix"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSetupMatrix"]; + "multipart/form-data": components["schemas"]["PatchedSetupMatrix"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetupMatrix"]; + }; + }; + }; + }; + input_setuprule_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSetupRule"][]; + }; + }; + }; + }; + input_setuprule_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSetupRule"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSetupRule"]; + "multipart/form-data": components["schemas"]["PatchedSetupRule"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSetupRule"]; + }; + }; + }; + }; + input_setuprule_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSetupRule"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSetupRule"]; + "multipart/form-data": components["schemas"]["PatchedSetupRule"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSetupRule"]; + }; + }; + }; + }; + input_setuprule_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_setuprule_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSetupRule"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSetupRule"]; + "multipart/form-data": components["schemas"]["PatchedSetupRule"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSetupRule"]; + }; + }; + }; + }; + input_setuprule_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetupRule"]; + }; + }; + }; + }; + input_setuprule_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["SetupRule"]; + "application/x-www-form-urlencoded": components["schemas"]["SetupRule"]; + "multipart/form-data": components["schemas"]["SetupRule"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetupRule"]; + }; + }; + }; + }; + input_setuprule_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_setuprule_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSetupRule"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSetupRule"]; + "multipart/form-data": components["schemas"]["PatchedSetupRule"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SetupRule"]; + }; + }; + }; + }; + input_skill_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSkill"][]; + }; + }; + }; + }; + input_skill_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSkill"]; + "multipart/form-data": components["schemas"]["PatchedSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSkill"]; + }; + }; + }; + }; + input_skill_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSkill"]; + "multipart/form-data": components["schemas"]["PatchedSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSkill"]; + }; + }; + }; + }; + input_skill_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_skill_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSkill"]; + "multipart/form-data": components["schemas"]["PatchedSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSkill"]; + }; + }; + }; + }; + input_skill_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Skill"]; + }; + }; + }; + }; + input_skill_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Skill"]; + "application/x-www-form-urlencoded": components["schemas"]["Skill"]; + "multipart/form-data": components["schemas"]["Skill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Skill"]; + }; + }; + }; + }; + input_skill_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_skill_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSkill"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSkill"]; + "multipart/form-data": components["schemas"]["PatchedSkill"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Skill"]; + }; + }; + }; + }; + input_suboperation_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSubOperation"][]; + }; + }; + }; + }; + input_suboperation_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSubOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSubOperation"]; + "multipart/form-data": components["schemas"]["PatchedSubOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSubOperation"]; + }; + }; + }; + }; + input_suboperation_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSubOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSubOperation"]; + "multipart/form-data": components["schemas"]["PatchedSubOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSubOperation"]; + }; + }; + }; + }; + input_suboperation_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_suboperation_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSubOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSubOperation"]; + "multipart/form-data": components["schemas"]["PatchedSubOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSubOperation"]; + }; + }; + }; + }; + input_suboperation_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SubOperation"]; + }; + }; + }; + }; + input_suboperation_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["SubOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["SubOperation"]; + "multipart/form-data": components["schemas"]["SubOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SubOperation"]; + }; + }; + }; + }; + input_suboperation_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_suboperation_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSubOperation"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSubOperation"]; + "multipart/form-data": components["schemas"]["PatchedSubOperation"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["SubOperation"]; + }; + }; + }; + }; + input_supplier_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSupplier"][]; + }; + }; + }; + }; + input_supplier_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSupplier"]; + "multipart/form-data": components["schemas"]["PatchedSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSupplier"]; + }; + }; + }; + }; + input_supplier_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSupplier"]; + "multipart/form-data": components["schemas"]["PatchedSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSupplier"]; + }; + }; + }; + }; + input_supplier_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_supplier_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSupplier"]; + "multipart/form-data": components["schemas"]["PatchedSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedSupplier"]; + }; + }; + }; + }; + input_supplier_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Supplier"]; + }; + }; + }; + }; + input_supplier_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["Supplier"]; + "application/x-www-form-urlencoded": components["schemas"]["Supplier"]; + "multipart/form-data": components["schemas"]["Supplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Supplier"]; + }; + }; + }; + }; + input_supplier_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_supplier_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedSupplier"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedSupplier"]; + "multipart/form-data": components["schemas"]["PatchedSupplier"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["Supplier"]; + }; + }; + }; + }; + input_workorder_list: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedWorkOrder"][]; + }; + }; + }; + }; + input_workorder_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedWorkOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedWorkOrder"]; + "multipart/form-data": components["schemas"]["PatchedWorkOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedWorkOrder"]; + }; + }; + }; + }; + input_workorder_create: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedWorkOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedWorkOrder"]; + "multipart/form-data": components["schemas"]["PatchedWorkOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedWorkOrder"]; + }; + }; + }; + }; + input_workorder_destroy: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_workorder_partial_update: { + parameters: { + query?: never; + header?: never; + path?: never; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedWorkOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedWorkOrder"]; + "multipart/form-data": components["schemas"]["PatchedWorkOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["PatchedWorkOrder"]; + }; + }; + }; + }; + input_workorder_retrieve: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManufacturingOrder"]; + }; + }; + }; + }; + input_workorder_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["ManufacturingOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["ManufacturingOrder"]; + "multipart/form-data": components["schemas"]["ManufacturingOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManufacturingOrder"]; + }; + }; + }; + }; + input_workorder_destroy_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: never; + responses: { + /** @description No response body */ + 204: { + headers: { + [name: string]: unknown; + }; + content?: never; + }; + }; + }; + input_workorder_partial_update_2: { + parameters: { + query?: never; + header?: never; + path: { + id: string; + }; + cookie?: never; + }; + requestBody?: { + content: { + "application/json": components["schemas"]["PatchedManufacturingOrder"]; + "application/x-www-form-urlencoded": components["schemas"]["PatchedManufacturingOrder"]; + "multipart/form-data": components["schemas"]["PatchedManufacturingOrder"]; + }; + }; + responses: { + 200: { + headers: { + [name: string]: unknown; + }; + content: { + "application/json": components["schemas"]["ManufacturingOrder"]; + }; + }; + }; + }; +} diff --git a/frontend/lib/api.ts b/frontend/lib/api.ts new file mode 100644 index 0000000000..61224e0dff --- /dev/null +++ b/frontend/lib/api.ts @@ -0,0 +1,25 @@ +// Same-origin authed fetch shared by the REST/data-layer calls. Injects the +// Bearer JWT and cookies, and (for mutations) the Django double-submit CSRF +// header. Throws AuthError on a 401/403 response so callers can prompt sign-in; +// returns the (redirect-followed) Response otherwise — the caller inspects +// res.ok / res.url for everything else. +import { getToken } from "./auth"; +import { csrfToken } from "./csrf"; +import { AuthError } from "./errors"; + +export async function authedFetch( + path: string, + init: RequestInit = {}, +): Promise { + const token = await getToken(); + const headers = new Headers(init.headers); + headers.set("Authorization", `Bearer ${token}`); + const method = (init.method ?? "GET").toUpperCase(); + if (method !== "GET" && method !== "HEAD") { + const csrf = csrfToken(); + if (csrf) headers.set("X-CSRFToken", csrf); + } + const res = await fetch(path, { ...init, headers, credentials: "include" }); + if (res.status === 401 || res.status === 403) throw new AuthError(res.status); + return res; +} diff --git a/frontend/lib/apiSchema.ts b/frontend/lib/apiSchema.ts new file mode 100644 index 0000000000..e8fe66e7cf --- /dev/null +++ b/frontend/lib/apiSchema.ts @@ -0,0 +1,24 @@ +// Typed API surface for the SPA (Phase 0 — typed client). +// +// Re-exports types generated from the live OpenAPI schema: `api-types.ts` is +// produced by `pnpm gen:api` (openapi-typescript) from `generated/openapi.yaml`, +// which `frepplectl.py spectacular` emits from the Django app. Importing these +// type-couples the frontend to the API contract — a field/enum rename in Django +// surfaces here as a TypeScript error (CI regenerates + diff-checks the client). +// +// Scope note: the streaming OUTPUT endpoints (pivot reports, pegging) are plain +// Django views with no DRF serializer, so they're absent from the schema and keep +// their hand-written shapes (their columns are dynamic time-buckets anyway). The +// typed surface here is the DRF input/master-data CRUD the SPA mutates. + +import type { components } from "./api-types"; + +export type Schemas = components["schemas"]; + +/** The three operationplan order types exposed as DRF input lists. */ +export type ManufacturingOrder = Schemas["ManufacturingOrder"]; +export type PurchaseOrder = Schemas["PurchaseOrder"]; +export type DistributionOrder = Schemas["DistributionOrder"]; + +/** Order lifecycle status, straight from the API enum (StatusCa7Enum). */ +export type OrderStatus = Schemas["StatusCa7Enum"]; diff --git a/frontend/lib/auth.ts b/frontend/lib/auth.ts new file mode 100644 index 0000000000..84aa2bdcb7 --- /dev/null +++ b/frontend/lib/auth.ts @@ -0,0 +1,33 @@ +// Obtain a short-lived JWT for the logged-in user from the same-origin Django +// app (resolved Q4: same-origin + JWT). The session cookie authorizes +// /api/token/, which mints the JWT used for websocket (subprotocol carrier) and +// REST (Authorization header) auth. +import { AuthError, HttpError } from "./errors"; + +let cached: { token: string; exp: number } | null = null; +// In-flight de-dup: several hooks mount together and all call getToken() on a +// cold cache; share the one request instead of firing N identical /api/token/. +let inflight: Promise | null = null; + +export async function getToken(): Promise { + const now = Date.now() / 1000; + if (cached && cached.exp - 30 > now) return cached.token; + if (!inflight) { + inflight = (async () => { + const res = await fetch("/api/token/", { credentials: "include" }); + if (res.status === 401 || res.status === 403) throw new AuthError(res.status); + if (!res.ok) + throw new HttpError(res.status, `token fetch failed: ${res.status}`); + const data = (await res.json()) as { token: string; exp?: number }; + cached = { token: data.token, exp: data.exp ?? now + 3600 }; + return cached.token; + })().finally(() => { + inflight = null; + }); + } + return inflight; +} + +export function clearToken(): void { + cached = null; +} diff --git a/frontend/lib/csrf.test.ts b/frontend/lib/csrf.test.ts new file mode 100644 index 0000000000..fb0b01915d --- /dev/null +++ b/frontend/lib/csrf.test.ts @@ -0,0 +1,23 @@ +import { describe, it, expect } from "vitest"; +import { parseCsrf } from "./csrf"; + +describe("parseCsrf", () => { + it("extracts the csrftoken value from a cookie string", () => { + expect(parseCsrf("a=1; csrftoken=abc123; b=2")).toBe("abc123"); + expect(parseCsrf("csrftoken=xyz")).toBe("xyz"); + }); + + it("url-decodes the value", () => { + expect(parseCsrf("csrftoken=a%20b")).toBe("a b"); + }); + + it("does not match a lookalike cookie name", () => { + expect(parseCsrf("xcsrftoken=nope")).toBe(""); + expect(parseCsrf("mycsrftoken=nope")).toBe(""); + }); + + it("returns empty string when absent", () => { + expect(parseCsrf("foo=bar")).toBe(""); + expect(parseCsrf("")).toBe(""); + }); +}); diff --git a/frontend/lib/csrf.ts b/frontend/lib/csrf.ts new file mode 100644 index 0000000000..28908b4dc2 --- /dev/null +++ b/frontend/lib/csrf.ts @@ -0,0 +1,17 @@ +// Read Django's `csrftoken` cookie so same-origin POSTs to Django (WSGI) views +// pass CsrfViewMiddleware. Django uses double-submit: the cookie is sent +// automatically with `credentials: "include"`, and the same value must be +// echoed in the `X-CSRFToken` header. The cookie is not HttpOnly, so JS can +// read it. Returns "" when unavailable (e.g. SSR), and callers omit the header. + +// Pure parser (testable without a DOM). The `(?:^|;\s*)` anchor avoids matching +// a different cookie that merely ends in "csrftoken" (e.g. "xcsrftoken"). +export function parseCsrf(cookie: string): string { + const m = cookie.match(/(?:^|;\s*)csrftoken=([^;]+)/); + return m ? decodeURIComponent(m[1]) : ""; +} + +export function csrfToken(): string { + if (typeof document === "undefined") return ""; + return parseCsrf(document.cookie); +} diff --git a/frontend/lib/demand.ts b/frontend/lib/demand.ts new file mode 100644 index 0000000000..eb8a04f699 --- /dev/null +++ b/frontend/lib/demand.ts @@ -0,0 +1,24 @@ +// Demand screen config (Phase 3). Read-only GridPivot over /api/output/demand/ +// (demand.OverviewReport), rendered by the generic . +import type { PivotScreenConfig } from "@/components/PivotScreen"; +import type { PivotSeries } from "./pivot"; + +function demandTitle(s: PivotSeries): string { + return String(s.fields.item ?? s.key); +} + +export const DEMAND: PivotScreenConfig = { + endpoint: "/api/output/demand/", + keyField: "item", + eyebrow: "Demand planning", + title: "Demand", + subtitle: + "Sales orders, the supply that covers them and the remaining backlog per item across time buckets.", + emptyText: "No demand series.", + shown: [ + { measure: "demand", label: "Orders" }, + { measure: "supply", label: "Supply" }, + { measure: "backlog", label: "Backlog" }, + ], + titleOf: demandTitle, +}; diff --git a/frontend/lib/errors.ts b/frontend/lib/errors.ts new file mode 100644 index 0000000000..c472b7e558 --- /dev/null +++ b/frontend/lib/errors.ts @@ -0,0 +1,25 @@ +// Typed network errors so callers can branch on auth failures without +// string-matching status codes out of error messages. +export class HttpError extends Error { + status: number; + constructor(status: number, message?: string) { + super(message ?? `HTTP ${status}`); + this.name = "HttpError"; + this.status = status; + } +} + +export class AuthError extends HttpError { + constructor(status = 401, message?: string) { + super(status, message ?? "authentication required"); + this.name = "AuthError"; + } +} + +// True for "you are not signed in" failures (401/403), wherever they surfaced. +export function isAuthError(e: unknown): boolean { + return ( + e instanceof AuthError || + (e instanceof HttpError && (e.status === 401 || e.status === 403)) + ); +} diff --git a/frontend/lib/forecast.test.ts b/frontend/lib/forecast.test.ts new file mode 100644 index 0000000000..a6b7a47987 --- /dev/null +++ b/frontend/lib/forecast.test.ts @@ -0,0 +1,166 @@ +import { describe, it, expect } from "vitest"; +import { + pivotForecast, + bucketNames, + parseForecast, + buildOverrideMessage, + buildBulkOverrideMessage, + toChartRows, + MEASURES, +} from "./forecast"; + +const resp = { + total: 1, + page: 1, + records: 2, + rows: [ + { + item: "itemA", + location: "loc1", + customer: "custX", + // measure order: orderstotal, ordersopen, ordersadjustment, forecastbaseline, + // forecastoverride, forecasttotal, forecastnet, forecastconsumed + "Jan 26": [10, 5, 0, 8, 2, 8, 10, 0], + "Feb 26": [12, 6, 1, 9, 0, 9, 9, 3], + }, + { + item: "itemB", + location: "loc1", + customer: "custY", + "Jan 26": [0, 0, 0, 4, null, 4, 4, 0], + "Feb 26": [1, 1, 0, 5, 0, 5, 5, 0], + }, + ], +}; + +describe("pivotForecast", () => { + it("maps per-bucket arrays to named measures", () => { + const series = pivotForecast(resp); + expect(series).toHaveLength(2); + expect(series[0].key).toBe("itemA"); + expect(series[0].fields.location).toBe("loc1"); + expect(series[0].buckets["Jan 26"].forecastnet).toBe(10); + expect(series[0].buckets["Jan 26"].forecastoverride).toBe(2); + expect(series[1].buckets["Feb 26"].forecastbaseline).toBe(5); + }); + + it("preserves nulls as null (not 0)", () => { + const series = pivotForecast(resp); + expect(series[1].buckets["Jan 26"].forecastoverride).toBeNull(); + }); + + it("does not truncate large result sets (fc-no-truncation)", () => { + const rows = Array.from({ length: 500 }, (_, i) => ({ + item: `item${i}`, + "Jan 26": [1, 2, 3, 4, 5, 6, 7, 8], + })); + const series = pivotForecast({ total: 1, page: 1, records: 500, rows }); + expect(series).toHaveLength(500); + }); + + it("treats scalar fields as identity and arrays as buckets", () => { + const series = pivotForecast(resp); + expect(Object.keys(series[0].fields).sort()).toEqual([ + "customer", + "item", + "location", + ]); + expect(Object.keys(series[0].buckets)).toEqual(["Jan 26", "Feb 26"]); + }); +}); + +describe("bucketNames", () => { + it("returns the ordered union of bucket names", () => { + expect(bucketNames(pivotForecast(resp))).toEqual(["Jan 26", "Feb 26"]); + }); +}); + +describe("toChartRows", () => { + it("flattens a series into orders/baseline/net points per bucket", () => { + const s = pivotForecast(resp)[0]; + const rows = toChartRows(s, [{ name: "Jan 26" }, { name: "Feb 26" }]); + expect(rows).toEqual([ + { bucket: "Jan 26", orders: 10, baseline: 8, net: 10 }, + { bucket: "Feb 26", orders: 12, baseline: 9, net: 9 }, + ]); + }); +}); + +describe("MEASURES", () => { + it("has the 8 forecast measures in crosses order", () => { + expect(MEASURES[4]).toBe("forecastoverride"); + expect(MEASURES).toHaveLength(8); + }); +}); + +describe("parseForecast (enriched response)", () => { + const enriched = { + measures: [...MEASURES], + buckets: [ + { name: "Jan 26", startdate: "2026-01-01", enddate: "2026-02-01" }, + { name: "Feb 26", startdate: "2026-02-01", enddate: "2026-03-01" }, + ], + data: resp, + }; + + it("uses the server-provided measure order and bucket dates", () => { + const { measures, buckets, series } = parseForecast(enriched); + expect(measures[4]).toBe("forecastoverride"); + expect(buckets[0]).toEqual({ + name: "Jan 26", + startdate: "2026-01-01", + enddate: "2026-02-01", + }); + expect(series).toHaveLength(2); + expect(series[0].buckets["Jan 26"].forecastnet).toBe(10); + }); + + it("falls back to default measures/buckets when absent", () => { + const { measures, buckets } = parseForecast({ data: resp }); + expect(measures).toEqual(MEASURES); + expect(buckets.map((b) => b.name)).toEqual(["Jan 26", "Feb 26"]); + }); +}); + +describe("buildOverrideMessage", () => { + it("emits the ForecastService payload for one cell edit", () => { + const series = pivotForecast(resp)[0]; + const bucket = { name: "Jan 26", startdate: "2026-01-01", enddate: "2026-02-01" }; + expect(buildOverrideMessage(series, bucket, 42)).toEqual({ + item: "itemA", + location: "loc1", + customer: "custX", + buckets: [ + { + bucket: "Jan 26", + startdate: "2026-01-01", + enddate: "2026-02-01", + forecastoverride: 42, + }, + ], + }); + }); + + it("carries a null override (clearing the cell)", () => { + const series = pivotForecast(resp)[0]; + const bucket = { name: "Feb 26", startdate: null, enddate: null }; + const msg = buildOverrideMessage(series, bucket, null); + expect(msg.buckets[0].forecastoverride).toBeNull(); + }); + + it("builds a bulk message with all edited buckets", () => { + const series = pivotForecast(resp)[0]; + const msg = buildBulkOverrideMessage(series, [ + { bucket: { name: "Jan 26", startdate: "a", enddate: "b" }, value: 1 }, + { bucket: { name: "Feb 26", startdate: "c", enddate: "d" }, value: 2 }, + ]); + expect(msg.item).toBe("itemA"); + expect(msg.buckets).toHaveLength(2); + expect(msg.buckets[1]).toEqual({ + bucket: "Feb 26", + startdate: "c", + enddate: "d", + forecastoverride: 2, + }); + }); +}); diff --git a/frontend/lib/forecast.ts b/frontend/lib/forecast.ts new file mode 100644 index 0000000000..5f53f79c61 --- /dev/null +++ b/frontend/lib/forecast.ts @@ -0,0 +1,148 @@ +// Forecast data layer (Phase 1B). Typed wrapper over the generic GridPivot +// parser in ./pivot — the forecast OUTPUT report is a GridPivot, so the actual +// pivot logic (scalars->fields, arrays->per-bucket measure cells) lives there and +// is shared with the other pivot screens (inventory, …). +import { + pivotRows, + bucketOrder, + type PivotRowResponse, + type BucketMeta, +} from "./pivot"; + +export const MEASURES = [ + "orderstotal", + "ordersopen", + "ordersadjustment", + "forecastbaseline", + "forecastoverride", + "forecasttotal", + "forecastnet", + "forecastconsumed", +] as const; + +export type Measure = (typeof MEASURES)[number]; + +// Measures a user can edit in the grid; everything else is computed/read-only. +export const EDITABLE_MEASURES: ReadonlySet = new Set([ + "forecastoverride", +]); + +export type ForecastPivotResponse = PivotRowResponse; + +export type ForecastCell = Partial>; + +export type ForecastSeries = { + key: string; // first row-field value (the series identity) + fields: Record; // item/location/customer/... + buckets: Record; // bucketName -> measures +}; + +const DEFAULT_ROW_FIELDS = ["item", "location", "customer"]; + +// Typed wrapper over the generic pivotRows (./pivot): the forecast key is the +// first row-field (item). NO top-300 truncation (fc-no-truncation). +export function pivotForecast( + resp: ForecastPivotResponse, + measures: readonly Measure[] = MEASURES, + rowFields: string[] = DEFAULT_ROW_FIELDS, +): ForecastSeries[] { + return pivotRows(resp, measures, rowFields[0]) as ForecastSeries[]; +} + +// The ordered bucket names across all series (union, preserving first-seen order). +export function bucketNames(series: ForecastSeries[]): string[] { + return bucketOrder(series); +} + +// Flatten one series into chart rows (one point per bucket) for plotting +// orders / baseline / net over time. Pure, unit-tested. +export type ForecastChartRow = { + bucket: string; + orders: number | null; + baseline: number | null; + net: number | null; +}; + +export function toChartRows( + series: ForecastSeries, + buckets: { name: string }[], +): ForecastChartRow[] { + return buckets.map((b) => { + const cell = series.buckets[b.name] ?? {}; + return { + bucket: b.name, + orders: cell.orderstotal ?? null, + baseline: cell.forecastbaseline ?? null, + net: cell.forecastnet ?? null, + }; + }); +} + +// The enriched forecast response (Phase 1B): the report's pivot object under +// `data`, plus the measure order and bucket dates the editor needs. +export type ForecastBucketMeta = BucketMeta; + +export type ForecastResponse = { + measures?: Measure[]; + buckets?: ForecastBucketMeta[]; + data: ForecastPivotResponse; +}; + +export function parseForecast(resp: ForecastResponse): { + measures: readonly Measure[]; + buckets: ForecastBucketMeta[]; + series: ForecastSeries[]; +} { + const measures = resp.measures?.length ? resp.measures : MEASURES; + const series = pivotForecast(resp.data, measures); + const buckets = + resp.buckets && resp.buckets.length + ? resp.buckets + : bucketNames(series).map((name) => ({ + name, + startdate: null, + enddate: null, + })); + return { measures, buckets, series }; +} + +export type OverrideMessage = { + item: string | null; + location: string | null; + customer: string | null; + buckets: { + bucket: string; + startdate: string | null; + enddate: string | null; + forecastoverride: number | null; + }[]; +}; + +// Build the ForecastService (/forecast/detail/) message for a set of edits: +// {item, location, customer, buckets:[{startdate, enddate, bucket, forecastoverride}]}. +export function buildBulkOverrideMessage( + series: ForecastSeries, + edits: { bucket: ForecastBucketMeta; value: number | null }[], +): OverrideMessage { + const f = series.fields; + return { + item: (f.item as string) ?? null, + location: (f.location as string) ?? null, + customer: (f.customer as string) ?? null, + buckets: edits.map((e) => ({ + bucket: e.bucket.name, + startdate: e.bucket.startdate, + enddate: e.bucket.enddate, + forecastoverride: e.value, + })), + }; +} + +// One-cell convenience wrapper. +export function buildOverrideMessage( + series: ForecastSeries, + bucket: ForecastBucketMeta, + value: number | null, +): OverrideMessage { + return buildBulkOverrideMessage(series, [{ bucket, value }]); +} diff --git a/frontend/lib/forecastEdit.test.ts b/frontend/lib/forecastEdit.test.ts new file mode 100644 index 0000000000..66d0cdd274 --- /dev/null +++ b/frontend/lib/forecastEdit.test.ts @@ -0,0 +1,52 @@ +import { describe, it, expect } from "vitest"; +import { + applyFill, + applyPercent, + applyCopyFirst, + detectOutliers, +} from "./forecastEdit"; + +describe("applyFill", () => { + it("sets every slot to the value", () => { + expect(applyFill(7, 3)).toEqual([7, 7, 7]); + expect(applyFill(null, 2)).toEqual([null, null]); + }); +}); + +describe("applyPercent", () => { + it("scales non-null values, preserves nulls", () => { + expect(applyPercent([100, null, 50], 10)).toEqual([110, null, 55]); + expect(applyPercent([100], -5)).toEqual([95]); + }); + it("rounds to avoid floating-point noise", () => { + expect(applyPercent([10], 10)).toEqual([11]); + expect(applyPercent([3], 33.3333)).toEqual([4]); + }); +}); + +describe("applyCopyFirst", () => { + it("copies the first non-null value across the row", () => { + expect(applyCopyFirst([null, 5, 9])).toEqual([5, 5, 5]); + expect(applyCopyFirst([null, null])).toEqual([null, null]); + }); +}); + +describe("detectOutliers", () => { + it("flags an extreme value via the IQR rule", () => { + const flags = detectOutliers([10, 11, 9, 10, 200]); + expect(flags[4]).toBe(true); + expect(flags.slice(0, 4)).toEqual([false, false, false, false]); + }); + it("flags nothing with fewer than 4 points", () => { + expect(detectOutliers([1, 100, 2])).toEqual([false, false, false]); + }); + it("never flags nulls", () => { + const flags = detectOutliers([10, 11, 9, 10, null]); + expect(flags[4]).toBe(false); + }); + it("flags no outliers in a tight series", () => { + expect(detectOutliers([10, 11, 9, 10, 11, 9])).toEqual( + [false, false, false, false, false, false], + ); + }); +}); diff --git a/frontend/lib/forecastEdit.ts b/frontend/lib/forecastEdit.ts new file mode 100644 index 0000000000..b9b42be1e4 --- /dev/null +++ b/frontend/lib/forecastEdit.ts @@ -0,0 +1,53 @@ +// Pure forecast-edit helpers (Phase 1B-3): bulk operations on a row of override +// values and outlier detection. Kept free of React/DOM so they are unit-tested +// directly (forecastEdit.test.ts). + +function round(n: number): number { + // Forecast quantities are whole-ish; keep 3 decimals to avoid fp noise. + return Math.round(n * 1000) / 1000; +} + +// Fill every slot with one value (bulk "set"). +export function applyFill(value: number | null, count: number): (number | null)[] { + return Array.from({ length: count }, () => value); +} + +// Scale each non-null value by a percentage: +10 -> x1.1, -5 -> x0.95. +export function applyPercent( + values: (number | null)[], + pct: number, +): (number | null)[] { + const factor = 1 + pct / 100; + return values.map((v) => (v == null ? null : round(v * factor))); +} + +// Copy the first non-null value across the whole row (bulk "fill right"). +export function applyCopyFirst(values: (number | null)[]): (number | null)[] { + const first = values.find((v) => v != null); + const v = first == null ? null : first; + return values.map(() => v); +} + +function quantile(sorted: number[], q: number): number { + if (sorted.length === 0) return NaN; + const pos = (sorted.length - 1) * q; + const base = Math.floor(pos); + const rest = pos - base; + return sorted[base + 1] !== undefined + ? sorted[base] + rest * (sorted[base + 1] - sorted[base]) + : sorted[base]; +} + +// Flag outliers using the Tukey IQR rule (robust to non-normal demand). Needs at +// least 4 points; otherwise nothing is flagged. Nulls are never outliers. +export function detectOutliers(values: (number | null)[]): boolean[] { + const nums = values.filter((v): v is number => v != null); + if (nums.length < 4) return values.map(() => false); + const sorted = [...nums].sort((a, b) => a - b); + const q1 = quantile(sorted, 0.25); + const q3 = quantile(sorted, 0.75); + const iqr = q3 - q1; + const lo = q1 - 1.5 * iqr; + const hi = q3 + 1.5 * iqr; + return values.map((v) => v != null && (v < lo || v > hi)); +} diff --git a/frontend/lib/forecastSave.ts b/frontend/lib/forecastSave.ts new file mode 100644 index 0000000000..aed99a18bd --- /dev/null +++ b/frontend/lib/forecastSave.ts @@ -0,0 +1,41 @@ +import { authedFetch } from "./api"; +import { HttpError } from "./errors"; +import { scenarioPrefix } from "./ws"; +import { + buildOverrideMessage, + buildBulkOverrideMessage, + type OverrideMessage, + type ForecastSeries, + type ForecastBucketMeta, +} from "./forecast"; + +async function post(message: OverrideMessage, scenario: string): Promise { + const res = await authedFetch(`${scenarioPrefix(scenario)}/forecast/detail/`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(message), + }); + if (!res.ok) + throw new HttpError(res.status, `forecast save failed: ${res.status}`); +} + +// Persist one override edit. The engine updates the override and re-nets; callers +// reload to pick up the new forecastnet. +export async function saveOverride( + series: ForecastSeries, + bucket: ForecastBucketMeta, + value: number | null, + scenario = "", +): Promise { + await post(buildOverrideMessage(series, bucket, value), scenario); +} + +// Persist a bulk edit (fill / +-% across a row) in a single request. +export async function saveBulkOverrides( + series: ForecastSeries, + edits: { bucket: ForecastBucketMeta; value: number | null }[], + scenario = "", +): Promise { + if (edits.length === 0) return; + await post(buildBulkOverrideMessage(series, edits), scenario); +} diff --git a/frontend/lib/inventory.ts b/frontend/lib/inventory.ts new file mode 100644 index 0000000000..72ac801292 --- /dev/null +++ b/frontend/lib/inventory.ts @@ -0,0 +1,31 @@ +// Inventory/Buffer screen config (Phase 3). Read-only GridPivot over +// /api/output/inventory/ (buffer.OverviewReport), rendered by the generic +// . The report exposes 40+ measures; we show a core subset (the +// full set ships in the response envelope for a later column picker). +import type { PivotScreenConfig } from "@/components/PivotScreen"; +import type { PivotSeries } from "./pivot"; + +// Buffer series label: "item @ location" (+ batch when present). +function inventoryTitle(s: PivotSeries): string { + const f = s.fields; + const base = [f.item, f.location].filter(Boolean).join(" @ "); + return f.batch ? `${base} / ${f.batch}` : base || s.key; +} + +export const INVENTORY: PivotScreenConfig = { + endpoint: "/api/output/inventory/", + keyField: "buffer", + eyebrow: "Supply", + title: "Inventory", + subtitle: + "On-hand, safety stock and material flow per buffer across time buckets, from the latest plan.", + emptyText: "No inventory buffers.", + shown: [ + { measure: "startoh", label: "Start OH" }, + { measure: "safetystock", label: "Safety stock" }, + { measure: "consumed", label: "Consumed" }, + { measure: "produced", label: "Produced" }, + { measure: "endoh", label: "End OH" }, + ], + titleOf: inventoryTitle, +}; diff --git a/frontend/lib/orders.test.ts b/frontend/lib/orders.test.ts new file mode 100644 index 0000000000..8f2814b99e --- /dev/null +++ b/frontend/lib/orders.test.ts @@ -0,0 +1,41 @@ +import { describe, it, expect } from "vitest"; +import { canEditOrder, normalizeChange, ORDER_TABS } from "./orders"; + +describe("canEditOrder", () => { + it("allows proposed/approved/confirmed, locks completed/closed", () => { + expect(canEditOrder({ status: "proposed" })).toBe(true); + expect(canEditOrder({ status: "confirmed" })).toBe(true); + expect(canEditOrder({ status: "completed" })).toBe(false); + expect(canEditOrder({ status: "closed" })).toBe(false); + }); + it("is case-insensitive and tolerant of a missing status", () => { + expect(canEditOrder({ status: "CLOSED" })).toBe(false); + expect(canEditOrder({})).toBe(true); + }); +}); + +describe("normalizeChange", () => { + it("expands a date-only edit to a naive midnight timestamp", () => { + expect(normalizeChange("startdate", "2026-06-26")).toBe("2026-06-26T00:00:00"); + expect(normalizeChange("enddate", "2026-07-01")).toBe("2026-07-01T00:00:00"); + }); + it("passes a full datetime and non-date fields through unchanged", () => { + expect(normalizeChange("startdate", "2026-06-26T08:00:00")).toBe( + "2026-06-26T08:00:00", + ); + expect(normalizeChange("quantity", "50")).toBe("50"); + expect(normalizeChange("status", "confirmed")).toBe("confirmed"); + }); +}); + +describe("ORDER_TABS", () => { + it("each tab has editable status/date/qty columns + a detail endpoint", () => { + for (const tab of ORDER_TABS) { + expect(tab.endpoint).toMatch(/^\/api\/input\/.+\/$/); + const status = tab.columns.find((c) => c.key === "status"); + expect(status?.pill).toBe(true); + expect(status?.edit).toBe("select"); + expect(tab.columns.find((c) => c.key === "quantity")?.edit).toBe("number"); + } + }); +}); diff --git a/frontend/lib/orders.ts b/frontend/lib/orders.ts new file mode 100644 index 0000000000..9bd1186762 --- /dev/null +++ b/frontend/lib/orders.ts @@ -0,0 +1,125 @@ +// Config + write helpers for the Orders screen (Phase 3 — MO / PO / DO summaries, +// now inline-editable). Each order type is a DRF input list; they share a core +// (reference / item / status / dates / quantity) plus one type-specific column. + +import { authedFetch } from "./api"; +import { HttpError } from "./errors"; +import { fmtDate, fmtNum, type Column, type RecordRow } from "./records"; +import type { OrderStatus } from "./apiSchema"; + +// Statuses an order can move through; executed ones are locked from editing. +// `satisfies readonly OrderStatus[]` type-couples this list to the API's status +// enum (Phase 0) — a renamed/removed status in Django breaks this typecheck. +export const ORDER_STATUSES = [ + "proposed", + "approved", + "confirmed", + "completed", + "closed", +] as const satisfies readonly OrderStatus[]; +const LOCKED = new Set(["completed", "closed"]); + +// The editable core columns: status (pill + select), the two dates, the quantity. +const CORE_TAIL: Column[] = [ + { key: "status", label: "Status", pill: true, edit: "select", options: ORDER_STATUSES }, + { key: "startdate", label: "Start", format: fmtDate, edit: "date" }, + { key: "enddate", label: "End", format: fmtDate, edit: "date" }, + { key: "quantity", label: "Qty", align: "right", format: fmtNum, edit: "number" }, +]; + +export type OrderTab = { + key: string; + label: string; + endpoint: string; + columns: Column[]; +}; + +export const ORDER_TABS: OrderTab[] = [ + { + key: "MO", + label: "Manufacturing", + endpoint: "/api/input/manufacturingorder/", + columns: [ + { key: "reference", label: "Reference" }, + { key: "item", label: "Item" }, + { key: "operation", label: "Operation" }, + ...CORE_TAIL, + ], + }, + { + key: "PO", + label: "Purchase", + endpoint: "/api/input/purchaseorder/", + columns: [ + { key: "reference", label: "Reference" }, + { key: "item", label: "Item" }, + { key: "supplier", label: "Supplier" }, + { key: "location", label: "Location" }, + ...CORE_TAIL, + ], + }, + { + key: "DO", + label: "Distribution", + endpoint: "/api/input/distributionorder/", + columns: [ + { key: "reference", label: "Reference" }, + { key: "item", label: "Item" }, + { key: "origin", label: "Origin" }, + { key: "destination", label: "Destination" }, + ...CORE_TAIL, + ], + }, +]; + +// Executed orders (completed/closed) are read-only — the engine won't move them. +export function canEditOrder(row: RecordRow): boolean { + return !LOCKED.has(String(row.status ?? "").toLowerCase()); +} + +// A date edit comes from a as "YYYY-MM-DD"; the API wants a +// naive ISO timestamp. Normalise date-only edits to midnight; pass others through. +export function normalizeChange(key: string, value: unknown): unknown { + if ((key === "startdate" || key === "enddate") && typeof value === "string") { + return value.length === 10 ? `${value}T00:00:00` : value; + } + return value; +} + +// PATCH the changed fields of one order. `listEndpoint` is the tab's list URL; +// the detail resource is `//`. Throws AuthError/HttpError. +export async function patchOrder( + listEndpoint: string, + reference: string, + changes: Record, + scenario = "", +): Promise { + const prefix = scenario ? `/${scenario}` : ""; + const body: Record = {}; + for (const [k, v] of Object.entries(changes)) body[k] = normalizeChange(k, v); + const res = await authedFetch( + `${prefix}${listEndpoint}${encodeURIComponent(reference)}/`, + { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + }, + ); + if (!res.ok) throw new HttpError(res.status, `save failed: ${res.status}`); +} + +// DELETE one order. Throws AuthError/HttpError on failure. +export async function deleteOrder( + listEndpoint: string, + reference: string, + scenario = "", +): Promise { + const prefix = scenario ? `/${scenario}` : ""; + const res = await authedFetch( + `${prefix}${listEndpoint}${encodeURIComponent(reference)}/`, + { method: "DELETE" }, + ); + // DRF returns 204 No Content on a successful delete. + if (!res.ok && res.status !== 204) + throw new HttpError(res.status, `delete failed: ${res.status}`); +} diff --git a/frontend/lib/pegging.test.ts b/frontend/lib/pegging.test.ts new file mode 100644 index 0000000000..1c63753866 --- /dev/null +++ b/frontend/lib/pegging.test.ts @@ -0,0 +1,148 @@ +import { describe, it, expect } from "vitest"; +import { + parsePegging, + fractionOf, + parseEngineDate, + axisTicks, + downstreamChain, + type PeggingRow, +} from "./pegging"; + +// A pre-order pegging tree: delivery(1) -> make(2) -> [purchase(3), ink(3)], +// then a second make(2). The `r()` helper fills the unused row fields. +function r(id: string, depth: number): PeggingRow { + return { id, depth, operation: id, type: "MO", item: null, quantity: 0, bars: [] }; +} +const TREE: PeggingRow[] = [ + r("delivery", 1), + r("make", 2), + r("purchase", 3), + r("ink", 3), + r("make2", 2), +]; + +const SAMPLE = { + window: { + start: "2026-05-28T00:00:00", + end: "2026-07-06T00:00:00", + due: "2026-06-17T00:00:00", + current: "2026-06-17T00:00:00", + }, + data: { + total: 1, + rows: [ + { + id: "product/39", + depth: "1", + operation: "Ship product @ factory 1", + type: "DLVR", + item: "product", + quantity: "100.0", + operationplans: [ + { + reference: "39", + operation: "Ship product @ factory 1", + quantity: "50.00000000", + startdate: "2026-06-26 01:00:00", + enddate: "2026-06-26 01:00:00", + status: "proposed", + type: "DLVR", + color: null, + criticality: 0, + item: "product", + location: "factory 1", + }, + ], + }, + ], + }, +}; + +describe("parsePegging", () => { + it("parses the window + tree rows + bars with numeric coercion", () => { + const p = parsePegging(SAMPLE); + expect(p.window.start).toBe("2026-05-28T00:00:00"); + expect(p.window.due).toBe("2026-06-17T00:00:00"); + expect(p.rows).toHaveLength(1); + const row = p.rows[0]; + expect(row.depth).toBe(1); // "1" -> 1 + expect(row.quantity).toBe(100); + expect(row.type).toBe("DLVR"); + expect(row.bars).toHaveLength(1); + expect(row.bars[0].reference).toBe("39"); + expect(row.bars[0].quantity).toBe(50); + expect(row.bars[0].status).toBe("proposed"); + expect(row.bars[0].criticality).toBe(0); + }); + + it("degrades to an empty tree on null/legacy bodies (no plan yet)", () => { + expect(parsePegging(null).rows).toEqual([]); + expect(parsePegging(undefined).window.start).toBeNull(); + expect(parsePegging({ total: 0 } as never).rows).toEqual([]); + }); +}); + +describe("fractionOf", () => { + const startMs = parseEngineDate("2026-05-28T00:00:00"); + const endMs = parseEngineDate("2026-07-06T00:00:00"); + + it("places a date as a 0..1 fraction of the window", () => { + // due (Jun 17) is partway through May28..Jul6 + const f = fractionOf("2026-06-17T00:00:00", startMs, endMs)!; + expect(f).toBeGreaterThan(0.4); + expect(f).toBeLessThan(0.6); + }); + + it("handles the engine's space-separated naive timestamps", () => { + const f = fractionOf("2026-06-26 01:00:00", startMs, endMs)!; + expect(f).toBeGreaterThan(0.7); + expect(f).toBeLessThan(0.8); + }); + + it("clamps out-of-window dates and rejects a degenerate window", () => { + expect(fractionOf("2020-01-01T00:00:00", startMs, endMs)).toBe(0); + expect(fractionOf("2030-01-01T00:00:00", startMs, endMs)).toBe(1); + expect(fractionOf("2026-06-01T00:00:00", endMs, startMs)).toBeNull(); + expect(fractionOf("", startMs, endMs)).toBeNull(); + }); +}); + +describe("axisTicks", () => { + it("returns day-snapped ticks across the window", () => { + const ticks = axisTicks(SAMPLE.window, 6); + expect(ticks.length).toBeGreaterThanOrEqual(5); + expect(ticks[0].fraction).toBe(0); + expect(ticks[ticks.length - 1].fraction).toBeLessThanOrEqual(1); + expect(typeof ticks[0].label).toBe("string"); + }); + + it("returns no ticks for a degenerate/empty window", () => { + expect(axisTicks({ start: null, end: null, due: null, current: null })).toEqual( + [], + ); + }); +}); + +describe("downstreamChain", () => { + it("returns the moved row + its ancestors toward the delivery (depth 1)", () => { + // purchase(3) -> nearest preceding make(2) -> delivery(1) + const c = downstreamChain(TREE, 2); + expect([...c].sort()).toEqual(["delivery", "make", "purchase"]); + }); + + it("does not include later siblings or unrelated branches", () => { + // 'ink'(3) ancestors are make(2) + delivery(1); make2 is a later sibling, out. + const c = downstreamChain(TREE, 3); + expect(c.has("make2")).toBe(false); + expect([...c].sort()).toEqual(["delivery", "ink", "make"]); + }); + + it("a depth-1 row (the delivery) affects only itself", () => { + expect([...downstreamChain(TREE, 0)]).toEqual(["delivery"]); + }); + + it("returns empty for an out-of-range index", () => { + expect(downstreamChain(TREE, -1).size).toBe(0); + expect(downstreamChain(TREE, 99).size).toBe(0); + }); +}); diff --git a/frontend/lib/pegging.ts b/frontend/lib/pegging.ts new file mode 100644 index 0000000000..ca2475bd60 --- /dev/null +++ b/frontend/lib/pegging.ts @@ -0,0 +1,155 @@ +// Data layer for the Demand Pegging Gantt (Phase 3-D). Parses the enriched +// /api/output/pegging// response into a typed tree of pegging rows + +// timeline bars, and provides the date->fraction geometry the Gantt renders +// with. Pure + framework-free so it unit-tests without React (see pegging.test.ts). + +// The absolute horizon + marker dates the server computes (ISO strings). The +// bare report stream drops these hidden columns; PeggingJSONView prepends them. +export type PeggingWindow = { + start: string | null; + end: string | null; + due: string | null; // the demand's due date (the red marker) + current: string | null; // "now" / last-plan date (the neutral marker) +}; + +// One scheduled operationplan = one bar on a lane. +export type PeggingBar = { + reference: string; + operation: string; + start: string; // raw "YYYY-MM-DD HH:MM:SS" (engine output, naive) + end: string; + quantity: number; + status: string; // proposed | approved | confirmed | completed | closed + type: string; // MO | WO | PO | DO | DLVR | STCK + criticality: number; // 0 = on time; higher = more at-risk (shown in tooltip) +}; + +// One node of the demand's supply-chain tree = one Gantt row (lane of bars). +export type PeggingRow = { + id: string; + depth: number; // 1 = the demand's delivery; deeper = upstream supply + operation: string; + type: string; + item: string | null; + quantity: number; // required quantity pegged to the demand + bars: PeggingBar[]; +}; + +export type Pegging = { + window: PeggingWindow; + rows: PeggingRow[]; +}; + +// The supply-chain rows whose timing depends on the operation at `rowIndex` — its +// chain toward the demand delivery (Phase 3-D3). The pegging tree is pre-order +// with depth 1 = the delivery and deeper = upstream supply, so the "downstream" +// of an upstream op is its ancestors: walking back from `rowIndex`, the nearest +// preceding row at each smaller depth, down to depth 1. Rescheduling the op may +// push those later — a re-plan computes the exact result; this just flags WHICH +// rows are affected. Returns a Set of row ids (incl. the moved row itself). +export function downstreamChain(rows: PeggingRow[], rowIndex: number): Set { + const affected = new Set(); + if (rowIndex < 0 || rowIndex >= rows.length) return affected; + affected.add(rows[rowIndex].id); + let wantDepth = rows[rowIndex].depth - 1; + for (let i = rowIndex - 1; i >= 0 && wantDepth >= 1; i--) { + if (rows[i].depth === wantDepth) { + affected.add(rows[i].id); + wantDepth -= 1; + } + } + return affected; +} + +function num(v: unknown): number { + const n = typeof v === "number" ? v : parseFloat(String(v)); + return Number.isFinite(n) ? n : 0; +} + +// The enriched endpoint's shape (loose - tolerate missing/streamed-empty bodies). +type RawBar = Record; +type RawRow = { operationplans?: RawBar[] } & Record; +type RawPegging = { + window?: Partial; + data?: { rows?: RawRow[] }; +}; + +// Parse the /api/output/pegging// JSON into the typed tree. Tolerant of +// an empty/legacy (unwrapped) body so the screen degrades to "no plan" instead +// of throwing. +export function parsePegging(json: RawPegging | null | undefined): Pegging { + const window: PeggingWindow = { + start: json?.window?.start ?? null, + end: json?.window?.end ?? null, + due: json?.window?.due ?? null, + current: json?.window?.current ?? null, + }; + const rawRows = json?.data?.rows ?? []; + const rows: PeggingRow[] = rawRows.map((r) => ({ + id: String(r.id ?? ""), + depth: num(r.depth), + operation: String(r.operation ?? ""), + type: String(r.type ?? ""), + item: (r.item as string) ?? null, + quantity: num(r.quantity), + bars: (r.operationplans ?? []).map((b) => ({ + reference: String(b.reference ?? ""), + operation: String(b.operation ?? ""), + start: String(b.startdate ?? ""), + end: String(b.enddate ?? ""), + quantity: num(b.quantity), + status: String(b.status ?? ""), + type: String(b.type ?? ""), + criticality: num(b.criticality), + })), + })); + return { window, rows }; +} + +// Engine timestamps come as "YYYY-MM-DD HH:MM:SS" (naive); the window comes as +// ISO. Normalize both to epoch ms so positions are consistent. Returns NaN for +// blanks so callers can skip un-dated bars. +export function parseEngineDate(s: string | null | undefined): number { + if (!s) return NaN; + return new Date(s.includes("T") ? s : s.replace(" ", "T")).getTime(); +} + +// Position of a date within [start,end] as a 0..1 fraction, clamped. Returns +// null when the window is degenerate/unparseable (caller hides the timeline). +export function fractionOf( + date: string | null | undefined, + startMs: number, + endMs: number, +): number | null { + const span = endMs - startMs; + if (!Number.isFinite(span) || span <= 0) return null; + const t = parseEngineDate(date ?? ""); + if (!Number.isFinite(t)) return null; + return Math.max(0, Math.min(1, (t - startMs) / span)); +} + +// Build ~`count` evenly spaced axis ticks across the window, snapped to day +// boundaries, as {fraction, label} for the Gantt header. +export function axisTicks( + window: PeggingWindow, + count = 6, +): { fraction: number; label: string }[] { + const startMs = parseEngineDate(window.start); + const endMs = parseEngineDate(window.end); + const span = endMs - startMs; + if (!Number.isFinite(span) || span <= 0) return []; + const day = 86_400_000; + const rawStep = span / count; + const step = Math.max(day, Math.round(rawStep / day) * day); // whole days + const ticks: { fraction: number; label: string }[] = []; + for (let t = startMs; t <= endMs + 1; t += step) { + ticks.push({ + fraction: Math.max(0, Math.min(1, (t - startMs) / span)), + label: new Date(t).toLocaleDateString(undefined, { + month: "short", + day: "numeric", + }), + }); + } + return ticks; +} diff --git a/frontend/lib/pivot.test.ts b/frontend/lib/pivot.test.ts new file mode 100644 index 0000000000..b5dfb7f8a5 --- /dev/null +++ b/frontend/lib/pivot.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect } from "vitest"; +import { pivotRows, bucketOrder, parsePivot } from "./pivot"; + +describe("pivotRows", () => { + it("splits scalar fields from per-bucket measure arrays", () => { + const rows = [{ item: "A", location: "L", Jan: [1, 2], Feb: [3, 4] }]; + const s = pivotRows({ rows }, ["startoh", "endoh"], "item"); + expect(s).toHaveLength(1); + expect(s[0].key).toBe("A"); + expect(s[0].fields).toEqual({ item: "A", location: "L" }); + expect(s[0].buckets.Jan).toEqual({ startoh: 1, endoh: 2 }); + expect(s[0].buckets.Feb).toEqual({ startoh: 3, endoh: 4 }); + }); + + it("maps missing/null slots to null and coerces numeric strings", () => { + const s = pivotRows({ rows: [{ k: "x", B: ["5", null] }] }, ["a", "b"], "k"); + expect(s[0].buckets.B).toEqual({ a: 5, b: null }); + }); + + it("returns [] for empty/absent rows (no truncation)", () => { + expect(pivotRows({}, ["a"], "k")).toEqual([]); + expect(pivotRows({ rows: [] }, ["a"], "k")).toEqual([]); + }); + + it("uses '' as key when the key field is absent", () => { + const s = pivotRows({ rows: [{ other: "z", B: [1] }] }, ["a"], "missing"); + expect(s[0].key).toBe(""); + }); +}); + +describe("bucketOrder", () => { + it("unions bucket names across series in first-seen order", () => { + const s = pivotRows( + { + rows: [ + { k: "1", Jan: [1], Feb: [1] }, + { k: "2", Feb: [1], Mar: [1] }, + ], + }, + ["a"], + "k", + ); + expect(bucketOrder(s)).toEqual(["Jan", "Feb", "Mar"]); + }); +}); + +describe("parsePivot", () => { + it("uses envelope measures + buckets when present", () => { + const env = { + measures: ["a", "b"], + buckets: [{ name: "Jan", startdate: "2026-01-01", enddate: "2026-02-01" }], + data: { rows: [{ k: "x", Jan: [10, 20] }] }, + }; + const { measures, buckets, series } = parsePivot(env, { keyField: "k" }); + expect(measures).toEqual(["a", "b"]); + expect(buckets[0].name).toBe("Jan"); + expect(series[0].buckets.Jan).toEqual({ a: 10, b: 20 }); + }); + + it("falls back to fallbackMeasures + derived buckets when omitted", () => { + const { measures, buckets } = parsePivot( + { data: { rows: [{ k: "x", Jan: [1] }] } }, + { keyField: "k", fallbackMeasures: ["a"] }, + ); + expect(measures).toEqual(["a"]); + expect(buckets).toEqual([{ name: "Jan", startdate: null, enddate: null }]); + }); +}); diff --git a/frontend/lib/pivot.ts b/frontend/lib/pivot.ts new file mode 100644 index 0000000000..9ca4031c40 --- /dev/null +++ b/frontend/lib/pivot.ts @@ -0,0 +1,98 @@ +// Generic GridPivot data layer, shared by the SPA's pivot screens (forecast, +// inventory, …). The enriched output endpoint (PivotJSONStreamView) returns +// { measures: string[], buckets: BucketMeta[], data: { rows: [...] } } +// where each row is one series: scalar values are dimension fields and array +// values are time buckets holding the measure values in `measures` order. The +// arrays are not self-describing, so the measure order comes from the envelope. + +export type BucketMeta = { + name: string; + startdate: string | null; + enddate: string | null; +}; + +export type PivotRowResponse = { + total?: number; + page?: number; + records?: number; + rows?: Array>; +}; + +export type PivotResponse = { + measures?: string[]; + buckets?: BucketMeta[]; + data: PivotRowResponse; +}; + +// One series: dimension fields + per-bucket { measure -> value } cells. +export type PivotCell = Record; +export type PivotSeries = { + key: string; // the chosen key-field value (the series identity) + fields: Record; + buckets: Record; +}; + +// Turn the pivot row list into series with named per-bucket measure cells. +// Scalar row values are series fields; array row values are time buckets. +export function pivotRows( + resp: PivotRowResponse, + measures: readonly string[], + keyField: string, +): PivotSeries[] { + const out: PivotSeries[] = []; + for (const row of resp.rows ?? []) { + const fields: Record = {}; + const buckets: Record = {}; + for (const [k, v] of Object.entries(row)) { + if (Array.isArray(v)) { + const cell: PivotCell = {}; + measures.forEach((m, idx) => { + const val = v[idx]; + cell[m] = val == null ? null : Number(val); + }); + buckets[k] = cell; + } else { + fields[k] = v as string | number | null; + } + } + out.push({ key: String(fields[keyField] ?? ""), fields, buckets }); + } + return out; // NO truncation - every series is returned +} + +// The ordered bucket names across all series (union, preserving first-seen order). +export function bucketOrder(series: PivotSeries[]): string[] { + const seen = new Set(); + const order: string[] = []; + for (const s of series) { + for (const b of Object.keys(s.buckets)) { + if (!seen.has(b)) { + seen.add(b); + order.push(b); + } + } + } + return order; +} + +// Parse a full enriched envelope into { measures, buckets, series }. When the +// envelope omits measures/buckets (older/bare endpoint) the caller's fallback +// measure order is used and bucket dates are left null. +export function parsePivot( + resp: PivotResponse, + opts: { keyField: string; fallbackMeasures?: readonly string[] }, +): { measures: string[]; buckets: BucketMeta[]; series: PivotSeries[] } { + const measures = ( + resp.measures?.length ? resp.measures : (opts.fallbackMeasures ?? []) + ) as string[]; + const series = pivotRows(resp.data, measures, opts.keyField); + const buckets = + resp.buckets && resp.buckets.length + ? resp.buckets + : bucketOrder(series).map((name) => ({ + name, + startdate: null, + enddate: null, + })); + return { measures, buckets, series }; +} diff --git a/frontend/lib/problems.ts b/frontend/lib/problems.ts new file mode 100644 index 0000000000..05ac8fc86b --- /dev/null +++ b/frontend/lib/problems.ts @@ -0,0 +1,19 @@ +// Config for the Problems / Constraints screen (Phase 3). Both the problem and +// constraint output reports share the same flat columns (entity / name / owner / +// description / start / end), so one column set serves a two-tab screen. + +import { fmtDate, type Column } from "./records"; + +export const PROBLEM_COLUMNS: Column[] = [ + { key: "entity", label: "Entity" }, + { key: "name", label: "Name" }, + { key: "owner", label: "Owner" }, + { key: "description", label: "Description" }, + { key: "startdate", label: "Start", format: fmtDate }, + { key: "enddate", label: "End", format: fmtDate }, +]; + +export const PROBLEM_TABS = [ + { key: "problem", label: "Problems", endpoint: "/api/output/problem/" }, + { key: "constraint", label: "Constraints", endpoint: "/api/output/constraint/" }, +] as const; diff --git a/frontend/lib/records.test.ts b/frontend/lib/records.test.ts new file mode 100644 index 0000000000..332866da1f --- /dev/null +++ b/frontend/lib/records.test.ts @@ -0,0 +1,60 @@ +import { describe, it, expect } from "vitest"; +import { parseRecords, cellText, fmtDate, fmtNum } from "./records"; +import type { Column } from "./records"; + +describe("parseRecords", () => { + it("passes a bare DRF array through", () => { + expect(parseRecords([{ a: 1 }, { a: 2 }])).toHaveLength(2); + }); + it("unwraps a GridReport {rows} stream", () => { + expect(parseRecords({ rows: [{ a: 1 }] })).toEqual([{ a: 1 }]); + }); + it("unwraps an enriched {data:{rows}} body", () => { + expect(parseRecords({ data: { rows: [{ a: 1 }] } })).toEqual([{ a: 1 }]); + }); + it("degrades to [] on null/garbage", () => { + expect(parseRecords(null)).toEqual([]); + expect(parseRecords(undefined)).toEqual([]); + expect(parseRecords({} as never)).toEqual([]); + }); +}); + +describe("cellText", () => { + const col: Column = { key: "x", label: "X" }; + it("renders raw values and the em-dash for null/undefined", () => { + expect(cellText(col, { x: "hi" })).toBe("hi"); + expect(cellText(col, { x: null })).toBe("—"); + expect(cellText(col, {})).toBe("—"); + }); + it("uses a column formatter when present", () => { + const c: Column = { key: "d", label: "D", format: fmtDate }; + expect(cellText(c, { d: "2026-06-26T01:00:00" })).toBe("2026-06-26 01:00"); + }); +}); + +describe("formatters", () => { + it("fmtDate trims an ISO/naive datetime to minutes", () => { + expect(fmtDate("2026-06-26 01:00:00")).toBe("2026-06-26 01:00"); + expect(fmtDate("2026-06-26T01:00:00")).toBe("2026-06-26 01:00"); + expect(fmtDate(null)).toBe("—"); + }); + it("fmtNum trims trailing zeros and handles blanks", () => { + expect(fmtNum("50.00000000")).toBe("50"); + expect(fmtNum(12.5)).toBe("12.5"); + expect(fmtNum(null)).toBe("—"); + expect(fmtNum("")).toBe("—"); + }); + + it("fmtNum renders non-finite / unparseable as the em-dash, not garbage", () => { + expect(fmtNum("NaN")).toBe("—"); + expect(fmtNum("Infinity")).toBe("—"); + expect(fmtNum(NaN)).toBe("—"); + expect(fmtNum("not a number")).toBe("—"); + }); + + it("fmtDate rejects non-date strings instead of leaking partials", () => { + expect(fmtDate("2026")).toBe("—"); + expect(fmtDate("2026-06")).toBe("—"); + expect(fmtDate("invalid")).toBe("—"); + }); +}); diff --git a/frontend/lib/records.ts b/frontend/lib/records.ts new file mode 100644 index 0000000000..f88bfe2e24 --- /dev/null +++ b/frontend/lib/records.ts @@ -0,0 +1,58 @@ +// Generic flat-record list layer (Phase 3 problem/constraint + order screens). +// Unlike the pivot screens (time-bucketed), these are plain record tables. The +// JSON arrives either as a GridReport stream (`{rows:[...]}`) or a bare DRF array +// (`[...]`); parseRecords normalises both to a row list. + +export type RecordRow = Record; + +// A displayed column. `format` maps the raw cell to text; `align` defaults left. +// `pill` renders a status pill; `edit` (with `options` for selects) declares the +// cell editable in an editable table — the table supplies the persistence. +export type Column = { + key: string; + label: string; + align?: "left" | "right" | "center"; + format?: (value: unknown, row: RecordRow) => string; + pill?: boolean; + edit?: "number" | "date" | "select"; + // readonly so a `... as const satisfies readonly OrderStatus[]` list (orders.ts, + // type-coupled to the API enum) can be passed directly; options are read-only. + options?: readonly string[]; +}; + +type RawList = RecordRow[] | { rows?: RecordRow[]; data?: { rows?: RecordRow[] } }; + +// Normalise a DRF array, a GridReport `{rows}` stream, or an enriched +// `{data:{rows}}` body to a flat row list. Tolerant of null/garbage (-> []). +export function parseRecords(json: RawList | null | undefined): RecordRow[] { + if (Array.isArray(json)) return json; + if (json && Array.isArray(json.rows)) return json.rows; + if (json && json.data && Array.isArray(json.data.rows)) return json.data.rows; + return []; +} + +// Render a cell to a string via the column's formatter, with sane defaults for +// null/dates/numbers so a screen needn't format every field. +export function cellText(col: Column, row: RecordRow): string { + const v = row[col.key]; + if (col.format) return col.format(v, row); + if (v == null) return "—"; + return String(v); +} + +// Common formatter: an ISO/naive datetime -> "YYYY-MM-DD HH:MM". Anything that +// isn't a recognisable date renders as "—" (a malformed value shouldn't masquerade +// as a partial date like "2026" or leak "invalid"). +export function fmtDate(v: unknown): string { + if (!v) return "—"; + const s = String(v).replace("T", " "); + return /^\d{4}-\d{2}-\d{2}/.test(s) ? s.slice(0, 16) : "—"; +} + +// Common formatter: a numeric-ish value -> trimmed number (drops "50.0000000"). +// Non-finite (NaN/Infinity) and unparseable values render as "—", never garbage. +export function fmtNum(v: unknown): string { + if (v == null || v === "") return "—"; + const n = typeof v === "number" ? v : parseFloat(String(v)); + return Number.isFinite(n) ? String(n) : "—"; +} diff --git a/frontend/lib/reschedule.test.ts b/frontend/lib/reschedule.test.ts new file mode 100644 index 0000000000..eff5c3360e --- /dev/null +++ b/frontend/lib/reschedule.test.ts @@ -0,0 +1,67 @@ +import { describe, it, expect } from "vitest"; +import { + rescheduleEndpoint, + isReschedulable, + shiftEngineDate, +} from "./reschedule"; + +describe("rescheduleEndpoint", () => { + it("maps each operationplan type to its DRF detail endpoint", () => { + expect(rescheduleEndpoint("MO")).toBe("manufacturingorder"); + expect(rescheduleEndpoint("WO")).toBe("workorder"); + expect(rescheduleEndpoint("PO")).toBe("purchaseorder"); + expect(rescheduleEndpoint("DO")).toBe("distributionorder"); + expect(rescheduleEndpoint("DLVR")).toBe("deliveryorder"); + }); + + it("returns null for non-reschedulable types (e.g. stock)", () => { + expect(rescheduleEndpoint("STCK")).toBeNull(); + expect(rescheduleEndpoint("")).toBeNull(); + }); +}); + +describe("isReschedulable", () => { + it("allows proposed/approved/confirmed orders of a known type", () => { + expect(isReschedulable("MO", "proposed")).toBe(true); + expect(isReschedulable("PO", "confirmed")).toBe(true); + expect(isReschedulable("DLVR", "approved")).toBe(true); + }); + + it("locks executed orders and unknown types", () => { + expect(isReschedulable("MO", "completed")).toBe(false); + expect(isReschedulable("MO", "closed")).toBe(false); + expect(isReschedulable("STCK", "proposed")).toBe(false); + }); + + it("is case-insensitive on status", () => { + expect(isReschedulable("MO", "COMPLETED")).toBe(false); + }); +}); + +describe("shiftEngineDate", () => { + const DAY = 86_400_000; + + it("shifts a naive engine timestamp and returns a naive ISO string", () => { + // +1 day from 2026-06-26 01:00:00 + expect(shiftEngineDate("2026-06-26 01:00:00", DAY)).toBe( + "2026-06-27T01:00:00", + ); + }); + + it("shifts backwards and preserves time-of-day", () => { + expect(shiftEngineDate("2026-06-26 09:30:15", -2 * DAY)).toBe( + "2026-06-24T09:30:15", + ); + }); + + it("accepts an already-ISO input too", () => { + expect(shiftEngineDate("2026-06-26T00:00:00", DAY)).toBe( + "2026-06-27T00:00:00", + ); + }); + + it("returns null for an unparseable date", () => { + expect(shiftEngineDate("", DAY)).toBeNull(); + expect(shiftEngineDate("not-a-date", DAY)).toBeNull(); + }); +}); diff --git a/frontend/lib/reschedule.ts b/frontend/lib/reschedule.ts new file mode 100644 index 0000000000..d008dc78d8 --- /dev/null +++ b/frontend/lib/reschedule.ts @@ -0,0 +1,76 @@ +// Reschedule write-path for the pegging Gantt (Phase 3-D2). Drag a bar → shift +// its start/end by the dragged time delta → PATCH the operationplan's dates. +// Pure helpers here (endpoint map, editability, date math) so they unit-test +// without React or the network; the PATCH itself is one small authedFetch. + +import { authedFetch } from "./api"; +import { HttpError } from "./errors"; +import { parseEngineDate } from "./pegging"; + +// frePPLe splits operationplans into per-type DRF detail endpoints. The bar's +// `type` selects which one to PATCH. STCK (inventory) has no reschedule endpoint. +const TYPE_ENDPOINT: Record = { + MO: "manufacturingorder", + WO: "workorder", + PO: "purchaseorder", + DO: "distributionorder", + DLVR: "deliveryorder", +}; + +// Statuses we won't let the user drag — the operationplan is already executed, +// so moving its dates is meaningless (and the engine forbids it). +const LOCKED_STATUSES = new Set(["completed", "closed"]); + +export function rescheduleEndpoint(type: string): string | null { + return TYPE_ENDPOINT[type] ?? null; +} + +export function isReschedulable(type: string, status: string): boolean { + return ( + rescheduleEndpoint(type) != null && + !LOCKED_STATUSES.has((status || "").toLowerCase()) + ); +} + +// Pad a number to 2 digits for the naive ISO formatter. +function p2(n: number): string { + return String(n).padStart(2, "0"); +} + +// Shift a naive engine timestamp ("YYYY-MM-DD HH:MM:SS") by `deltaMs` and return +// a naive ISO string ("YYYY-MM-DDTHH:MM:SS") — no timezone suffix, matching the +// engine's tz-naive convention so a round-trip doesn't drift by the UTC offset. +// Returns null when the input can't be parsed (caller skips the PATCH). +export function shiftEngineDate(engineDate: string, deltaMs: number): string | null { + const t = parseEngineDate(engineDate); + if (!Number.isFinite(t)) return null; + const d = new Date(t + deltaMs); + return ( + `${d.getFullYear()}-${p2(d.getMonth() + 1)}-${p2(d.getDate())}` + + `T${p2(d.getHours())}:${p2(d.getMinutes())}:${p2(d.getSeconds())}` + ); +} + +// PATCH an operationplan's start/end dates. `reference` is the DRF lookup. The +// scenario prefix routes to the right database (same convention as the reads). +// Throws AuthError (via authedFetch) on 401/403, HttpError on any other non-2xx. +export async function patchReschedule(opts: { + type: string; + reference: string; + startdate: string; // naive ISO + enddate: string; // naive ISO + scenario?: string; +}): Promise { + const endpoint = rescheduleEndpoint(opts.type); + if (!endpoint) throw new Error(`${opts.type} is not reschedulable`); + const prefix = opts.scenario ? `/${opts.scenario}` : ""; + const path = `${prefix}/api/input/${endpoint}/${encodeURIComponent(opts.reference)}/`; + const res = await authedFetch(path, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ startdate: opts.startdate, enddate: opts.enddate }), + }); + if (!res.ok) { + throw new HttpError(res.status, `reschedule failed: ${res.status}`); + } +} diff --git a/frontend/lib/resource.ts b/frontend/lib/resource.ts new file mode 100644 index 0000000000..b4ddd2772f --- /dev/null +++ b/frontend/lib/resource.ts @@ -0,0 +1,26 @@ +// Resource screen config (Phase 3). Read-only GridPivot over +// /api/output/resource/ (resource.OverviewReport), rendered by the generic +// . Utilization-pivot view; the timeline Gantt is a later increment. +import type { PivotScreenConfig } from "@/components/PivotScreen"; +import type { PivotSeries } from "./pivot"; + +function resourceTitle(s: PivotSeries): string { + return String(s.fields.resource ?? s.key); +} + +export const RESOURCE: PivotScreenConfig = { + endpoint: "/api/output/resource/", + keyField: "resource", + eyebrow: "Capacity", + title: "Resource", + subtitle: + "Available capacity, load and utilization per resource across time buckets.", + emptyText: "No resources.", + shown: [ + { measure: "available", label: "Available" }, + { measure: "load", label: "Load" }, + { measure: "utilization", label: "Utilization %" }, + { measure: "load_confirmed", label: "Load (confirmed)" }, + ], + titleOf: resourceTitle, +}; diff --git a/frontend/lib/session.test.ts b/frontend/lib/session.test.ts new file mode 100644 index 0000000000..6c31113383 --- /dev/null +++ b/frontend/lib/session.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { decodeClaims } from "./session"; + +function tokenFor(payload: object): string { + const b64 = btoa(JSON.stringify(payload)) + .replace(/\+/g, "-") + .replace(/\//g, "_") + .replace(/=+$/, ""); + return `header.${b64}.signature`; +} + +describe("decodeClaims", () => { + it("extracts user and exp from the payload segment", () => { + const claims = decodeClaims(tokenFor({ user: "admin", exp: 1781710132 })); + expect(claims.user).toBe("admin"); + expect(claims.exp).toBe(1781710132); + }); + + it("returns an empty object for a malformed token", () => { + expect(decodeClaims("not-a-jwt")).toEqual({}); + expect(decodeClaims("")).toEqual({}); + expect(decodeClaims("a.!!!notbase64!!!.c")).toEqual({}); + }); + + it("tolerates a payload missing user/exp", () => { + expect(decodeClaims(tokenFor({ foo: "bar" }))).toEqual({ foo: "bar" }); + }); +}); diff --git a/frontend/lib/session.ts b/frontend/lib/session.ts new file mode 100644 index 0000000000..ca2e974510 --- /dev/null +++ b/frontend/lib/session.ts @@ -0,0 +1,41 @@ +// Session helpers. The SPA is authorized by the Django session cookie; /api/token/ +// mints a short-lived JWT for it. We reuse that endpoint to answer "is there a +// session, and who is it?" — the JWT payload carries the username. When there is +// no session the screens can't load data, so the UI sends the user to the +// (same-origin) Django login page and back. + +export type Session = { user: string; exp: number }; + +// Exported for unit testing (decodes a JWT payload without verifying it — the +// server is the authority; this is only for display of the username/exp). +export function decodeClaims(token: string): { user?: string; exp?: number } { + try { + const payload = token.split(".")[1]; + const json = atob(payload.replace(/-/g, "+").replace(/_/g, "/")); + return JSON.parse(json) as { user?: string; exp?: number }; + } catch { + return {}; + } +} + +// Returns the session, or null when unauthenticated (401). Throws only on a +// genuine network error so callers can distinguish "logged out" from "offline". +export async function fetchSession(): Promise { + const res = await fetch("/api/token/", { credentials: "include" }); + if (res.status === 401 || res.status === 403) return null; + if (!res.ok) throw new Error(`session check failed: ${res.status}`); + const data = (await res.json()) as { token: string; exp?: number }; + const claims = decodeClaims(data.token); + return { user: claims.user ?? "user", exp: data.exp ?? claims.exp ?? 0 }; +} + +// Same-origin Django login, returning to `next` (defaults to the current path). +export function loginUrl(next?: string): string { + const dest = + next ?? (typeof window !== "undefined" ? window.location.pathname : "/"); + return `/data/login/?next=${encodeURIComponent(dest)}`; +} + +export function logoutUrl(): string { + return "/data/logout/"; +} diff --git a/frontend/lib/useDemandList.ts b/frontend/lib/useDemandList.ts new file mode 100644 index 0000000000..2bef1518d4 --- /dev/null +++ b/frontend/lib/useDemandList.ts @@ -0,0 +1,69 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { authedFetch } from "./api"; +import { HttpError, isAuthError } from "./errors"; + +// A demand the picker can select. Only the fields the picker shows. +export type DemandSummary = { + name: string; + item: string | null; + customer: string | null; + due: string | null; + status: string | null; +}; + +type RawDemand = Record; + +// List demands for the pegging-screen picker via the input REST API. Returns the +// raw list; the page filters/typeaheads client-side (demo datasets are small). +export function useDemandList(scenario = ""): { + demands: DemandSummary[]; + loading: boolean; + error: string | null; + authError: boolean; +} { + const [demands, setDemands] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [authError, setAuthError] = useState(false); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + setAuthError(false); + + async function load() { + try { + const prefix = scenario ? `/${scenario}` : ""; + const res = await authedFetch(`${prefix}/api/input/demand/?format=json`); + if (!res.ok) + throw new HttpError(res.status, `demand list failed: ${res.status}`); + const json = (await res.json()) as RawDemand[]; + if (cancelled) return; + const list: DemandSummary[] = (Array.isArray(json) ? json : []).map((d) => ({ + name: String(d.name ?? ""), + item: (d.item as string) ?? null, + customer: (d.customer as string) ?? null, + due: (d.due as string) ?? null, + status: (d.status as string) ?? null, + })); + setDemands(list); + } catch (e) { + if (cancelled) return; + if (isAuthError(e)) setAuthError(true); + setError(e instanceof Error ? e.message : String(e)); + } finally { + if (!cancelled) setLoading(false); + } + } + + load(); + return () => { + cancelled = true; + }; + }, [scenario]); + + return { demands, loading, error, authError }; +} diff --git a/frontend/lib/useForecast.ts b/frontend/lib/useForecast.ts new file mode 100644 index 0000000000..2bdf17f84b --- /dev/null +++ b/frontend/lib/useForecast.ts @@ -0,0 +1,84 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { authedFetch } from "./api"; +import { HttpError, isAuthError } from "./errors"; +import { + parseForecast, + type ForecastSeries, + type ForecastBucketMeta, +} from "./forecast"; + +// Read the forecast OUTPUT report for a scenario (optionally filtered to one +// forecast `name`) and pivot it into editable series. Same-origin fetch with a +// Bearer JWT (the output endpoint also accepts the session cookie). +export function useForecast( + scenario = "", + name?: string, +): { + series: ForecastSeries[]; + buckets: ForecastBucketMeta[]; + loading: boolean; + error: string | null; + authError: boolean; + reload: () => void; +} { + const [series, setSeries] = useState([]); + const [buckets, setBuckets] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [authError, setAuthError] = useState(false); + const [nonce, setNonce] = useState(0); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + setAuthError(false); + + async function load() { + try { + const prefix = scenario ? `/${scenario}` : ""; + const qs = name ? `?name=${encodeURIComponent(name)}` : ""; + const res = await authedFetch(`${prefix}/api/output/forecast/${qs}`); + if (!res.ok) + throw new HttpError(res.status, `forecast fetch failed: ${res.status}`); + const text = await res.text(); + if (cancelled) return; + let json: unknown; + try { + json = JSON.parse(text); + } catch { + // An uncomputed/empty forecast: frePPLe's empty-grid report emits + // non-strict JSON. Treat it as no series rather than an error. + setSeries([]); + setBuckets([]); + return; + } + const parsed = parseForecast(json as Parameters[0]); + setSeries(parsed.series); + setBuckets(parsed.buckets); + } catch (e) { + if (cancelled) return; + if (isAuthError(e)) setAuthError(true); + setError(e instanceof Error ? e.message : String(e)); + } finally { + if (!cancelled) setLoading(false); + } + } + + load(); + return () => { + cancelled = true; + }; + }, [scenario, name, nonce]); + + return { + series, + buckets, + loading, + error, + authError, + reload: () => setNonce((n) => n + 1), + }; +} diff --git a/frontend/lib/usePegging.ts b/frontend/lib/usePegging.ts new file mode 100644 index 0000000000..d1eb76fa89 --- /dev/null +++ b/frontend/lib/usePegging.ts @@ -0,0 +1,77 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { authedFetch } from "./api"; +import { HttpError, isAuthError } from "./errors"; +import { parsePegging, type Pegging } from "./pegging"; + +// Fetch + parse the demand-pegging Gantt for one demand in a scenario. Mirrors +// usePivotReport's contract (loading/error/authError/reload). `demand` empty => +// idle (nothing selected yet). Tolerates a non-strict-JSON body (no plan yet). +export function usePegging( + demand: string, + scenario = "", +): { + pegging: Pegging | null; + loading: boolean; + error: string | null; + authError: boolean; + reload: () => void; +} { + const [pegging, setPegging] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const [authError, setAuthError] = useState(false); + const [nonce, setNonce] = useState(0); + + useEffect(() => { + if (!demand) { + setPegging(null); + setLoading(false); + setError(null); + return; + } + let cancelled = false; + setLoading(true); + setError(null); + setAuthError(false); + + async function load() { + try { + const prefix = scenario ? `/${scenario}` : ""; + const path = `${prefix}/api/output/pegging/${encodeURIComponent(demand)}/?format=json`; + const res = await authedFetch(path); + if (!res.ok) + throw new HttpError(res.status, `pegging fetch failed: ${res.status}`); + const text = await res.text(); + if (cancelled) return; + let json: unknown = null; + try { + json = JSON.parse(text); + } catch { + /* no plan computed yet -> empty tree */ + } + setPegging(parsePegging(json as Parameters[0])); + } catch (e) { + if (cancelled) return; + if (isAuthError(e)) setAuthError(true); + setError(e instanceof Error ? e.message : String(e)); + } finally { + if (!cancelled) setLoading(false); + } + } + + load(); + return () => { + cancelled = true; + }; + }, [demand, scenario, nonce]); + + return { + pegging, + loading, + error, + authError, + reload: () => setNonce((n) => n + 1), + }; +} diff --git a/frontend/lib/usePivotReport.ts b/frontend/lib/usePivotReport.ts new file mode 100644 index 0000000000..8bf7efa47b --- /dev/null +++ b/frontend/lib/usePivotReport.ts @@ -0,0 +1,85 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { authedFetch } from "./api"; +import { HttpError, isAuthError } from "./errors"; +import { parsePivot, type PivotSeries, type BucketMeta } from "./pivot"; + +// Read any enriched GridPivot OUTPUT report (inventory / demand / resource / …) +// for a scenario and pivot it into series. Tolerant of an empty/non-strict-JSON +// body when no plan has been computed yet (the empty-grid report). +export function usePivotReport( + endpoint: string, + keyField: string, + scenario = "", +): { + series: PivotSeries[]; + buckets: BucketMeta[]; + measures: string[]; + loading: boolean; + error: string | null; + authError: boolean; + reload: () => void; +} { + const [series, setSeries] = useState([]); + const [buckets, setBuckets] = useState([]); + const [measures, setMeasures] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [authError, setAuthError] = useState(false); + const [nonce, setNonce] = useState(0); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + setAuthError(false); + + async function load() { + try { + const prefix = scenario ? `/${scenario}` : ""; + const res = await authedFetch(`${prefix}${endpoint}`); + if (!res.ok) + throw new HttpError(res.status, `${endpoint} fetch failed: ${res.status}`); + const text = await res.text(); + if (cancelled) return; + let json: unknown; + try { + json = JSON.parse(text); + } catch { + setSeries([]); + setBuckets([]); + setMeasures([]); + return; + } + const parsed = parsePivot(json as Parameters[0], { + keyField, + }); + setSeries(parsed.series); + setBuckets(parsed.buckets); + setMeasures(parsed.measures); + } catch (e) { + if (cancelled) return; + if (isAuthError(e)) setAuthError(true); + setError(e instanceof Error ? e.message : String(e)); + } finally { + if (!cancelled) setLoading(false); + } + } + + load(); + return () => { + cancelled = true; + }; + }, [endpoint, keyField, scenario, nonce]); + + return { + series, + buckets, + measures, + loading, + error, + authError, + reload: () => setNonce((n) => n + 1), + }; +} diff --git a/frontend/lib/useRecordList.ts b/frontend/lib/useRecordList.ts new file mode 100644 index 0000000000..29f25a4bb8 --- /dev/null +++ b/frontend/lib/useRecordList.ts @@ -0,0 +1,73 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { authedFetch } from "./api"; +import { HttpError, isAuthError } from "./errors"; +import { parseRecords, type RecordRow } from "./records"; + +// Fetch a flat record list (problem/constraint output reports, or DRF order +// lists) for a scenario. Mirrors usePivotReport's loading/error/authError/reload +// contract. Tolerant of an empty/non-strict-JSON body (no plan computed yet). +export function useRecordList( + endpoint: string, + scenario = "", +): { + records: RecordRow[]; + loading: boolean; + error: string | null; + authError: boolean; + reload: () => void; +} { + const [records, setRecords] = useState([]); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [authError, setAuthError] = useState(false); + const [nonce, setNonce] = useState(0); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + setAuthError(false); + + async function load() { + try { + const prefix = scenario ? `/${scenario}` : ""; + // Append format=json, picking & vs ? so an endpoint that already carries + // query params (e.g. a future ?filter=) still composes correctly. + const sep = endpoint.includes("?") ? "&" : "?"; + const res = await authedFetch(`${prefix}${endpoint}${sep}format=json`); + if (!res.ok) + throw new HttpError(res.status, `${endpoint} fetch failed: ${res.status}`); + const text = await res.text(); + if (cancelled) return; + let json: unknown = null; + try { + json = JSON.parse(text); + } catch { + /* empty/no-plan body -> [] */ + } + setRecords(parseRecords(json as Parameters[0])); + } catch (e) { + if (cancelled) return; + if (isAuthError(e)) setAuthError(true); + setError(e instanceof Error ? e.message : String(e)); + } finally { + if (!cancelled) setLoading(false); + } + } + + load(); + return () => { + cancelled = true; + }; + }, [endpoint, scenario, nonce]); + + return { + records, + loading, + error, + authError, + reload: () => setNonce((n) => n + 1), + }; +} diff --git a/frontend/lib/useReplan.ts b/frontend/lib/useReplan.ts new file mode 100644 index 0000000000..7af04a38d5 --- /dev/null +++ b/frontend/lib/useReplan.ts @@ -0,0 +1,58 @@ +"use client"; + +import { useState } from "react"; +import { authedFetch } from "./api"; +import { openAuthedSocket, parseStatus, scenarioPrefix } from "./ws"; + +// Re-plan loop for the pegging Gantt (Phase 3-D3). A reschedule persists dates +// but pegging is engine-computed, so it stays stale until a plan runs. `replan` +// launches runplan and resolves once it reaches a terminal state over the task +// websocket — the caller then re-fetches the pegging to show the real downstream. +// +// We subscribe to ws/tasks/ BEFORE launching: TaskProgressConsumer sends no +// backlog (asgi.py), so the only runplan completion we'll see is the one we kick +// off, and subscribing first means we can't miss it. +export function useReplan(scenario = ""): { + replan: () => Promise; + running: boolean; +} { + const [running, setRunning] = useState(false); + + async function replan() { + setRunning(true); + let ws: WebSocket | null = null; + try { + ws = await openAuthedSocket(`${scenarioPrefix(scenario)}/ws/tasks/`); + const socket = ws; + const done = new Promise((resolve) => { + // Safety cap so a stuck/never-broadcasting plan can't hang the UI. + const cap = setTimeout(resolve, 120_000); + socket.onmessage = (e: MessageEvent) => { + try { + const t = JSON.parse(e.data) as { name?: string; status?: string }; + if (t.name === "runplan") { + const s = parseStatus(t.status ?? null).state; + if (s === "done" || s === "failed" || s === "canceled") { + clearTimeout(cap); + resolve(); + } + } + } catch { + /* ignore non-task frames */ + } + }; + socket.onclose = () => { + clearTimeout(cap); + resolve(); + }; + }); + await authedFetch("/execute/launch/runplan/", { method: "POST" }); + await done; + } finally { + ws?.close(); + setRunning(false); + } + } + + return { replan, running }; +} diff --git a/frontend/lib/useSession.ts b/frontend/lib/useSession.ts new file mode 100644 index 0000000000..9fc24277bb --- /dev/null +++ b/frontend/lib/useSession.ts @@ -0,0 +1,35 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { fetchSession, type Session } from "./session"; + +export type SessionState = { + session: Session | null; + status: "loading" | "authed" | "anon" | "offline"; +}; + +// Resolve the current session once on mount. Used by the app shell (to show the +// user / a sign-in CTA) and by screens to gate data loads behind auth. +export function useSession(): SessionState { + const [state, setState] = useState({ + session: null, + status: "loading", + }); + + useEffect(() => { + let alive = true; + fetchSession() + .then((s) => { + if (!alive) return; + setState({ session: s, status: s ? "authed" : "anon" }); + }) + .catch(() => { + if (alive) setState({ session: null, status: "offline" }); + }); + return () => { + alive = false; + }; + }, []); + + return state; +} diff --git a/frontend/lib/useTaskLog.ts b/frontend/lib/useTaskLog.ts new file mode 100644 index 0000000000..a856211e74 --- /dev/null +++ b/frontend/lib/useTaskLog.ts @@ -0,0 +1,54 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { openAuthedSocket, scenarioPrefix } from "./ws"; + +// Stream a task's log tail over ws/tasks/{id}/log/ (Phase 1A-2). The server +// sends {"log": ""} chunks as the worker appends, and {"done": true} +// when the task finishes. +export function useTaskLog( + taskId: number | null, + scenario = "", +): { log: string; done: boolean; connected: boolean } { + const [log, setLog] = useState(""); + const [done, setDone] = useState(false); + const [connected, setConnected] = useState(false); + const wsRef = useRef(null); + + useEffect(() => { + if (taskId == null) return; + setLog(""); + setDone(false); + let closed = false; + + async function connect(): Promise { + try { + const ws = await openAuthedSocket( + `${scenarioPrefix(scenario)}/ws/tasks/${taskId}/log/`, + ); + if (closed) { + ws.close(); + return; // unmounted/switched task while awaiting the token + } + wsRef.current = ws; + ws.onopen = () => setConnected(true); + ws.onmessage = (e: MessageEvent) => { + const msg = JSON.parse(e.data) as { log?: string; done?: boolean }; + if (msg.log) setLog((prev) => prev + msg.log); + if (msg.done) setDone(true); + }; + ws.onclose = () => setConnected(false); + } catch { + setConnected(false); + } + } + + connect(); + return () => { + closed = true; + wsRef.current?.close(); + }; + }, [taskId, scenario]); + + return { log, done, connected }; +} diff --git a/frontend/lib/useTaskProgress.ts b/frontend/lib/useTaskProgress.ts new file mode 100644 index 0000000000..91b5576a84 --- /dev/null +++ b/frontend/lib/useTaskProgress.ts @@ -0,0 +1,75 @@ +"use client"; + +import { useEffect, useRef, useState } from "react"; +import { openAuthedSocket, scenarioPrefix } from "./ws"; +import { isAuthError } from "./errors"; + +export type TaskUpdate = { + id: number; + name: string; + status: string | null; + message: string | null; + started: string | null; + finished: string | null; +}; + +// Subscribe to live task progress over ws/tasks/ (Phase 1A), replacing the +// legacy 5s polling. Auto-reconnects; the server pushes one message per task +// status change. +export function useTaskProgress(scenario = ""): { + tasks: TaskUpdate[]; + connected: boolean; + authError: boolean; +} { + const [tasks, setTasks] = useState>({}); + const [connected, setConnected] = useState(false); + const [authError, setAuthError] = useState(false); + const wsRef = useRef(null); + + useEffect(() => { + let closed = false; + let retry: ReturnType | undefined; + + async function connect(): Promise { + try { + const ws = await openAuthedSocket(`${scenarioPrefix(scenario)}/ws/tasks/`); + if (closed) { + ws.close(); + return; // unmounted while awaiting the token — don't leak the socket + } + setAuthError(false); + wsRef.current = ws; + ws.onopen = () => setConnected(true); + ws.onmessage = (e: MessageEvent) => { + const t = JSON.parse(e.data) as TaskUpdate; + setTasks((prev) => ({ ...prev, [t.id]: t })); + }; + ws.onclose = () => { + setConnected(false); + if (!closed) retry = setTimeout(connect, 2000); + }; + } catch (e) { + // No Django session: surface it (the screen prompts sign-in) instead of + // looping invisibly. Other errors retry with a fixed 2s backoff. + if (isAuthError(e)) { + setAuthError(true); + return; + } + if (!closed) retry = setTimeout(connect, 2000); + } + } + + connect(); + return () => { + closed = true; + if (retry) clearTimeout(retry); + wsRef.current?.close(); + }; + }, [scenario]); + + return { + tasks: Object.values(tasks).sort((a, b) => b.id - a.id), + connected, + authError, + }; +} diff --git a/frontend/lib/ws.test.ts b/frontend/lib/ws.test.ts new file mode 100644 index 0000000000..fe47c3ec5b --- /dev/null +++ b/frontend/lib/ws.test.ts @@ -0,0 +1,28 @@ +import { describe, it, expect } from "vitest"; +import { parseStatus } from "./ws"; + +describe("parseStatus", () => { + it("treats null / non-percent / unknown as waiting", () => { + expect(parseStatus(null)).toEqual({ percent: 0, state: "waiting" }); + expect(parseStatus("Waiting")).toEqual({ percent: 0, state: "waiting" }); + expect(parseStatus("queued")).toEqual({ percent: 0, state: "waiting" }); + }); + + it("maps a percentage to running and clamps to 0..100", () => { + expect(parseStatus("42%")).toEqual({ percent: 42, state: "running" }); + expect(parseStatus("0%")).toEqual({ percent: 0, state: "running" }); + expect(parseStatus("150%")).toEqual({ percent: 100, state: "running" }); + expect(parseStatus("-5%")).toEqual({ percent: 0, state: "running" }); + }); + + it("recognises terminal states case-insensitively", () => { + expect(parseStatus("Done")).toEqual({ percent: 100, state: "done" }); + expect(parseStatus("FAILED")).toEqual({ percent: 100, state: "failed" }); + expect(parseStatus("canceled")).toEqual({ percent: 100, state: "canceled" }); + }); + + it("tolerates surrounding whitespace", () => { + expect(parseStatus(" 55% ")).toEqual({ percent: 55, state: "running" }); + expect(parseStatus(" Done ")).toEqual({ percent: 100, state: "done" }); + }); +}); diff --git a/frontend/lib/ws.ts b/frontend/lib/ws.ts new file mode 100644 index 0000000000..2f6cdd4149 --- /dev/null +++ b/frontend/lib/ws.ts @@ -0,0 +1,41 @@ +// Shared websocket helpers for the live screens. +import { getToken } from "./auth"; + +export function wsUrl(path: string): string { + if (typeof window === "undefined") return ""; + const proto = window.location.protocol === "https:" ? "wss:" : "ws:"; + return `${proto}//${window.location.host}${path}`; +} + +// Open an authenticated websocket. Browsers cannot set request headers on a +// WebSocket, so the JWT is carried in the Sec-WebSocket-Protocol subprotocol as +// ["bearer", ""] - which TokenMiddleware (asgi.py) reads server-side. +export async function openAuthedSocket(path: string): Promise { + const token = await getToken(); + return new WebSocket(wsUrl(path), ["bearer", token]); +} + +export function scenarioPrefix(scenario: string | undefined): string { + return scenario ? `/${scenario}` : ""; +} + +// Parse a frePPLe task status ("0%".."100%", "Done", "Failed", "Waiting") into a +// 0-100 progress number plus a coarse state for the UI. +export type TaskState = "waiting" | "running" | "done" | "failed" | "canceled"; + +export function parseStatus(status: string | null): { + percent: number; + state: TaskState; +} { + if (!status) return { percent: 0, state: "waiting" }; + const s = status.trim(); + if (s.endsWith("%")) { + const pct = Math.max(0, Math.min(100, parseFloat(s))); + return { percent: isNaN(pct) ? 0 : pct, state: "running" }; + } + const lower = s.toLowerCase(); + if (lower === "done") return { percent: 100, state: "done" }; + if (lower === "failed") return { percent: 100, state: "failed" }; + if (lower === "canceled") return { percent: 100, state: "canceled" }; + return { percent: 0, state: "waiting" }; +} diff --git a/frontend/next.config.mjs b/frontend/next.config.mjs new file mode 100644 index 0000000000..b58cdacbf7 --- /dev/null +++ b/frontend/next.config.mjs @@ -0,0 +1,24 @@ +/** @type {import('next').NextConfig} */ +const nextConfig = { + reactStrictMode: true, + // Emit a self-contained server (.next/standalone) so the runtime image ships + // only the traced deps — no source, no devDependencies. See e2e/Dockerfile.frontend. + output: "standalone", + async rewrites() { + const backend = process.env.FREPPLE_BACKEND || "http://localhost:8000"; + // Dev-only: proxy the Django/engine HTTP routes the SPA calls so the browser + // keeps one origin (cookies/JWT flow naturally). In prod these are inert — + // nginx / the Ingress own the routing (see e2e/nginx.conf + the Helm + // Ingress, which are the canonical routing table). NOTE: websockets + // (/ws/...) are NOT handled here — Next rewrites don't upgrade WS; run + // `next dev` behind the e2e nginx to exercise live progress. + return [ + { source: "/api/:path*", destination: `${backend}/api/:path*` }, + { source: "/data/:path*", destination: `${backend}/data/:path*` }, + { source: "/execute/:path*", destination: `${backend}/execute/:path*` }, + { source: "/forecast/:path*", destination: `${backend}/forecast/:path*` }, + ]; + }, +}; + +export default nextConfig; diff --git a/frontend/package-lock.json b/frontend/package-lock.json new file mode 100644 index 0000000000..d1f371d7c4 --- /dev/null +++ b/frontend/package-lock.json @@ -0,0 +1,2786 @@ +{ + "name": "frepple-frontend", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "frepple-frontend", + "version": "0.1.0", + "dependencies": { + "next": "14.2.35", + "react": "18.3.1", + "react-dom": "18.3.1", + "recharts": "2.12.7" + }, + "devDependencies": { + "@types/node": "20.14.10", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "openapi-typescript": "7.0.0", + "typescript": "5.5.3", + "vitest": "2.0.5" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz", + "integrity": "sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.21.5.tgz", + "integrity": "sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz", + "integrity": "sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.21.5.tgz", + "integrity": "sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz", + "integrity": "sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz", + "integrity": "sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz", + "integrity": "sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz", + "integrity": "sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz", + "integrity": "sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz", + "integrity": "sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz", + "integrity": "sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz", + "integrity": "sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz", + "integrity": "sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz", + "integrity": "sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz", + "integrity": "sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz", + "integrity": "sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz", + "integrity": "sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz", + "integrity": "sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz", + "integrity": "sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz", + "integrity": "sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz", + "integrity": "sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz", + "integrity": "sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz", + "integrity": "sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@next/env": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", + "license": "MIT" + }, + "node_modules/@next/swc-darwin-arm64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-darwin-x64": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-arm64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-gnu": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@redocly/ajv": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/@redocly/ajv/-/ajv-8.11.2.tgz", + "integrity": "sha512-io1JpnwtIcvojV7QKDUSIuMN/ikdOUd1ReEnUnMKGfDVridQZ31J0MmIuqwuRjWDZfmvr+Q0MqCcfHM2gTivOg==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2", + "uri-js-replace": "^1.0.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@redocly/config": { + "version": "0.22.0", + "resolved": "https://registry.npmjs.org/@redocly/config/-/config-0.22.0.tgz", + "integrity": "sha512-gAy93Ddo01Z3bHuVdPWfCwzgfaYgMdaZPcfL7JZ7hWJoK9V0lXDbigTWkhiPFAaLWzbOJ+kbUQG1+XwIm0KRGQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@redocly/openapi-core": { + "version": "1.34.15", + "resolved": "https://registry.npmjs.org/@redocly/openapi-core/-/openapi-core-1.34.15.tgz", + "integrity": "sha512-HAwCnNyKcs5XGQqms+9t7OdAPM/5TDstmhF+0i7tdCFato2QKuYIlyWETwkXd8c5zbltr1oB+6y9NTeQLr2d6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/ajv": "8.11.2", + "@redocly/config": "0.22.0", + "colorette": "1.4.0", + "https-proxy-agent": "7.0.6", + "js-levenshtein": "1.1.6", + "js-yaml": "4.1.1", + "minimatch": "5.1.9", + "pluralize": "8.0.0", + "yaml-ast-parser": "0.0.43" + }, + "engines": { + "node": ">=18.17.0", + "npm": ">=9.5.0" + } + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.0.tgz", + "integrity": "sha512-IPIQ55ythEHkfEd9jMEi32OQ7SxURsGA43JI22lj01OLZNt2NUbJX8YUHxkVWyQ6daHPNn0truF5nSj3DQp6YQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.0.tgz", + "integrity": "sha512-M6s9cr10MibETyo8JsOkq+Lo1+lU6hcvb1MApnUql5qte/5hMEgzlN8/ReIKNfRV8rrqX50W1BX9zoUhC192RA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.0.tgz", + "integrity": "sha512-BqCoMoIbn0keKys+dEAdBa70EtOwV1bEsQCUgU9FdiZmmMge/Zk7LlkYGqbrdHR+Frnt0E1FOanly+rlwvvQzw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.0.tgz", + "integrity": "sha512-SIMzST3VFNXDAbeIWDWiFCNM5qncUBDWaEV7NfE7oZbDt2mgfW4MvbKdbYiGOLoM32gbTv608UMd0XktEYSD7w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.0.tgz", + "integrity": "sha512-ezjfSQMP7ArdUsbBwbQIfwAlhE84I2iVnzQNCFSveqV42q+BmKlzVpf7mxv5EchLcoWU4y6/heFzVg1F+hodUQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.0.tgz", + "integrity": "sha512-9+qTWGW9AZRhnUgwtTwzNwcPlL87ngkeN0LA+q1bADvmY9aNvWaF2TFW8BZgnQPYxpDI7+rMVLivcd4V737TAQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.0.tgz", + "integrity": "sha512-T1dMEQhXA/jkJ/jyMIw9IovK8bSUq7A8kLIlvZTb/6YIVsp2zLavr4F3oyllHWo7eIVJRyE5n3tUjQJEbE1IuQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.0.tgz", + "integrity": "sha512-2as0LgT7qQpyceQq6VUJYnumUMUrgGQCWIiDIN9DE0/tglsk6o66uCB4f3djRawAltvfCNLyZZrsqbPA6inCsA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.0.tgz", + "integrity": "sha512-bVURMg+6eNN9C/yc0aVjooZcwTTtYF4YW3xta5pP0//r3o1V8gXEHXWCndj47w/HhwsFroZrFhR+6uQP5T0n0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.0.tgz", + "integrity": "sha512-Ful8pM/2yYI83PViWdFdpZhdI8HJ5qsXANe5atypbHDf+KIBBDsZsbyy8hbXnULVvW9NsTh5DHwbcBftyLTfiw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.0.tgz", + "integrity": "sha512-9Gp/DgrkzfUBmNPVTyPTvay+4xEP7M/clXpj3efXBcm6uTIVIgDg4rqUpqKXvLEuFRVuEpSAOkhgNeecvaZ4Cg==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.0.tgz", + "integrity": "sha512-m9tsJz54LUXkSYM8+8PG81B9IKK5r+2T0clMq4QrS16xFosufU7firBDAZEsDheDs7wTlP7h3++S7lMsU955HA==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.0.tgz", + "integrity": "sha512-3UvJ5PNVU16aJf6M3tFI24pWzAl2/ynfbyRN3ICyQajK1lSkrnVYNnLz3v04J32qKa0FczJc22zeToc0lr2A3w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.0.tgz", + "integrity": "sha512-vRWUAbYLGHBZS6Q8Msb2sfnf1fvJf+47t8l/TwOerM2qArzy+IeNMTHrYLHXh95h8MoatPHI5hhSZNs+mGXKPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.0.tgz", + "integrity": "sha512-c00T5SYENHAt86cfW47URaP3Us5vLC/4QO7GYud1G5VNRffCwwCuBspwqYrriuJB+5m0WFzClCn9wed0FBjKvg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.0.tgz", + "integrity": "sha512-krrCDilhXOwFkSkO3Wm9I/f9H0L92XHHwy2fwxjukxIbh0dem8gZqOW5Y8BsHrpJv5qwlRBV+Wl4ZFyRWhUpwg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.0.tgz", + "integrity": "sha512-7pfYFSTc4/rUC/FtAI0Qp6QthDBCIi6/AuP1xYqFk5vanI6KnL5dWKP60OM/05LOsbwTmIcvr6eXC4CJuJ75IA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.0.tgz", + "integrity": "sha512-7SDIalKeIpG0Ifogbbdn58HmSotYMlf23K3dCJEmiVd9Fg36Vmni82iPQec27N3wY4Bvbxftkxz6vSx9OcouTg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.0.tgz", + "integrity": "sha512-eRZevouTH2i1HeAVLqJuLnt256krQkGY0TN6WsTmsIhuzbh457HuWDMakKwmi0Cjadux983CoSr8Lim2QhUIFw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.0.tgz", + "integrity": "sha512-3oVS7FLGa4U1qcvao9ylGxrjXZyUQqR8UwxEcnUEyPX53O/C/mKDZegNXTdHCP+h3e6ta/f1EN38Yif1mmZHYg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.0.tgz", + "integrity": "sha512-yTB9TgfWj5wHe5QgktAgXTLLot1gvEjl1NiPPAUiCs4oPrIWFl5V4nC3GrkNdj9LaAU4s94nVrGbGOCqUpyWsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.0.tgz", + "integrity": "sha512-5LOhoaesY3doG1c+ac/2JtgREpKoJr5bUHH8tKY0V8di7+uSV6BwLs2PlR0/yzefGOkR+wE7ZolZphHCsyG5Rw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.0.tgz", + "integrity": "sha512-yYkWHhmbhRTWTnWos5HC4GcPQfjlzzCNbM9e/+GXrLuaBXYA3qSDR9f0Vgufd5S8yX81U8jPKp7ZnAjZFMtRnw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.0.tgz", + "integrity": "sha512-SoTb6lPg25xZlA2ibwQ++ahCCnH+FP0qmEuafMJ4gznZKOlXioKEAeJLgCrqjM98ACziXM9V1amFjICVL4IFoA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.0.tgz", + "integrity": "sha512-5L+T1fMX4RIEBoZzT0+sQ0PhTS36NULFmMXtl1TZo44TMAROIMHbZufSOjVWt/Y622BtxgxtaNOokbTDvfsrZA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, + "node_modules/@swc/helpers": { + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", + "license": "Apache-2.0", + "dependencies": { + "@swc/counter": "^0.1.3", + "tslib": "^2.4.0" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.2.tgz", + "integrity": "sha512-hOLWVbm7uRza0BYXpIIW5pxfrKe0W+D5lrFiAEYR+pb6w3N2SwSMaJbXdUfSEv+dT4MfHBLtn5js0LAWaO6otw==", + "license": "MIT" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==", + "license": "MIT" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==", + "license": "MIT" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "license": "MIT", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==", + "license": "MIT" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "license": "MIT", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.8.tgz", + "integrity": "sha512-lae0iWfcDeR7qt7rA88BNiqdvPS5pFVPpo5OfjElwNaT2yyekbM0C9vK+yqBqEmHr6lDkRnYNoTBYlAgJa7a4w==", + "license": "MIT", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==", + "license": "MIT" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.14.10", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.10.tgz", + "integrity": "sha512-MdiXf+nDuMvY0gJKxyfZ7/6UFsETO7mGKF54MVD/ekJS6HdFtpZFBgrh6Pseu64XTb2MLyFPlbW6hj8HYRQNOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.3.tgz", + "integrity": "sha512-hti/R0pS0q1/xx+TsI73XIqk26eBsISZ2R0wUijXIngRK9R/e7Xw/cXVxQK7R5JjW+SV4zGcn5hXjudkN/pLIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.0.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.0", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.0.tgz", + "integrity": "sha512-EhwApuTmMBmXuFOikhQLIBUn6uFg81SwLMOAUgodJF14SOBOCMdU04gDoYi0WOJJHD144TL32z4yDqCW3dnkQg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/react": "*" + } + }, + "node_modules/@vitest/expect": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.0.5.tgz", + "integrity": "sha512-yHZtwuP7JZivj65Gxoi8upUN2OzHTi3zVfjwdpu2WrvCZPLwsJ2Ey5ILIPccoW23dd/zQBlJ4/dhi7DWNyXCpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/pretty-format": { + "version": "2.1.9", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.9.tgz", + "integrity": "sha512-KhRIdGV2U9HOUzxfiHmY8IFHTdqtOhIzCpd8WRdJiE7D/HUcZVD0EgQCVjm+Q9gkUXWgBvMmTtZgIG48wq7sOQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.0.5.tgz", + "integrity": "sha512-TfRfZa6Bkk9ky4tW0z20WKXFEwwvWhRY+84CnSEtq4+3ZvDlJyY32oNTJtM7AW9ihW90tX/1Q78cb6FjoAs+ig==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "2.0.5", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.0.5.tgz", + "integrity": "sha512-SgCPUeDFLaM0mIUHfaArq8fD2WbaXG/zVXjRupthYfYGzc8ztbFbu6dUNOblBG7XLMR1kEhS/DNnfCZ2IhdDew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "magic-string": "^0.30.10", + "pathe": "^1.1.2" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot/node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.0.5.tgz", + "integrity": "sha512-c/jdthAhvJdpfVuaexSrnawxZz6pywlTPe84LUB2m/4t3rl2fTo9NFGBG4oWgaD+FTgDDV8hJ/nibT7IfH3JfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.0.5.tgz", + "integrity": "sha512-d8HKbqIcya+GR67mkZbrzhS5kKhtp8dQLcmRZLGTscGVg7yImT82cIrhtn2L8+VujWcy6KZweApgNmPsTAO/UQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "2.0.5", + "estree-walker": "^3.0.3", + "loupe": "^3.1.1", + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils/node_modules/@vitest/pretty-format": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.0.5.tgz", + "integrity": "sha512-h8k+1oWHfwTkyTkb9egzwNMfJAEx4veaPSnMeKbVSjp4euqGSbQlm5+6VHwTr7u4FJslVVsUG5nopCaAYdOmSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^1.2.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/busboy": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", + "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==", + "dependencies": { + "streamsearch": "^1.1.0" + }, + "engines": { + "node": ">=10.16.0" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/client-only": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/colorette": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.4.0.tgz", + "integrity": "sha512-Y2oEozpomLn7Q3HFP7dpww7AtMJplbM9lGZP6RDfHqmbeRjiwRg4n6VM6j4KLmRke85uWEI7JqF17f3pqdRA0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "license": "ISC", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.2.tgz", + "integrity": "sha512-AJDdYOdnyRDV5b6ArilzCPPwc1ejkHcoyFarqlPqT7zRYjhavcT3uSrqcMvsgh2CgoPbK3RCwyHaVyxYcP2Arg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "license": "ISC", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "license": "ISC", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "license": "ISC", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "license": "ISC", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "license": "ISC", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", + "license": "MIT" + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/esbuild": { + "version": "0.21.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", + "integrity": "sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=12" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.21.5", + "@esbuild/android-arm": "0.21.5", + "@esbuild/android-arm64": "0.21.5", + "@esbuild/android-x64": "0.21.5", + "@esbuild/darwin-arm64": "0.21.5", + "@esbuild/darwin-x64": "0.21.5", + "@esbuild/freebsd-arm64": "0.21.5", + "@esbuild/freebsd-x64": "0.21.5", + "@esbuild/linux-arm": "0.21.5", + "@esbuild/linux-arm64": "0.21.5", + "@esbuild/linux-ia32": "0.21.5", + "@esbuild/linux-loong64": "0.21.5", + "@esbuild/linux-mips64el": "0.21.5", + "@esbuild/linux-ppc64": "0.21.5", + "@esbuild/linux-riscv64": "0.21.5", + "@esbuild/linux-s390x": "0.21.5", + "@esbuild/linux-x64": "0.21.5", + "@esbuild/netbsd-x64": "0.21.5", + "@esbuild/openbsd-x64": "0.21.5", + "@esbuild/sunos-x64": "0.21.5", + "@esbuild/win32-arm64": "0.21.5", + "@esbuild/win32-ia32": "0.21.5", + "@esbuild/win32-x64": "0.21.5" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "license": "MIT" + }, + "node_modules/execa": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-8.0.1.tgz", + "integrity": "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^8.0.1", + "human-signals": "^5.0.0", + "is-stream": "^3.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^5.1.0", + "onetime": "^6.0.0", + "signal-exit": "^4.1.0", + "strip-final-newline": "^3.0.0" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-equals": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.4.0.tgz", + "integrity": "sha512-jt2DW/aNFNwke7AUd+Z+e6pz39KO5rzdbbFCg2sGafS4mk13MI7Z8O5z9cADNn5lhGODIgLwug6TZO2ctf7kcw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-stream": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-8.0.1.tgz", + "integrity": "sha512-VaUJspBffn/LMCJVoMvSAdmscJyS1auj5Zulnn5UoYcY531UWmdwhRWkcGKnGU93m5HSXP9LP2usOryrBtQowA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "license": "ISC" + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-5.0.0.tgz", + "integrity": "sha512-AXcZb6vzzrFAUE61HnN4mpLqd/cSIwNQjtNWR0euPm6y0iqx3G4gOXaIDdtdDwZmhwe82LA6+zinmW4UBWVePQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=16.17.0" + } + }, + "node_modules/index-to-position": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/index-to-position/-/index-to-position-1.2.0.tgz", + "integrity": "sha512-Yg7+ztRkqslMAS2iFaU+Oa4KTSidr63OsFGlOrJoW981kIYO3CGCS3wA95P1mUi/IVSJkn0D479KTJpVpvFNuw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-3.0.0.tgz", + "integrity": "sha512-LnQR4bZ9IADDRSkvpqMGvt/tEJWclzklNgSw48V5EAaAeDd6qGvN8ei6k5p0tvxSR171VmGyHuTiAOfxAbr8kA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-levenshtein": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/js-levenshtein/-/js-levenshtein-1.1.6.tgz", + "integrity": "sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/mimic-fn": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-4.0.0.tgz", + "integrity": "sha512-vqiC06CuhBTUdZH+RYl8sFrL096vA45Ok5ISO6sE/Mr1jRbGH4Csnhi8f3wKVl7x8mO4Au7Ir9D3Oyv1VYMFJw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/minimatch": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.1.9.tgz", + "integrity": "sha512-7o1wEA2RyMP7Iu7GNba9vc0RWWGACJOCZBJX2GJWip0ikV+wcOsgVuY9uE8CPiyQhkGFSlhuSkZPavN7u1c2Fw==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/next": { + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", + "license": "MIT", + "dependencies": { + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", + "busboy": "1.6.0", + "caniuse-lite": "^1.0.30001579", + "graceful-fs": "^4.2.11", + "postcss": "8.4.31", + "styled-jsx": "5.1.1" + }, + "bin": { + "next": "dist/bin/next" + }, + "engines": { + "node": ">=18.17.0" + }, + "optionalDependencies": { + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", + "react": "^18.2.0", + "react-dom": "^18.2.0", + "sass": "^1.3.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + }, + "@playwright/test": { + "optional": true + }, + "sass": { + "optional": true + } + } + }, + "node_modules/npm-run-path": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-5.3.0.tgz", + "integrity": "sha512-ppwTtiJZq0O/ai0z7yfudtBpWIoxM8yE6nHi1X47eFR2EWORqfbu6CnPlNsjeN683eT0qG6H/Pyf9fCcvjnnnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-run-path/node_modules/path-key": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-4.0.0.tgz", + "integrity": "sha512-haREypq7xkM7ErfgIyA0z+Bj4AGKlMSdlQE2jvJo6huWD1EdkKYV+G/T4nq0YEF2vgTT8kqMFKo1uHn950r4SQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/onetime": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-6.0.0.tgz", + "integrity": "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "mimic-fn": "^4.0.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/openapi-typescript": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/openapi-typescript/-/openapi-typescript-7.0.0.tgz", + "integrity": "sha512-5NobO3pavTUVmErRVjnfiIIqCNjCrZeva4ElOA3nNKcSo4Jm5G7zv4WLcw6S+jDVnGGRkchxnJ2yIJBp9ULUAg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@redocly/openapi-core": "^1.16.0", + "ansi-colors": "^4.1.3", + "parse-json": "^8.1.0", + "supports-color": "^9.4.0", + "yargs-parser": "^21.1.1" + }, + "bin": { + "openapi-typescript": "bin/cli.js" + }, + "peerDependencies": { + "typescript": "^5.x" + } + }, + "node_modules/parse-json": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-8.3.0.tgz", + "integrity": "sha512-ybiGyvspI+fAoRQbIPRddCcSTV9/LsJbf0e/S85VLowVGzRmokfneg2kwVW/KU5rOXrPSbF1qAKPMgNTqqROQQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.26.2", + "index-to-position": "^1.1.0", + "type-fest": "^4.39.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-1.1.2.tgz", + "integrity": "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/pluralize": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-8.0.0.tgz", + "integrity": "sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss": { + "version": "8.4.31", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", + "integrity": "sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.6", + "picocolors": "^1.0.0", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "license": "MIT" + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "license": "MIT", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "license": "BSD-3-Clause", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/recharts": { + "version": "2.12.7", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.12.7.tgz", + "integrity": "sha512-hlLJMhPQfv4/3NBSAyq3gzGg4h2v69RJh6KU7b3pXYNNAELs9kEoXOjbkxdXpALqKBoVmVptGfLpxdaVYqjmXQ==", + "deprecated": "1.x and 2.x branches are no longer active. Bump to Recharts v3 to receive latest features and bugfixes. See https://github.com/recharts/recharts/wiki/3.0-migration-guide", + "license": "MIT", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^16.10.2", + "react-smooth": "^4.0.0", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "license": "MIT", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rollup": { + "version": "4.62.0", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.0.tgz", + "integrity": "sha512-nc72Wgq62I7rtDV4izT5/aaS0zxy3kttkinf9586ApknY3jZO9NYsmtc24fUckA0X7Q2v+ML4a15pdUlV5V/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.0", + "@rollup/rollup-android-arm64": "4.62.0", + "@rollup/rollup-darwin-arm64": "4.62.0", + "@rollup/rollup-darwin-x64": "4.62.0", + "@rollup/rollup-freebsd-arm64": "4.62.0", + "@rollup/rollup-freebsd-x64": "4.62.0", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.0", + "@rollup/rollup-linux-arm-musleabihf": "4.62.0", + "@rollup/rollup-linux-arm64-gnu": "4.62.0", + "@rollup/rollup-linux-arm64-musl": "4.62.0", + "@rollup/rollup-linux-loong64-gnu": "4.62.0", + "@rollup/rollup-linux-loong64-musl": "4.62.0", + "@rollup/rollup-linux-ppc64-gnu": "4.62.0", + "@rollup/rollup-linux-ppc64-musl": "4.62.0", + "@rollup/rollup-linux-riscv64-gnu": "4.62.0", + "@rollup/rollup-linux-riscv64-musl": "4.62.0", + "@rollup/rollup-linux-s390x-gnu": "4.62.0", + "@rollup/rollup-linux-x64-gnu": "4.62.0", + "@rollup/rollup-linux-x64-musl": "4.62.0", + "@rollup/rollup-openbsd-x64": "4.62.0", + "@rollup/rollup-openharmony-arm64": "4.62.0", + "@rollup/rollup-win32-arm64-msvc": "4.62.0", + "@rollup/rollup-win32-ia32-msvc": "4.62.0", + "@rollup/rollup-win32-x64-gnu": "4.62.0", + "@rollup/rollup-win32-x64-msvc": "4.62.0", + "fsevents": "~2.3.2" + } + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/strip-final-newline": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-3.0.0.tgz", + "integrity": "sha512-dOESqjYr96iWYylGObzd39EuNTa5VJxyvVAEm5Jnh7KGo75V43Hk1odPQkNDyXNmUR6k+gEiDVXnjB8HJ3crXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/styled-jsx": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", + "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "license": "MIT", + "dependencies": { + "client-only": "0.0.1" + }, + "engines": { + "node": ">= 12.0.0" + }, + "peerDependencies": { + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + }, + "peerDependenciesMeta": { + "@babel/core": { + "optional": true + }, + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/supports-color": { + "version": "9.4.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-9.4.0.tgz", + "integrity": "sha512-VL+lNrEoIXww1coLPOmiEmK/0sGigko5COxI09KzHc2VJXJsQ37UaQ+8quuxjDeA7+KnLGTWRyOXSLLR2Wb4jw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-1.2.0.tgz", + "integrity": "sha512-weEDEq7Z5eTHPDh4xjX789+fHfF+P8boiFB+0vbWzpbnbsEr/GRaohi/uMKxg8RZMXnl1ItAi/IUHWMsjDV7kQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-3.0.2.tgz", + "integrity": "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "license": "0BSD" + }, + "node_modules/type-fest": { + "version": "4.41.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-4.41.0.tgz", + "integrity": "sha512-TeTSQ6H5YHvpqVwBRcnLDCBnDOHWYu7IvGbHT6N8AOymcr9PJGjc1GTtiWZTYg0NCgYwvnYWEkVChQAr9bjfwA==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/typescript": { + "version": "5.5.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.5.3.tgz", + "integrity": "sha512-/hreyEujaB0w76zKo6717l3L0o/qEUtRgdvUBvlkhoWeOVMjMuHNHk0BRBzikzuGDqNmPQbg5ifMEqsHLiIUcQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "dev": true, + "license": "MIT" + }, + "node_modules/uri-js-replace": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/uri-js-replace/-/uri-js-replace-1.0.1.tgz", + "integrity": "sha512-W+C9NWNLFOoBI2QWDp4UT9pv65r2w5Cx+3sTYFvtMdDBxkKt1syCqsUdSFAChbEe1uK5TfS04wt/nGwmaeIQ0g==", + "dev": true, + "license": "MIT" + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "license": "MIT AND ISC", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vite": { + "version": "5.4.21", + "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", + "integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.21.3", + "postcss": "^8.4.43", + "rollup": "^4.20.0" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^18.0.0 || >=20.0.0", + "less": "*", + "lightningcss": "^1.21.0", + "sass": "*", + "sass-embedded": "*", + "stylus": "*", + "sugarss": "*", + "terser": "^5.4.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.0.5.tgz", + "integrity": "sha512-LdsW4pxj0Ot69FAoXZ1yTnA9bjGohr2yNBU7QKRxpz8ITSkhuDl6h3zS/tvgz4qrNjeRnvrWeXQ8ZF7Um4W00Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.3.5", + "pathe": "^1.1.2", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vite/node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/vitest": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-2.0.5.tgz", + "integrity": "sha512-8GUxONfauuIdeSl5f9GTgVEpg5BTOlplET4WEDaeY2QBiN8wSm68vxN/tb5z405OwppfoCavnwXafiaYBC/xOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.3.0", + "@vitest/expect": "2.0.5", + "@vitest/pretty-format": "^2.0.5", + "@vitest/runner": "2.0.5", + "@vitest/snapshot": "2.0.5", + "@vitest/spy": "2.0.5", + "@vitest/utils": "2.0.5", + "chai": "^5.1.1", + "debug": "^4.3.5", + "execa": "^8.0.1", + "magic-string": "^0.30.10", + "pathe": "^1.1.2", + "std-env": "^3.7.0", + "tinybench": "^2.8.0", + "tinypool": "^1.0.0", + "tinyrainbow": "^1.2.0", + "vite": "^5.0.0", + "vite-node": "2.0.5", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || >=20.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/node": "^18.0.0 || >=20.0.0", + "@vitest/browser": "2.0.5", + "@vitest/ui": "2.0.5", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/yaml-ast-parser": { + "version": "0.0.43", + "resolved": "https://registry.npmjs.org/yaml-ast-parser/-/yaml-ast-parser-0.0.43.tgz", + "integrity": "sha512-2PTINUwsRqSd+s8XxKaJWQlUuEMHJQyEuh2edBbW8KNJz0SJPwUSD2zRWqezFEdN7IzAgeuYHFUCF7o8zRdZ0A==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + } + } +} diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000000..322f5c3949 --- /dev/null +++ b/frontend/package.json @@ -0,0 +1,29 @@ +{ + "name": "frepple-frontend", + "version": "0.1.0", + "private": true, + "description": "frePPLe modern UI (Next.js). Phase 1A: Execute screen with live task progress + log tail.", + "scripts": { + "dev": "next dev", + "build": "next build", + "start": "next start", + "lint": "next lint", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "gen:api": "openapi-typescript ../generated/openapi.yaml -o lib/api-types.ts" + }, + "dependencies": { + "next": "14.2.35", + "react": "18.3.1", + "react-dom": "18.3.1", + "recharts": "2.12.7" + }, + "devDependencies": { + "@types/node": "20.14.10", + "@types/react": "18.3.3", + "@types/react-dom": "18.3.0", + "openapi-typescript": "7.0.0", + "typescript": "5.5.3", + "vitest": "2.0.5" + } +} diff --git a/frontend/public/.gitkeep b/frontend/public/.gitkeep new file mode 100644 index 0000000000..e69de29bb2 diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json new file mode 100644 index 0000000000..bfe62a4a64 --- /dev/null +++ b/frontend/tsconfig.json @@ -0,0 +1,21 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["dom", "dom.iterable", "ES2022"], + "allowJs": false, + "skipLibCheck": true, + "strict": true, + "noEmit": true, + "esModuleInterop": true, + "module": "esnext", + "moduleResolution": "bundler", + "resolveJsonModule": true, + "isolatedModules": true, + "jsx": "preserve", + "incremental": true, + "plugins": [{ "name": "next" }], + "paths": { "@/*": ["./*"] } + }, + "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], + "exclude": ["node_modules"] +} diff --git a/generated/openapi.yaml b/generated/openapi.yaml new file mode 100644 index 0000000000..6e68739212 --- /dev/null +++ b/generated/openapi.yaml @@ -0,0 +1,13327 @@ +openapi: 3.0.3 +info: + title: frePPLe API + version: 1.0.0 + description: REST API for the frePPLe planning application. +paths: + /api/common/attribute/: + get: + operationId: common_attribute_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: query + name: editable + schema: + type: boolean + - in: query + name: initially_hidden + schema: + type: boolean + - in: query + name: lastmodified + schema: + type: string + format: date-time + - in: query + name: lastmodified__gt + schema: + type: string + format: date-time + - in: query + name: lastmodified__gte + schema: + type: string + format: date-time + - in: query + name: lastmodified__in + schema: + type: array + items: + type: string + format: date-time + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: lastmodified__lt + schema: + type: string + format: date-time + - in: query + name: lastmodified__lte + schema: + type: string + format: date-time + - in: query + name: model + schema: + type: string + enum: + - apikey + - bucket + - bucketdetail + - buffer + - calendar + - calendarbucket + - constraint + - customer + - demand + - forecast + - item + - itemdistribution + - itemsupplier + - location + - operation + - operationdependency + - operationmaterial + - operationplan + - operationplanmaterial + - operationplanresource + - operationresource + - problem + - resource + - resourceskill + - setupmatrix + - setuprule + - skill + - suboperation + - supplier + - user + - workorder + description: |- + * `calendar` - calendar + * `supplier` - supplier + * `item` - item + * `location` - location + * `customer` - customer + * `demand` - demand + * `operation` - operation + * `operationplan` - operationplan + * `operationplanmaterial` - operationplanmaterial + * `setupmatrix` - setupmatrix + * `skill` - skill + * `resource` - resource + * `operationplanresource` - operationplanresource + * `suboperation` - suboperation + * `setuprule` - setuprule + * `resourceskill` - resourceskill + * `operationresource` - operationresource + * `operationmaterial` - operationmaterial + * `itemsupplier` - itemsupplier + * `itemdistribution` - itemdistribution + * `calendarbucket` - calendarbucket + * `buffer` - buffer + * `operationdependency` - operationdependency + * `workorder` - workorder + * `forecast` - forecast + * `constraint` - constraint + * `problem` - problem + * `user` - user + * `bucket` - bucket + * `bucketdetail` - bucketdetail + * `apikey` - apikey + - in: query + name: model__in + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: name + schema: + type: string + - in: query + name: name__contains + schema: + type: string + - in: query + name: name__in + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + - in: query + name: source + schema: + type: string + - in: query + name: source__in + schema: + type: array + items: + type: string + description: Multiple values may be separated by commas. + explode: false + style: form + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedAttribute' + description: '' + post: + operationId: common_attribute_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAttribute' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedAttribute' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedAttribute' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAttribute' + description: '' + put: + operationId: common_attribute_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAttribute' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedAttribute' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedAttribute' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAttribute' + description: '' + patch: + operationId: common_attribute_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAttribute' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedAttribute' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedAttribute' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAttribute' + description: '' + delete: + operationId: common_attribute_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/attribute/{id}/: + get: + operationId: common_attribute_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + description: '' + put: + operationId: common_attribute_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Attribute' + multipart/form-data: + schema: + $ref: '#/components/schemas/Attribute' + required: true + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + description: '' + patch: + operationId: common_attribute_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedAttribute' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedAttribute' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedAttribute' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Attribute' + description: '' + delete: + operationId: common_attribute_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/bucket/: + get: + operationId: common_bucket_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedBucket' + description: '' + post: + operationId: common_bucket_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucket' + description: '' + put: + operationId: common_bucket_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucket' + description: '' + patch: + operationId: common_bucket_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucket' + description: '' + delete: + operationId: common_bucket_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/bucket/{id}/: + get: + operationId: common_bucket_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Bucket' + description: '' + put: + operationId: common_bucket_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Bucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Bucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/Bucket' + required: true + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Bucket' + description: '' + patch: + operationId: common_bucket_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Bucket' + description: '' + delete: + operationId: common_bucket_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/bucketdetail/: + get: + operationId: common_bucketdetail_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedBucketDetail' + description: '' + post: + operationId: common_bucketdetail_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + description: '' + put: + operationId: common_bucketdetail_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + description: '' + patch: + operationId: common_bucketdetail_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + description: '' + delete: + operationId: common_bucketdetail_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/bucketdetail/{id}/: + get: + operationId: common_bucketdetail_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BucketDetail' + description: '' + put: + operationId: common_bucketdetail_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/BucketDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/BucketDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/BucketDetail' + required: true + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BucketDetail' + description: '' + patch: + operationId: common_bucketdetail_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBucketDetail' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/BucketDetail' + description: '' + delete: + operationId: common_bucketdetail_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/comment/: + get: + operationId: common_comment_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedComment' + description: '' + post: + operationId: common_comment_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedComment' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedComment' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedComment' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedComment' + description: '' + put: + operationId: common_comment_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedComment' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedComment' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedComment' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedComment' + description: '' + patch: + operationId: common_comment_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedComment' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedComment' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedComment' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedComment' + description: '' + delete: + operationId: common_comment_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/comment/{id}/: + get: + operationId: common_comment_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + description: '' + put: + operationId: common_comment_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Comment' + multipart/form-data: + schema: + $ref: '#/components/schemas/Comment' + required: true + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + description: '' + patch: + operationId: common_comment_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedComment' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedComment' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedComment' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Comment' + description: '' + delete: + operationId: common_comment_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/parameter/: + get: + operationId: common_parameter_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedParameter' + description: '' + post: + operationId: common_parameter_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedParameter' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedParameter' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedParameter' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedParameter' + description: '' + put: + operationId: common_parameter_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedParameter' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedParameter' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedParameter' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedParameter' + description: '' + patch: + operationId: common_parameter_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedParameter' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedParameter' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedParameter' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedParameter' + description: '' + delete: + operationId: common_parameter_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/common/parameter/{id}/: + get: + operationId: common_parameter_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Parameter' + description: '' + put: + operationId: common_parameter_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Parameter' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Parameter' + multipart/form-data: + schema: + $ref: '#/components/schemas/Parameter' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Parameter' + description: '' + patch: + operationId: common_parameter_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedParameter' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedParameter' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedParameter' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Parameter' + description: '' + delete: + operationId: common_parameter_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - common + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/forecast/forecast/: + get: + operationId: forecast_forecast_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedForecast' + description: '' + post: + operationId: forecast_forecast_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecast' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedForecast' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedForecast' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecast' + description: '' + put: + operationId: forecast_forecast_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecast' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedForecast' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedForecast' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecast' + description: '' + patch: + operationId: forecast_forecast_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecast' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedForecast' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedForecast' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecast' + description: '' + delete: + operationId: forecast_forecast_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/forecast/forecast/{id}/: + get: + operationId: forecast_forecast_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Forecast' + description: '' + put: + operationId: forecast_forecast_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Forecast' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Forecast' + multipart/form-data: + schema: + $ref: '#/components/schemas/Forecast' + required: true + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Forecast' + description: '' + patch: + operationId: forecast_forecast_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecast' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedForecast' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedForecast' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Forecast' + description: '' + delete: + operationId: forecast_forecast_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/forecast/forecastplan/: + get: + operationId: forecast_forecastplan_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedForecastPlan' + description: '' + post: + operationId: forecast_forecastplan_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + description: '' + put: + operationId: forecast_forecastplan_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + description: '' + patch: + operationId: forecast_forecastplan_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedForecastPlan' + description: '' + delete: + operationId: forecast_forecastplan_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/forecast/measure/: + get: + operationId: forecast_measure_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedMeasure' + description: '' + post: + operationId: forecast_measure_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMeasure' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedMeasure' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedMeasure' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMeasure' + description: '' + put: + operationId: forecast_measure_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMeasure' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedMeasure' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedMeasure' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMeasure' + description: '' + patch: + operationId: forecast_measure_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMeasure' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedMeasure' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedMeasure' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedMeasure' + description: '' + delete: + operationId: forecast_measure_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/forecast/measure/{id}/: + get: + operationId: forecast_measure_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + description: No response body + put: + operationId: forecast_measure_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + description: No response body + patch: + operationId: forecast_measure_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + description: No response body + delete: + operationId: forecast_measure_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - forecast + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/buffer/: + get: + operationId: input_buffer_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedBuffer' + description: '' + post: + operationId: input_buffer_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBuffer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBuffer' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBuffer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBuffer' + description: '' + put: + operationId: input_buffer_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBuffer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBuffer' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBuffer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBuffer' + description: '' + patch: + operationId: input_buffer_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBuffer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBuffer' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBuffer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBuffer' + description: '' + delete: + operationId: input_buffer_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/buffer/{id}/: + get: + operationId: input_buffer_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Buffer' + description: '' + put: + operationId: input_buffer_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Buffer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Buffer' + multipart/form-data: + schema: + $ref: '#/components/schemas/Buffer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Buffer' + description: '' + patch: + operationId: input_buffer_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedBuffer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedBuffer' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedBuffer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Buffer' + description: '' + delete: + operationId: input_buffer_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/calendar/: + get: + operationId: input_calendar_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedCalendar' + description: '' + post: + operationId: input_calendar_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendar' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCalendar' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCalendar' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendar' + description: '' + put: + operationId: input_calendar_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendar' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCalendar' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCalendar' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendar' + description: '' + patch: + operationId: input_calendar_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendar' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCalendar' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCalendar' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendar' + description: '' + delete: + operationId: input_calendar_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/calendar/{id}/: + get: + operationId: input_calendar_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + description: '' + put: + operationId: input_calendar_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Calendar' + multipart/form-data: + schema: + $ref: '#/components/schemas/Calendar' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + description: '' + patch: + operationId: input_calendar_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendar' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCalendar' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCalendar' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Calendar' + description: '' + delete: + operationId: input_calendar_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/calendarbucket/: + get: + operationId: input_calendarbucket_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedCalendarBucket' + description: '' + post: + operationId: input_calendarbucket_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + description: '' + put: + operationId: input_calendarbucket_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + description: '' + patch: + operationId: input_calendarbucket_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + description: '' + delete: + operationId: input_calendarbucket_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/calendarbucket/{id}/: + get: + operationId: input_calendarbucket_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CalendarBucket' + description: '' + put: + operationId: input_calendarbucket_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/CalendarBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/CalendarBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/CalendarBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CalendarBucket' + description: '' + patch: + operationId: input_calendarbucket_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCalendarBucket' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/CalendarBucket' + description: '' + delete: + operationId: input_calendarbucket_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/customer/: + get: + operationId: input_customer_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedCustomer' + description: '' + post: + operationId: input_customer_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCustomer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCustomer' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCustomer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCustomer' + description: '' + put: + operationId: input_customer_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCustomer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCustomer' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCustomer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCustomer' + description: '' + patch: + operationId: input_customer_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCustomer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCustomer' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCustomer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCustomer' + description: '' + delete: + operationId: input_customer_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/customer/{id}/: + get: + operationId: input_customer_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + description: '' + put: + operationId: input_customer_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Customer' + multipart/form-data: + schema: + $ref: '#/components/schemas/Customer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + description: '' + patch: + operationId: input_customer_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedCustomer' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedCustomer' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedCustomer' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Customer' + description: '' + delete: + operationId: input_customer_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/deliveryorder/: + get: + operationId: input_deliveryorder_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedDeliveryOrder' + description: '' + post: + operationId: input_deliveryorder_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + description: '' + put: + operationId: input_deliveryorder_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + description: '' + patch: + operationId: input_deliveryorder_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + description: '' + delete: + operationId: input_deliveryorder_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/deliveryorder/{id}/: + get: + operationId: input_deliveryorder_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DeliveryOrder' + description: '' + put: + operationId: input_deliveryorder_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DeliveryOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/DeliveryOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/DeliveryOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DeliveryOrder' + description: '' + patch: + operationId: input_deliveryorder_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDeliveryOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DeliveryOrder' + description: '' + delete: + operationId: input_deliveryorder_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/demand/: + get: + operationId: input_demand_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedDemand' + description: '' + post: + operationId: input_demand_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDemand' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDemand' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDemand' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDemand' + description: '' + put: + operationId: input_demand_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDemand' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDemand' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDemand' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDemand' + description: '' + patch: + operationId: input_demand_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDemand' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDemand' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDemand' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDemand' + description: '' + delete: + operationId: input_demand_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/demand/{id}/: + get: + operationId: input_demand_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Demand' + description: '' + put: + operationId: input_demand_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Demand' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Demand' + multipart/form-data: + schema: + $ref: '#/components/schemas/Demand' + required: true + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Demand' + description: '' + patch: + operationId: input_demand_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDemand' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDemand' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDemand' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Demand' + description: '' + delete: + operationId: input_demand_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/distributionorder/: + get: + operationId: input_distributionorder_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedDistributionOrder' + description: '' + post: + operationId: input_distributionorder_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + description: '' + put: + operationId: input_distributionorder_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + description: '' + patch: + operationId: input_distributionorder_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + description: '' + delete: + operationId: input_distributionorder_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/distributionorder/{id}/: + get: + operationId: input_distributionorder_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionOrder' + description: '' + put: + operationId: input_distributionorder_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/DistributionOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/DistributionOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionOrder' + description: '' + patch: + operationId: input_distributionorder_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedDistributionOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/DistributionOrder' + description: '' + delete: + operationId: input_distributionorder_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/item/: + get: + operationId: input_item_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedItem' + description: '' + post: + operationId: input_item_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItem' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItem' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItem' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItem' + description: '' + put: + operationId: input_item_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItem' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItem' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItem' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItem' + description: '' + patch: + operationId: input_item_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItem' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItem' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItem' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItem' + description: '' + delete: + operationId: input_item_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/item/{id}/: + get: + operationId: input_item_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + description: '' + put: + operationId: input_item_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Item' + multipart/form-data: + schema: + $ref: '#/components/schemas/Item' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + description: '' + patch: + operationId: input_item_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItem' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItem' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItem' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Item' + description: '' + delete: + operationId: input_item_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/itemdistribution/: + get: + operationId: input_itemdistribution_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedItemDistribution' + description: '' + post: + operationId: input_itemdistribution_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + description: '' + put: + operationId: input_itemdistribution_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + description: '' + patch: + operationId: input_itemdistribution_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + description: '' + delete: + operationId: input_itemdistribution_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/itemdistribution/{id}/: + get: + operationId: input_itemdistribution_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ItemDistribution' + description: '' + put: + operationId: input_itemdistribution_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ItemDistribution' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ItemDistribution' + multipart/form-data: + schema: + $ref: '#/components/schemas/ItemDistribution' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ItemDistribution' + description: '' + patch: + operationId: input_itemdistribution_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItemDistribution' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ItemDistribution' + description: '' + delete: + operationId: input_itemdistribution_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/itemsupplier/: + get: + operationId: input_itemsupplier_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedItemSupplier' + description: '' + post: + operationId: input_itemsupplier_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + description: '' + put: + operationId: input_itemsupplier_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + description: '' + patch: + operationId: input_itemsupplier_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + description: '' + delete: + operationId: input_itemsupplier_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/itemsupplier/{id}/: + get: + operationId: input_itemsupplier_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ItemSupplier' + description: '' + put: + operationId: input_itemsupplier_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ItemSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ItemSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/ItemSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ItemSupplier' + description: '' + patch: + operationId: input_itemsupplier_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedItemSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ItemSupplier' + description: '' + delete: + operationId: input_itemsupplier_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/location/: + get: + operationId: input_location_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedLocation' + description: '' + post: + operationId: input_location_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedLocation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedLocation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedLocation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedLocation' + description: '' + put: + operationId: input_location_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedLocation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedLocation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedLocation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedLocation' + description: '' + patch: + operationId: input_location_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedLocation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedLocation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedLocation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedLocation' + description: '' + delete: + operationId: input_location_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/location/{id}/: + get: + operationId: input_location_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + description: '' + put: + operationId: input_location_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Location' + multipart/form-data: + schema: + $ref: '#/components/schemas/Location' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + description: '' + patch: + operationId: input_location_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedLocation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedLocation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedLocation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Location' + description: '' + delete: + operationId: input_location_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/manufacturingorder/: + get: + operationId: input_manufacturingorder_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedManufacturingOrder' + description: '' + post: + operationId: input_manufacturingorder_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + description: '' + put: + operationId: input_manufacturingorder_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + description: '' + patch: + operationId: input_manufacturingorder_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + description: '' + delete: + operationId: input_manufacturingorder_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/manufacturingorder/{id}/: + get: + operationId: input_manufacturingorder_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + description: '' + put: + operationId: input_manufacturingorder_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + description: '' + patch: + operationId: input_manufacturingorder_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + description: '' + delete: + operationId: input_manufacturingorder_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operation/: + get: + operationId: input_operation_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedOperation' + description: '' + post: + operationId: input_operation_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperation' + description: '' + put: + operationId: input_operation_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperation' + description: '' + patch: + operationId: input_operation_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperation' + description: '' + delete: + operationId: input_operation_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operation/{id}/: + get: + operationId: input_operation_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Operation' + description: '' + put: + operationId: input_operation_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Operation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Operation' + multipart/form-data: + schema: + $ref: '#/components/schemas/Operation' + required: true + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Operation' + description: '' + patch: + operationId: input_operation_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Operation' + description: '' + delete: + operationId: input_operation_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationdependency/: + get: + operationId: input_operationdependency_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedOperationDependency' + description: '' + post: + operationId: input_operationdependency_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + description: '' + put: + operationId: input_operationdependency_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + description: '' + patch: + operationId: input_operationdependency_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + description: '' + delete: + operationId: input_operationdependency_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationdependency/{id}/: + get: + operationId: input_operationdependency_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationDependency' + description: '' + put: + operationId: input_operationdependency_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationDependency' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/OperationDependency' + multipart/form-data: + schema: + $ref: '#/components/schemas/OperationDependency' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationDependency' + description: '' + patch: + operationId: input_operationdependency_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationDependency' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationDependency' + description: '' + delete: + operationId: input_operationdependency_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationmaterial/: + get: + operationId: input_operationmaterial_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedOperationMaterial' + description: '' + post: + operationId: input_operationmaterial_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + description: '' + put: + operationId: input_operationmaterial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + description: '' + patch: + operationId: input_operationmaterial_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + description: '' + delete: + operationId: input_operationmaterial_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationmaterial/{id}/: + get: + operationId: input_operationmaterial_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationMaterial' + description: '' + put: + operationId: input_operationmaterial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/OperationMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/OperationMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationMaterial' + description: '' + patch: + operationId: input_operationmaterial_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationMaterial' + description: '' + delete: + operationId: input_operationmaterial_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationplanmaterial/: + get: + operationId: input_operationplanmaterial_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + description: '' + post: + operationId: input_operationplanmaterial_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + description: '' + put: + operationId: input_operationplanmaterial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + description: '' + patch: + operationId: input_operationplanmaterial_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + description: '' + delete: + operationId: input_operationplanmaterial_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationplanmaterial/{id}/: + get: + operationId: input_operationplanmaterial_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationPlanMaterial' + description: '' + put: + operationId: input_operationplanmaterial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationPlanMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/OperationPlanMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/OperationPlanMaterial' + required: true + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationPlanMaterial' + description: '' + patch: + operationId: input_operationplanmaterial_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationPlanMaterial' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationPlanMaterial' + description: '' + delete: + operationId: input_operationplanmaterial_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationplanresource/: + get: + operationId: input_operationplanresource_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedOperationPlanResource' + description: '' + post: + operationId: input_operationplanresource_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + description: '' + put: + operationId: input_operationplanresource_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + description: '' + patch: + operationId: input_operationplanresource_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + description: '' + delete: + operationId: input_operationplanresource_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationplanresource/{id}/: + get: + operationId: input_operationplanresource_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationPlanResource' + description: '' + put: + operationId: input_operationplanresource_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationPlanResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/OperationPlanResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/OperationPlanResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationPlanResource' + description: '' + patch: + operationId: input_operationplanresource_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationPlanResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationPlanResource' + description: '' + delete: + operationId: input_operationplanresource_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationresource/: + get: + operationId: input_operationresource_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedOperationResource' + description: '' + post: + operationId: input_operationresource_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + description: '' + put: + operationId: input_operationresource_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + description: '' + patch: + operationId: input_operationresource_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + description: '' + delete: + operationId: input_operationresource_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/operationresource/{id}/: + get: + operationId: input_operationresource_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationResource' + description: '' + put: + operationId: input_operationresource_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/OperationResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/OperationResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/OperationResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationResource' + description: '' + patch: + operationId: input_operationresource_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedOperationResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/OperationResource' + description: '' + delete: + operationId: input_operationresource_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/purchaseorder/: + get: + operationId: input_purchaseorder_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedPurchaseOrder' + description: '' + post: + operationId: input_purchaseorder_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + description: '' + put: + operationId: input_purchaseorder_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + description: '' + patch: + operationId: input_purchaseorder_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + description: '' + delete: + operationId: input_purchaseorder_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/purchaseorder/{id}/: + get: + operationId: input_purchaseorder_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PurchaseOrder' + description: '' + put: + operationId: input_purchaseorder_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PurchaseOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PurchaseOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PurchaseOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PurchaseOrder' + description: '' + patch: + operationId: input_purchaseorder_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedPurchaseOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PurchaseOrder' + description: '' + delete: + operationId: input_purchaseorder_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/resource/: + get: + operationId: input_resource_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedResource' + description: '' + post: + operationId: input_resource_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResource' + description: '' + put: + operationId: input_resource_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResource' + description: '' + patch: + operationId: input_resource_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResource' + description: '' + delete: + operationId: input_resource_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/resource/{id}/: + get: + operationId: input_resource_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: '' + put: + operationId: input_resource_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Resource' + multipart/form-data: + schema: + $ref: '#/components/schemas/Resource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: '' + patch: + operationId: input_resource_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResource' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedResource' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedResource' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Resource' + description: '' + delete: + operationId: input_resource_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/resourceskill/: + get: + operationId: input_resourceskill_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedResourceSkill' + description: '' + post: + operationId: input_resourceskill_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + description: '' + put: + operationId: input_resourceskill_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + description: '' + patch: + operationId: input_resourceskill_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + description: '' + delete: + operationId: input_resourceskill_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/resourceskill/{id}/: + get: + operationId: input_resourceskill_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceSkill' + description: '' + put: + operationId: input_resourceskill_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ResourceSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/ResourceSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceSkill' + description: '' + patch: + operationId: input_resourceskill_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedResourceSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ResourceSkill' + description: '' + delete: + operationId: input_resourceskill_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/setupmatrix/: + get: + operationId: input_setupmatrix_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedSetupMatrix' + description: '' + post: + operationId: input_setupmatrix_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + description: '' + put: + operationId: input_setupmatrix_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + description: '' + patch: + operationId: input_setupmatrix_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + description: '' + delete: + operationId: input_setupmatrix_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/setupmatrix/{id}/: + get: + operationId: input_setupmatrix_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SetupMatrix' + description: '' + put: + operationId: input_setupmatrix_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SetupMatrix' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SetupMatrix' + multipart/form-data: + schema: + $ref: '#/components/schemas/SetupMatrix' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SetupMatrix' + description: '' + patch: + operationId: input_setupmatrix_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSetupMatrix' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SetupMatrix' + description: '' + delete: + operationId: input_setupmatrix_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/setuprule/: + get: + operationId: input_setuprule_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedSetupRule' + description: '' + post: + operationId: input_setuprule_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + description: '' + put: + operationId: input_setuprule_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + description: '' + patch: + operationId: input_setuprule_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + description: '' + delete: + operationId: input_setuprule_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/setuprule/{id}/: + get: + operationId: input_setuprule_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SetupRule' + description: '' + put: + operationId: input_setuprule_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SetupRule' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SetupRule' + multipart/form-data: + schema: + $ref: '#/components/schemas/SetupRule' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SetupRule' + description: '' + patch: + operationId: input_setuprule_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSetupRule' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SetupRule' + description: '' + delete: + operationId: input_setuprule_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/skill/: + get: + operationId: input_skill_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedSkill' + description: '' + post: + operationId: input_skill_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSkill' + description: '' + put: + operationId: input_skill_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSkill' + description: '' + patch: + operationId: input_skill_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSkill' + description: '' + delete: + operationId: input_skill_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/skill/{id}/: + get: + operationId: input_skill_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Skill' + description: '' + put: + operationId: input_skill_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Skill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Skill' + multipart/form-data: + schema: + $ref: '#/components/schemas/Skill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Skill' + description: '' + patch: + operationId: input_skill_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSkill' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSkill' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSkill' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Skill' + description: '' + delete: + operationId: input_skill_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/suboperation/: + get: + operationId: input_suboperation_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedSubOperation' + description: '' + post: + operationId: input_suboperation_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + description: '' + put: + operationId: input_suboperation_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + description: '' + patch: + operationId: input_suboperation_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + description: '' + delete: + operationId: input_suboperation_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/suboperation/{id}/: + get: + operationId: input_suboperation_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SubOperation' + description: '' + put: + operationId: input_suboperation_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/SubOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/SubOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/SubOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SubOperation' + description: '' + patch: + operationId: input_suboperation_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSubOperation' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/SubOperation' + description: '' + delete: + operationId: input_suboperation_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/supplier/: + get: + operationId: input_supplier_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedSupplier' + description: '' + post: + operationId: input_supplier_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSupplier' + description: '' + put: + operationId: input_supplier_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSupplier' + description: '' + patch: + operationId: input_supplier_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSupplier' + description: '' + delete: + operationId: input_supplier_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/supplier/{id}/: + get: + operationId: input_supplier_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Supplier' + description: '' + put: + operationId: input_supplier_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/Supplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/Supplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/Supplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Supplier' + description: '' + patch: + operationId: input_supplier_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedSupplier' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedSupplier' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedSupplier' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/Supplier' + description: '' + delete: + operationId: input_supplier_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/workorder/: + get: + operationId: input_workorder_list + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + type: array + items: + $ref: '#/components/schemas/PatchedWorkOrder' + description: '' + post: + operationId: input_workorder_create + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + description: '' + put: + operationId: input_workorder_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + description: '' + patch: + operationId: input_workorder_partial_update + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedWorkOrder' + description: '' + delete: + operationId: input_workorder_destroy + description: |- + Customized API view for the REST framework.: + - support for request-specific scenario database + - add 'title' to the context of the html view + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body + /api/input/workorder/{id}/: + get: + operationId: input_workorder_retrieve + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + description: '' + put: + operationId: input_workorder_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + description: '' + patch: + operationId: input_workorder_partial_update_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + requestBody: + content: + application/json: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + application/x-www-form-urlencoded: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + multipart/form-data: + schema: + $ref: '#/components/schemas/PatchedManufacturingOrder' + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '200': + content: + application/json: + schema: + $ref: '#/components/schemas/ManufacturingOrder' + description: '' + delete: + operationId: input_workorder_destroy_2 + description: |- + Customized API view for the REST framework. + - support for request-specific scenario database + - add 'title' to the context of the html view + parameters: + - in: path + name: id + schema: + type: string + pattern: ^(.+)$ + required: true + tags: + - input + security: + - basicAuth: [] + - cookieAuth: [] + responses: + '204': + description: No response body +components: + schemas: + Attribute: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + model: + $ref: '#/components/schemas/ModelEnum' + name: + type: string + label: + type: string + editable: + type: boolean + initially_hidden: + type: boolean + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - label + - lastmodified + BlankEnum: + enum: + - '' + Bucket: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: + type: string + nullable: true + level: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Higher values indicate more granular time buckets + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - lastmodified + - level + BucketDetail: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + bucket: + type: string + name: + type: string + startdate: + type: string + format: date-time + title: Start date + enddate: + type: string + format: date-time + title: End date + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - enddate + - lastmodified + - name + Buffer: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/BufferTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + location: + type: string + description: Unique identifier + item: + type: string + description: Unique identifier + batch: + type: string + nullable: true + default: '' + onhand: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: current inventory + minimum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: safety stock + minimum_calendar: + type: string + nullable: true + description: Calendar storing a time-dependent safety stock profile + maximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: maximum stock + maximum_calendar: + type: string + nullable: true + description: Calendar storing a time-dependent maximum stock profile + min_interval: + type: string + nullable: true + description: Batching window for grouping replenishments in batches + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + BufferTypeEnum: + enum: + - default + - infinite + type: string + description: |- + * `default` - default + * `infinite` - infinite + Calendar: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + defaultvalue: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Default value + description: Value to be used when no entry is effective + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - lastmodified + CalendarBucket: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + calendar: + type: string + startdate: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + title: Start date + enddate: + type: string + format: date-time + nullable: true + default: '2030-12-31T00:00:00' + title: End date + value: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + default: 0 + monday: + type: boolean + tuesday: + type: boolean + wednesday: + type: boolean + thursday: + type: boolean + friday: + type: boolean + saturday: + type: boolean + sunday: + type: boolean + starttime: + type: string + format: time + nullable: true + title: Start time + endtime: + type: string + format: time + nullable: true + title: End time + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + Comment: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + object_pk: + type: string + title: Object id + comment: + type: string + title: Message + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + content_type: + type: integer + user: + type: integer + readOnly: true + nullable: true + type: + $ref: '#/components/schemas/CommentTypeEnum' + required: + - comment + - content_type + - id + - lastmodified + - object_pk + - user + CommentTypeEnum: + enum: + - add + - change + - delete + - comment + - follower + type: string + description: |- + * `add` - Add + * `change` - Change + * `delete` - Delete + * `comment` - comment + * `follower` - follower + Customer: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + owner: + type: string + description: Hierarchical parent + nullable: true + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - lastmodified + DeliveryOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + demand: + type: string + description: Unique identifier + nullable: true + item: + type: string + description: Unique identifier + nullable: true + location: + type: string + description: Unique identifier + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + due: + type: string + format: date-time + readOnly: true + nullable: true + description: Due date of the demand/forecast + batch: + type: string + nullable: true + description: MTO batch name + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + forecast: + type: string + readOnly: true + nullable: true + required: + - delay + - due + - forecast + - lastmodified + - plan + Demand: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + item: + type: string + description: Unique identifier + customer: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + due: + type: string + format: date-time + description: Due date of the sales order + status: + nullable: true + description: |- + Status of the demand. Only "open" and "quote" demands are planned + + * `inquiry` - inquiry + * `quote` - quote + * `open` - open + * `closed` - closed + * `canceled` - canceled + oneOf: + - $ref: '#/components/schemas/DemandStatusEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + operation: + type: string + nullable: true + title: Delivery operation + description: Operation used to satisfy this demand + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Priority of the demand (lower numbers indicate more important + demands) + batch: + type: string + nullable: true + description: MTO batch name + delay: + type: string + readOnly: true + nullable: true + plannedquantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + title: Planned quantity + description: Quantity planned for delivery + deliverydate: + type: string + format: date-time + readOnly: true + nullable: true + title: Delivery date + description: Delivery date of the demand + plan: + readOnly: true + nullable: true + minshipment: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Minimum shipment + description: Minimum shipment quantity when planning this demand + maxlateness: + type: string + nullable: true + title: Maximum lateness + description: Maximum lateness allowed when planning this demand + owner: + type: string + nullable: true + policy: + nullable: true + description: |- + Defines how sales orders are shipped together + + * `independent` - independent + * `alltogether` - all together + * `inratio` - in ratio + oneOf: + - $ref: '#/components/schemas/PolicyEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + required: + - customer + - delay + - deliverydate + - due + - item + - location + - plan + - plannedquantity + - quantity + DemandStatusEnum: + enum: + - inquiry + - quote + - open + - closed + - canceled + type: string + description: |- + * `inquiry` - inquiry + * `quote` - quote + * `open` - open + * `closed` - closed + * `canceled` - canceled + DistributionOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + item: + type: string + description: Unique identifier + nullable: true + origin: + type: string + description: Unique identifier + nullable: true + destination: + type: string + description: Unique identifier + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + batch: + type: string + nullable: true + description: MTO batch name + criticality: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + forecast: + type: string + readOnly: true + nullable: true + required: + - criticality + - delay + - forecast + - lastmodified + - plan + Forecast: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + batch: + type: string + nullable: true + description: MTO batch name + customer: + type: string + description: Unique identifier + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + operation: + type: string + nullable: true + title: Delivery operation + description: Operation used to satisfy this demand + method: + nullable: true + title: Forecast method + description: |- + Method used to generate a base forecast + + * `automatic` - Automatic + * `constant` - Constant + * `trend` - Trend + * `seasonal` - Seasonal + * `intermittent` - Intermittent + * `moving average` - Moving average + * `manual` - Manual + * `aggregate` - Aggregate + oneOf: + - $ref: '#/components/schemas/MethodEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Priority of the demand (lower numbers indicate more important + demands) + minshipment: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Minimum shipment + description: Minimum shipment quantity when planning this demand + maxlateness: + type: string + nullable: true + title: Maximum lateness + description: Maximum lateness allowed when planning this demand + discrete: + type: boolean + description: Round forecast numbers to integers + out_smape: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Estimated forecast error + out_method: + type: string + nullable: true + title: Calculated forecast method + out_deviation: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Calculated standard deviation + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - customer + - item + - lastmodified + - location + Item: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + owner: + type: string + description: Hierarchical parent + nullable: true + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Cost of the item + volume: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Volume of the item + weight: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Weight of the item + periodofcover: + type: integer + readOnly: true + nullable: true + title: Period of cover + description: Period of cover in days + uom: + type: string + nullable: true + title: Unit of measure + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/ItemTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + latedemandcount: + type: integer + readOnly: true + nullable: true + title: Count of late demands + latedemandquantity: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Quantity of late demands + latedemandvalue: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Value of late demand + unplanneddemandcount: + type: integer + readOnly: true + nullable: true + title: Count of unplanned demands + unplanneddemandquantity: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Quantity of unplanned demands + unplanneddemandvalue: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Value of unplanned demands + demand_pattern: + type: string + readOnly: true + nullable: true + adi: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + cv2: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + outlier_1b: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Outliers last bucket + outlier_6b: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Outliers last 6 buckets + outlier_12b: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Outliers last 12 buckets + required: + - adi + - cv2 + - demand_pattern + - lastmodified + - latedemandcount + - latedemandquantity + - latedemandvalue + - outlier_12b + - outlier_1b + - outlier_6b + - periodofcover + - unplanneddemandcount + - unplanneddemandquantity + - unplanneddemandvalue + ItemDistribution: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + item: + type: string + description: Unique identifier + location: + type: string + description: Destination location to be replenished + origin: + type: string + description: Source location shipping the item + leadtime: + type: string + nullable: true + title: Lead time + description: Transport lead time + sizeminimum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size minimum + description: A minimum shipping quantity + sizemultiple: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size multiple + description: A multiple shipping quantity + sizemaximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size maximum + description: A maximum shipping quantity + batchwindow: + type: string + nullable: true + title: Batching window + description: Proposed distribution orders within this window will be grouped + together + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Shipping cost per unit + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority among all alternates + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + ItemSupplier: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + nullable: true + supplier: + type: string + description: Unique identifier + leadtime: + type: string + nullable: true + title: Lead time + description: Purchasing lead time + sizeminimum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size minimum + description: A minimum purchasing quantity + sizemultiple: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size multiple + description: A multiple purchasing quantity + sizemaximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size maximum + description: A maximum purchasing quantity + batchwindow: + type: string + nullable: true + title: Batching window + description: Proposed purchase orders within this window will be grouped + together + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Purchasing cost per unit + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority among all alternates + hard_safety_leadtime: + type: string + nullable: true + title: Hard safety lead time + description: hard safety lead time + extra_safety_leadtime: + type: string + nullable: true + title: Soft safety lead time + description: soft safety lead time + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + ItemTypeEnum: + enum: + - make to stock + - make to order + type: string + description: |- + * `make to stock` - make to stock + * `make to order` - make to order + Location: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + owner: + type: string + description: Hierarchical parent + nullable: true + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + available: + type: string + nullable: true + description: Calendar defining the working hours and holidays + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - lastmodified + ManufacturingOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + operation: + type: string + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + quantity_completed: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Completed quantity + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + batch: + type: string + nullable: true + description: MTO batch name + criticality: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + owner: + type: string + description: Hierarchical parent + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + resources: + type: array + items: + $ref: '#/components/schemas/OperationPlanResourceNested' + materials: + type: array + items: + $ref: '#/components/schemas/OperationPlanMaterialNested' + demand: + type: string + description: Unique identifier + nullable: true + forecast: + type: string + readOnly: true + nullable: true + required: + - criticality + - delay + - forecast + - lastmodified + - plan + MeasureTypeEnum: + enum: + - aggregate + - local + - computed + type: string + description: |- + * `aggregate` - aggregate + * `local` - local + * `computed` - computed + MethodEnum: + enum: + - automatic + - constant + - trend + - seasonal + - intermittent + - moving average + - manual + - aggregate + type: string + description: |- + * `automatic` - Automatic + * `constant` - Constant + * `trend` - Trend + * `seasonal` - Seasonal + * `intermittent` - Intermittent + * `moving average` - Moving average + * `manual` - Manual + * `aggregate` - Aggregate + ModeFutureEnum: + enum: + - edit + - view + - hide + type: string + description: |- + * `edit` - edit + * `view` - view + * `hide` - hide + ModePastEnum: + enum: + - edit + - view + - hide + type: string + description: |- + * `edit` - edit + * `view` - view + * `hide` - hide + ModelEnum: + enum: + - calendar + - supplier + - item + - location + - customer + - demand + - operation + - operationplan + - operationplanmaterial + - setupmatrix + - skill + - resource + - operationplanresource + - suboperation + - setuprule + - resourceskill + - operationresource + - operationmaterial + - itemsupplier + - itemdistribution + - calendarbucket + - buffer + - operationdependency + - workorder + - forecast + - constraint + - problem + - user + - bucket + - bucketdetail + - apikey + type: string + description: |- + * `calendar` - calendar + * `supplier` - supplier + * `item` - item + * `location` - location + * `customer` - customer + * `demand` - demand + * `operation` - operation + * `operationplan` - operationplan + * `operationplanmaterial` - operationplanmaterial + * `setupmatrix` - setupmatrix + * `skill` - skill + * `resource` - resource + * `operationplanresource` - operationplanresource + * `suboperation` - suboperation + * `setuprule` - setuprule + * `resourceskill` - resourceskill + * `operationresource` - operationresource + * `operationmaterial` - operationmaterial + * `itemsupplier` - itemsupplier + * `itemdistribution` - itemdistribution + * `calendarbucket` - calendarbucket + * `buffer` - buffer + * `operationdependency` - operationdependency + * `workorder` - workorder + * `forecast` - forecast + * `constraint` - constraint + * `problem` - problem + * `user` - user + * `bucket` - bucket + * `bucketdetail` - bucketdetail + * `apikey` - apikey + NullEnum: + enum: + - null + Operation: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/OperationTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + item: + type: string + description: Item produced by this operation + nullable: true + location: + type: string + description: Unique identifier + fence: + type: string + nullable: true + title: Release fence + description: Operationplans within this time window from the current day + are expected to be released to production ERP + batchwindow: + type: string + nullable: true + title: Batching window + description: The solver algorithm will scan for opportunities to create + batches within this time window before and after the requirement date + posttime: + type: string + nullable: true + title: Post-op time + description: A delay time to be respected as a soft constraint after ending + the operation + sizeminimum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size minimum + description: Minimum production quantity + sizemultiple: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size multiple + description: Multiple production quantity + sizemaximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size maximum + description: Maximum production quantity + owner: + type: string + nullable: true + description: Parent operation (which must be of type routing, alternate + or split) + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority among all alternates + effective_start: + type: string + format: date-time + nullable: true + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + available: + type: string + nullable: true + description: Calendar defining the working hours and holidays + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Cost per produced unit + duration: + type: string + nullable: true + description: Fixed production time for setup and overhead + duration_per: + type: string + nullable: true + title: Duration per unit + description: Production time per produced piece + search: + nullable: true + title: Search mode + description: |- + Method to select preferred alternate + + * `PRIORITY` - priority + * `MINCOST` - minimum cost + * `MINPENALTY` - minimum penalty + * `MINCOSTPENALTY` - minimum cost plus penalty + oneOf: + - $ref: '#/components/schemas/SearchEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - lastmodified + - location + OperationDependency: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operation: + type: string + description: operation + blockedby: + type: string + title: Blocked by operation + description: blocked by operation + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Quantity relation between the operations + safety_leadtime: + type: string + nullable: true + title: Soft safety lead time + description: soft safety lead time + hard_safety_leadtime: + type: string + nullable: true + title: Hard safety lead time + description: hard safety lead time + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + OperationMaterial: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operation: + type: string + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Quantity to consume or produce per piece + quantity_fixed: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Fixed quantity + description: Fixed quantity to consume or produce + transferbatch: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Transfer batch quantity + description: Batch size by in which material is produced or consumed + offset: + type: string + nullable: true + description: Time offset from the start or end to consume or produce material + type: + nullable: true + description: |- + Consume/produce material at the start or the end of the operationplan + + * `start` - Start + * `end` - End + * `transfer_batch` - Batch transfer + oneOf: + - $ref: '#/components/schemas/OperationMaterialTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + name: + type: string + nullable: true + description: Name of this operation material to identify alternates + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority of this operation material in a group of alternates + search: + nullable: true + title: Search mode + description: |- + Method to select preferred alternate + + * `PRIORITY` - priority + * `MINCOST` - minimum cost + * `MINPENALTY` - minimum penalty + * `MINCOSTPENALTY` - minimum cost plus penalty + oneOf: + - $ref: '#/components/schemas/SearchEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + OperationMaterialTypeEnum: + enum: + - start + - end + - transfer_batch + type: string + description: |- + * `start` - Start + * `end` - End + * `transfer_batch` - Batch transfer + OperationPlanMaterial: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operationplan: + type: string + description: Unique identifier + title: Reference + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + onhand: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + flowdate: + type: string + format: date-time + title: Date + status: + nullable: true + title: Material status + description: |- + status of the material production or consumption + + * `proposed` - proposed + * `confirmed` - confirmed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/Status9c1Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - flowdate + - id + - item + - lastmodified + - location + - operationplan + - quantity + OperationPlanMaterialNested: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + item: + type: string + description: Unique identifier + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + flowdate: + type: string + format: date-time + title: Date + required: + - flowdate + - item + - quantity + OperationPlanResource: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operationplan: + type: string + description: Unique identifier + title: Reference + resource: + type: string + description: Unique identifier + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + startdate: + type: string + readOnly: true + enddate: + type: string + readOnly: true + status: + nullable: true + title: Load status + description: |- + Status of the resource assignment + + * `proposed` - proposed + * `confirmed` - confirmed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/Status9c1Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + setup: + type: string + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - enddate + - id + - lastmodified + - startdate + OperationPlanResourceNested: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + resource: + type: string + description: Unique identifier + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + setup: + type: string + nullable: true + OperationResource: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operation: + type: string + resource: + type: string + description: Unique identifier + skill: + type: string + description: Required skill to perform the operation + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Required quantity of the resource + quantity_fixed: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Constant part of the capacity consumption (bucketized resources + only) + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + name: + type: string + nullable: true + description: Name of this operation resource to identify alternates + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority of this operation resource in a group of alternates + setup: + type: string + nullable: true + description: Setup required on the resource for this operation + search: + nullable: true + title: Search mode + description: |- + Method to select preferred alternate + + * `PRIORITY` - priority + * `MINCOST` - minimum cost + * `MINPENALTY` - minimum penalty + * `MINCOSTPENALTY` - minimum cost plus penalty + oneOf: + - $ref: '#/components/schemas/SearchEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + OperationTypeEnum: + enum: + - fixed_time + - time_per + - routing + - alternate + - split + type: string + description: |- + * `fixed_time` - fixed_time + * `time_per` - time_per + * `routing` - routing + * `alternate` - alternate + * `split` - split + Parameter: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + value: + type: string + nullable: true + description: + type: string + nullable: true + required: + - lastmodified + PatchedAttribute: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + model: + $ref: '#/components/schemas/ModelEnum' + name: + type: string + label: + type: string + editable: + type: boolean + initially_hidden: + type: boolean + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedBucket: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: + type: string + nullable: true + level: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Higher values indicate more granular time buckets + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedBucketDetail: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + bucket: + type: string + name: + type: string + startdate: + type: string + format: date-time + title: Start date + enddate: + type: string + format: date-time + title: End date + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedBuffer: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/BufferTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + location: + type: string + description: Unique identifier + item: + type: string + description: Unique identifier + batch: + type: string + nullable: true + default: '' + onhand: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: current inventory + minimum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: safety stock + minimum_calendar: + type: string + nullable: true + description: Calendar storing a time-dependent safety stock profile + maximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: maximum stock + maximum_calendar: + type: string + nullable: true + description: Calendar storing a time-dependent maximum stock profile + min_interval: + type: string + nullable: true + description: Batching window for grouping replenishments in batches + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedCalendar: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + defaultvalue: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Default value + description: Value to be used when no entry is effective + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedCalendarBucket: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + calendar: + type: string + startdate: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + title: Start date + enddate: + type: string + format: date-time + nullable: true + default: '2030-12-31T00:00:00' + title: End date + value: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + default: 0 + monday: + type: boolean + tuesday: + type: boolean + wednesday: + type: boolean + thursday: + type: boolean + friday: + type: boolean + saturday: + type: boolean + sunday: + type: boolean + starttime: + type: string + format: time + nullable: true + title: Start time + endtime: + type: string + format: time + nullable: true + title: End time + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedComment: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + object_pk: + type: string + title: Object id + comment: + type: string + title: Message + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + content_type: + type: integer + user: + type: integer + readOnly: true + nullable: true + type: + $ref: '#/components/schemas/CommentTypeEnum' + PatchedCustomer: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + owner: + type: string + description: Hierarchical parent + nullable: true + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedDeliveryOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + demand: + type: string + description: Unique identifier + nullable: true + item: + type: string + description: Unique identifier + nullable: true + location: + type: string + description: Unique identifier + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + due: + type: string + format: date-time + readOnly: true + nullable: true + description: Due date of the demand/forecast + batch: + type: string + nullable: true + description: MTO batch name + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + forecast: + type: string + readOnly: true + nullable: true + PatchedDemand: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + item: + type: string + description: Unique identifier + customer: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + due: + type: string + format: date-time + description: Due date of the sales order + status: + nullable: true + description: |- + Status of the demand. Only "open" and "quote" demands are planned + + * `inquiry` - inquiry + * `quote` - quote + * `open` - open + * `closed` - closed + * `canceled` - canceled + oneOf: + - $ref: '#/components/schemas/DemandStatusEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + operation: + type: string + nullable: true + title: Delivery operation + description: Operation used to satisfy this demand + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Priority of the demand (lower numbers indicate more important + demands) + batch: + type: string + nullable: true + description: MTO batch name + delay: + type: string + readOnly: true + nullable: true + plannedquantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + title: Planned quantity + description: Quantity planned for delivery + deliverydate: + type: string + format: date-time + readOnly: true + nullable: true + title: Delivery date + description: Delivery date of the demand + plan: + readOnly: true + nullable: true + minshipment: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Minimum shipment + description: Minimum shipment quantity when planning this demand + maxlateness: + type: string + nullable: true + title: Maximum lateness + description: Maximum lateness allowed when planning this demand + owner: + type: string + nullable: true + policy: + nullable: true + description: |- + Defines how sales orders are shipped together + + * `independent` - independent + * `alltogether` - all together + * `inratio` - in ratio + oneOf: + - $ref: '#/components/schemas/PolicyEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + PatchedDistributionOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + item: + type: string + description: Unique identifier + nullable: true + origin: + type: string + description: Unique identifier + nullable: true + destination: + type: string + description: Unique identifier + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + batch: + type: string + nullable: true + description: MTO batch name + criticality: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + forecast: + type: string + readOnly: true + nullable: true + PatchedForecast: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + batch: + type: string + nullable: true + description: MTO batch name + customer: + type: string + description: Unique identifier + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + operation: + type: string + nullable: true + title: Delivery operation + description: Operation used to satisfy this demand + method: + nullable: true + title: Forecast method + description: |- + Method used to generate a base forecast + + * `automatic` - Automatic + * `constant` - Constant + * `trend` - Trend + * `seasonal` - Seasonal + * `intermittent` - Intermittent + * `moving average` - Moving average + * `manual` - Manual + * `aggregate` - Aggregate + oneOf: + - $ref: '#/components/schemas/MethodEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Priority of the demand (lower numbers indicate more important + demands) + minshipment: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Minimum shipment + description: Minimum shipment quantity when planning this demand + maxlateness: + type: string + nullable: true + title: Maximum lateness + description: Maximum lateness allowed when planning this demand + discrete: + type: boolean + description: Round forecast numbers to integers + out_smape: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Estimated forecast error + out_method: + type: string + nullable: true + title: Calculated forecast method + out_deviation: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Calculated standard deviation + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedForecastPlan: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + customer: + type: string + description: Unique identifier + startdate: + type: string + format: date-time + title: Start date + enddate: + type: string + format: date-time + title: End date + value: {} + PatchedItem: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + owner: + type: string + description: Hierarchical parent + nullable: true + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Cost of the item + volume: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Volume of the item + weight: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Weight of the item + periodofcover: + type: integer + readOnly: true + nullable: true + title: Period of cover + description: Period of cover in days + uom: + type: string + nullable: true + title: Unit of measure + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/ItemTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + latedemandcount: + type: integer + readOnly: true + nullable: true + title: Count of late demands + latedemandquantity: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Quantity of late demands + latedemandvalue: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Value of late demand + unplanneddemandcount: + type: integer + readOnly: true + nullable: true + title: Count of unplanned demands + unplanneddemandquantity: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Quantity of unplanned demands + unplanneddemandvalue: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Value of unplanned demands + demand_pattern: + type: string + readOnly: true + nullable: true + adi: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + cv2: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + outlier_1b: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Outliers last bucket + outlier_6b: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Outliers last 6 buckets + outlier_12b: + type: string + format: decimal + pattern: ^-?\d{0,9}(?:\.\d{0,6})?$ + readOnly: true + nullable: true + title: Outliers last 12 buckets + PatchedItemDistribution: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + item: + type: string + description: Unique identifier + location: + type: string + description: Destination location to be replenished + origin: + type: string + description: Source location shipping the item + leadtime: + type: string + nullable: true + title: Lead time + description: Transport lead time + sizeminimum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size minimum + description: A minimum shipping quantity + sizemultiple: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size multiple + description: A multiple shipping quantity + sizemaximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size maximum + description: A maximum shipping quantity + batchwindow: + type: string + nullable: true + title: Batching window + description: Proposed distribution orders within this window will be grouped + together + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Shipping cost per unit + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority among all alternates + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedItemSupplier: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + nullable: true + supplier: + type: string + description: Unique identifier + leadtime: + type: string + nullable: true + title: Lead time + description: Purchasing lead time + sizeminimum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size minimum + description: A minimum purchasing quantity + sizemultiple: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size multiple + description: A multiple purchasing quantity + sizemaximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size maximum + description: A maximum purchasing quantity + batchwindow: + type: string + nullable: true + title: Batching window + description: Proposed purchase orders within this window will be grouped + together + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Purchasing cost per unit + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority among all alternates + hard_safety_leadtime: + type: string + nullable: true + title: Hard safety lead time + description: hard safety lead time + extra_safety_leadtime: + type: string + nullable: true + title: Soft safety lead time + description: soft safety lead time + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedLocation: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + owner: + type: string + description: Hierarchical parent + nullable: true + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + available: + type: string + nullable: true + description: Calendar defining the working hours and holidays + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedManufacturingOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + operation: + type: string + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + quantity_completed: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Completed quantity + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + batch: + type: string + nullable: true + description: MTO batch name + criticality: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + owner: + type: string + description: Hierarchical parent + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + resources: + type: array + items: + $ref: '#/components/schemas/OperationPlanResourceNested' + materials: + type: array + items: + $ref: '#/components/schemas/OperationPlanMaterialNested' + demand: + type: string + description: Unique identifier + nullable: true + forecast: + type: string + readOnly: true + nullable: true + PatchedMeasure: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + label: + type: string + nullable: true + description: Label to be displayed in the user interface + description: + type: string + nullable: true + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/MeasureTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + discrete: + type: boolean + nullable: true + defaultvalue: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Default value + mode_future: + nullable: true + title: Mode in future periods + oneOf: + - $ref: '#/components/schemas/ModeFutureEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + mode_past: + nullable: true + title: Mode in past periods + oneOf: + - $ref: '#/components/schemas/ModePastEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + compute_expression: + type: string + nullable: true + description: Formula to compute values + update_expression: + type: string + nullable: true + description: Formula executed when updating this field + overrides: + type: string + nullable: true + title: Override measure + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedOperation: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/OperationTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + item: + type: string + description: Item produced by this operation + nullable: true + location: + type: string + description: Unique identifier + fence: + type: string + nullable: true + title: Release fence + description: Operationplans within this time window from the current day + are expected to be released to production ERP + batchwindow: + type: string + nullable: true + title: Batching window + description: The solver algorithm will scan for opportunities to create + batches within this time window before and after the requirement date + posttime: + type: string + nullable: true + title: Post-op time + description: A delay time to be respected as a soft constraint after ending + the operation + sizeminimum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size minimum + description: Minimum production quantity + sizemultiple: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size multiple + description: Multiple production quantity + sizemaximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Size maximum + description: Maximum production quantity + owner: + type: string + nullable: true + description: Parent operation (which must be of type routing, alternate + or split) + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority among all alternates + effective_start: + type: string + format: date-time + nullable: true + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + available: + type: string + nullable: true + description: Calendar defining the working hours and holidays + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Cost per produced unit + duration: + type: string + nullable: true + description: Fixed production time for setup and overhead + duration_per: + type: string + nullable: true + title: Duration per unit + description: Production time per produced piece + search: + nullable: true + title: Search mode + description: |- + Method to select preferred alternate + + * `PRIORITY` - priority + * `MINCOST` - minimum cost + * `MINPENALTY` - minimum penalty + * `MINCOSTPENALTY` - minimum cost plus penalty + oneOf: + - $ref: '#/components/schemas/SearchEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedOperationDependency: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operation: + type: string + description: operation + blockedby: + type: string + title: Blocked by operation + description: blocked by operation + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Quantity relation between the operations + safety_leadtime: + type: string + nullable: true + title: Soft safety lead time + description: soft safety lead time + hard_safety_leadtime: + type: string + nullable: true + title: Hard safety lead time + description: hard safety lead time + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedOperationMaterial: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operation: + type: string + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Quantity to consume or produce per piece + quantity_fixed: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Fixed quantity + description: Fixed quantity to consume or produce + transferbatch: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Transfer batch quantity + description: Batch size by in which material is produced or consumed + offset: + type: string + nullable: true + description: Time offset from the start or end to consume or produce material + type: + nullable: true + description: |- + Consume/produce material at the start or the end of the operationplan + + * `start` - Start + * `end` - End + * `transfer_batch` - Batch transfer + oneOf: + - $ref: '#/components/schemas/OperationMaterialTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + name: + type: string + nullable: true + description: Name of this operation material to identify alternates + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority of this operation material in a group of alternates + search: + nullable: true + title: Search mode + description: |- + Method to select preferred alternate + + * `PRIORITY` - priority + * `MINCOST` - minimum cost + * `MINPENALTY` - minimum penalty + * `MINCOSTPENALTY` - minimum cost plus penalty + oneOf: + - $ref: '#/components/schemas/SearchEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedOperationPlanMaterial: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operationplan: + type: string + description: Unique identifier + title: Reference + item: + type: string + description: Unique identifier + location: + type: string + description: Unique identifier + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + onhand: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + flowdate: + type: string + format: date-time + title: Date + status: + nullable: true + title: Material status + description: |- + status of the material production or consumption + + * `proposed` - proposed + * `confirmed` - confirmed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/Status9c1Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedOperationPlanResource: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operationplan: + type: string + description: Unique identifier + title: Reference + resource: + type: string + description: Unique identifier + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + startdate: + type: string + readOnly: true + enddate: + type: string + readOnly: true + status: + nullable: true + title: Load status + description: |- + Status of the resource assignment + + * `proposed` - proposed + * `confirmed` - confirmed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/Status9c1Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + setup: + type: string + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedOperationResource: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operation: + type: string + resource: + type: string + description: Unique identifier + skill: + type: string + description: Required skill to perform the operation + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Required quantity of the resource + quantity_fixed: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Constant part of the capacity consumption (bucketized resources + only) + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + name: + type: string + nullable: true + description: Name of this operation resource to identify alternates + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority of this operation resource in a group of alternates + setup: + type: string + nullable: true + description: Setup required on the resource for this operation + search: + nullable: true + title: Search mode + description: |- + Method to select preferred alternate + + * `PRIORITY` - priority + * `MINCOST` - minimum cost + * `MINPENALTY` - minimum penalty + * `MINCOSTPENALTY` - minimum cost plus penalty + oneOf: + - $ref: '#/components/schemas/SearchEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedParameter: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + value: + type: string + nullable: true + description: + type: string + nullable: true + PatchedPurchaseOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + item: + type: string + description: Unique identifier + nullable: true + supplier: + type: string + description: Unique identifier + nullable: true + location: + type: string + description: Unique identifier + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + batch: + type: string + nullable: true + description: MTO batch name + criticality: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + forecast: + type: string + readOnly: true + nullable: true + PatchedResource: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/ResourceTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + maximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Size of the resource + maximum_calendar: + type: string + nullable: true + description: Calendar defining the resource size varying over time + available: + type: string + nullable: true + description: Calendar defining the working hours and holidays + location: + type: string + description: Unique identifier + nullable: true + owner: + type: string + description: Hierarchical parent + nullable: true + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Cost for using 1 unit of the resource for 1 hour + maxearly: + type: string + nullable: true + title: Max early + description: Time window before the ask date where we look for available + capacity + setupmatrix: + type: string + nullable: true + title: Setup matrix + description: Setup matrix defining the conversion time and cost + setup: + type: string + nullable: true + description: Setup of the resource at the start of the plan + efficiency: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Efficiency % + description: Efficiency percentage. Operations will take longer on resources + with efficiency less than 100%. + efficiency_calendar: + type: string + nullable: true + title: Efficiency % calendar + description: Calendar defining the efficiency percentage varying over time + constrained: + type: boolean + nullable: true + description: controls whether or not this resource is planned in finite + capacity mode + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + overloadcount: + type: integer + readOnly: true + nullable: true + title: Count of capacity overload problems + PatchedResourceSkill: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + resource: + type: string + description: Unique identifier + skill: + type: string + description: Unique identifier + effective_start: + type: string + format: date-time + nullable: true + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority of this skill in a group of alternates + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedSetupMatrix: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedSetupRule: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + setupmatrix: + type: string + title: Setup matrix + fromsetup: + type: string + nullable: true + title: From setup + description: Name of the old setup (wildcard characters are supported) + tosetup: + type: string + nullable: true + title: To setup + description: Name of the new setup (wildcard characters are supported) + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + duration: + type: string + nullable: true + description: Duration of the changeover + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Cost of the conversion + resource: + type: string + description: Extra resource used during this changeover + nullable: true + PatchedSkill: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedSubOperation: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operation: + type: string + description: Parent operation + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Sequence of this operation among the suboperations. Negative + values are ignored. + suboperation: + type: string + description: Child operation + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedSupplier: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + owner: + type: string + description: Hierarchical parent + nullable: true + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + available: + type: string + nullable: true + description: Calendar defining the working hours and holidays + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + PatchedWorkOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + operation: + type: string + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + quantity_completed: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Completed quantity + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + batch: + type: string + nullable: true + description: MTO batch name + criticality: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + owner: + type: string + description: Hierarchical parent + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + resources: + type: array + items: + $ref: '#/components/schemas/OperationPlanResourceNested' + materials: + type: array + items: + $ref: '#/components/schemas/OperationPlanMaterialNested' + demand: + type: string + description: Unique identifier + nullable: true + forecast: + type: string + readOnly: true + nullable: true + PolicyEnum: + enum: + - independent + - alltogether + - inratio + type: string + description: |- + * `independent` - independent + * `alltogether` - all together + * `inratio` - in ratio + PurchaseOrder: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + reference: + type: string + description: Unique identifier + status: + nullable: true + description: |- + Status of the order + + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + oneOf: + - $ref: '#/components/schemas/StatusCa7Enum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + item: + type: string + description: Unique identifier + nullable: true + supplier: + type: string + description: Unique identifier + nullable: true + location: + type: string + description: Unique identifier + nullable: true + quantity: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + startdate: + type: string + format: date-time + nullable: true + title: Start date + description: start date + enddate: + type: string + format: date-time + nullable: true + title: End date + description: end date + batch: + type: string + nullable: true + description: MTO batch name + criticality: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + readOnly: true + nullable: true + delay: + type: string + readOnly: true + nullable: true + plan: + readOnly: true + nullable: true + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + forecast: + type: string + readOnly: true + nullable: true + required: + - criticality + - delay + - forecast + - lastmodified + - plan + Resource: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + type: + nullable: true + oneOf: + - $ref: '#/components/schemas/ResourceTypeEnum' + - $ref: '#/components/schemas/BlankEnum' + - $ref: '#/components/schemas/NullEnum' + maximum: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Size of the resource + maximum_calendar: + type: string + nullable: true + description: Calendar defining the resource size varying over time + available: + type: string + nullable: true + description: Calendar defining the working hours and holidays + location: + type: string + description: Unique identifier + nullable: true + owner: + type: string + description: Hierarchical parent + nullable: true + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Cost for using 1 unit of the resource for 1 hour + maxearly: + type: string + nullable: true + title: Max early + description: Time window before the ask date where we look for available + capacity + setupmatrix: + type: string + nullable: true + title: Setup matrix + description: Setup matrix defining the conversion time and cost + setup: + type: string + nullable: true + description: Setup of the resource at the start of the plan + efficiency: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + title: Efficiency % + description: Efficiency percentage. Operations will take longer on resources + with efficiency less than 100%. + efficiency_calendar: + type: string + nullable: true + title: Efficiency % calendar + description: Calendar defining the efficiency percentage varying over time + constrained: + type: boolean + nullable: true + description: controls whether or not this resource is planned in finite + capacity mode + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + overloadcount: + type: integer + readOnly: true + nullable: true + title: Count of capacity overload problems + required: + - lastmodified + - overloadcount + ResourceSkill: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + resource: + type: string + description: Unique identifier + skill: + type: string + description: Unique identifier + effective_start: + type: string + format: date-time + nullable: true + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + nullable: true + description: Priority of this skill in a group of alternates + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + ResourceTypeEnum: + enum: + - default + - buckets + - buckets_day + - buckets_week + - buckets_month + - infinite + type: string + description: |- + * `default` - default + * `buckets` - buckets + * `buckets_day` - buckets_day + * `buckets_week` - buckets_week + * `buckets_month` - buckets_month + * `infinite` - infinite + SearchEnum: + enum: + - PRIORITY + - MINCOST + - MINPENALTY + - MINCOSTPENALTY + type: string + description: |- + * `PRIORITY` - priority + * `MINCOST` - minimum cost + * `MINPENALTY` - minimum penalty + * `MINCOSTPENALTY` - minimum cost plus penalty + SetupMatrix: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - lastmodified + SetupRule: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + setupmatrix: + type: string + title: Setup matrix + fromsetup: + type: string + nullable: true + title: From setup + description: Name of the old setup (wildcard characters are supported) + tosetup: + type: string + nullable: true + title: To setup + description: Name of the new setup (wildcard characters are supported) + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + duration: + type: string + nullable: true + description: Duration of the changeover + cost: + type: string + format: decimal + pattern: ^-?\d{0,12}(?:\.\d{0,8})?$ + nullable: true + description: Cost of the conversion + resource: + type: string + description: Extra resource used during this changeover + nullable: true + required: + - id + Skill: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - lastmodified + Status9c1Enum: + enum: + - proposed + - confirmed + - closed + type: string + description: |- + * `proposed` - proposed + * `confirmed` - confirmed + * `closed` - closed + StatusCa7Enum: + enum: + - proposed + - approved + - confirmed + - completed + - closed + type: string + description: |- + * `proposed` - proposed + * `approved` - approved + * `confirmed` - confirmed + * `completed` - completed + * `closed` - closed + SubOperation: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + id: + type: integer + readOnly: true + title: Identifier + operation: + type: string + description: Parent operation + priority: + type: integer + maximum: 2147483647 + minimum: -2147483648 + description: Sequence of this operation among the suboperations. Negative + values are ignored. + suboperation: + type: string + description: Child operation + effective_start: + type: string + format: date-time + nullable: true + default: '1971-01-01T00:00:00' + description: Validity start date + effective_end: + type: string + format: date-time + nullable: true + description: Validity end date + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - id + - lastmodified + Supplier: + type: object + description: |- + The django model serializer extends the default implementation with the + following capabilities: + - Ability to work with natural keys. + - Support for create-or-update on the POST method. + The default POST method only supports create. + The PUT method remains update-only. + - Enable partial updates by default + properties: + name: + type: string + description: Unique identifier + owner: + type: string + description: Hierarchical parent + nullable: true + description: + type: string + nullable: true + category: + type: string + nullable: true + subcategory: + type: string + nullable: true + available: + type: string + nullable: true + description: Calendar defining the working hours and holidays + source: + type: string + nullable: true + lastmodified: + type: string + format: date-time + readOnly: true + title: Last modified + required: + - lastmodified + securitySchemes: + basicAuth: + type: http + scheme: basic + cookieAuth: + type: apiKey + in: cookie + name: sessionid diff --git a/include/frepple/model.h b/include/frepple/model.h index 9418f37c32..bc791e4da4 100644 --- a/include/frepple/model.h +++ b/include/frepple/model.h @@ -2742,9 +2742,15 @@ class OperationPlan final : public Object, * Subclasses of the Operation class may use this constructor in their * own override of the createOperationPlan method. */ - OperationPlan() { initType(metadata); } + OperationPlan() { + sequence = sequenceCounter.fetch_add(1, memory_order_relaxed); + initType(metadata); + } - OperationPlan(Operation* o) : oper(o) { initType(metadata); } + OperationPlan(Operation* o) : oper(o) { + sequence = sequenceCounter.fetch_add(1, memory_order_relaxed); + initType(metadata); + } static const unsigned short STATUS_APPROVED = 1; static const unsigned short STATUS_CONFIRMED = 2; @@ -2772,6 +2778,15 @@ class OperationPlan final : public Object, static unsigned long counterMin; static string referenceMax; + /* Monotonic counter handing each operationplan a unique creation-sequence + * number (the `sequence` data member below). Used only as the final, + * deterministic tie-breaker in operator< - replacing a pointer comparison that + * was not reproducible across platforms/runs. Atomic so concurrent per-cluster + * solver threads don't race; relaxed because only uniqueness is needed, not + * cross-thread ordering. The sequence is deterministic for a single-threaded + * solve (the reproducible case). 64-bit so a long-running daemon never wraps. */ + static atomic sequenceCounter; + /* Flag controlling where setup time verification should be performed. */ static bool propagatesetups; @@ -2844,6 +2859,10 @@ class OperationPlan final : public Object, */ PooledString info; + /* Unique creation-sequence number, assigned from sequenceCounter in the + * constructor. The deterministic final tie-breaker in operator<. */ + unsigned long sequence = 0; + /* Hidden, static field to store the location during import. */ static Location* loc; @@ -8664,7 +8683,9 @@ class Problem::iterator { /* Equality operator. */ bool operator==(const iterator& t) const { return iter == t.iter; } - Problem& operator*() const { return *iter; } + // Forms a reference to null at end(); never dereferenced there (callers test + // against end() first). See FREPPLE_NO_SANITIZE_NULL in utils.h. + FREPPLE_NO_SANITIZE_NULL Problem& operator*() const { return *iter; } Problem* operator->() const { return iter; } }; diff --git a/include/frepple/timeline.h b/include/frepple/timeline.h index 7a60e6cd33..c84944383b 100644 --- a/include/frepple/timeline.h +++ b/include/frepple/timeline.h @@ -290,7 +290,9 @@ class TimeLine { const_iterator(const iterator& c) : cur(c.cur) {} - const Event& operator*() const { return *cur; } + // Forms a reference to null at end()/default-ctor; never dereferenced there + // (operator++ guards). See FREPPLE_NO_SANITIZE_NULL in utils.h. + FREPPLE_NO_SANITIZE_NULL const Event& operator*() const { return *cur; } const Event* operator->() const { return cur; } diff --git a/include/frepple/utils.h b/include/frepple/utils.h index eccc7b37d3..c812eea835 100644 --- a/include/frepple/utils.h +++ b/include/frepple/utils.h @@ -38,6 +38,7 @@ inline bool unused_function() { return PyDateTimeAPI == nullptr; } #include #include +#include #include #include #include @@ -61,6 +62,18 @@ using namespace std; #include +// A few container iterators intentionally form a reference to a null sentinel at +// end() (e.g. the Timeline and Problem-list operator*). The value is never +// dereferenced - operator++ and every caller guard against end() first - so this +// is the same benign UB the standard library has for *v.end(). Mark just those +// spots no_sanitize("null") so the engine-ubsan gate can be blocking without +// flagging the idiom. A no-op in non-sanitized builds; supported by GCC + Clang. +#if defined(__GNUC__) || defined(__clang__) +#define FREPPLE_NO_SANITIZE_NULL __attribute__((no_sanitize("null"))) +#else +#define FREPPLE_NO_SANITIZE_NULL +#endif + constexpr double ROUNDING_ERROR = 0.000001; namespace frepple { @@ -2647,6 +2660,25 @@ class PythonData : public DataValue { Py_INCREF(obj); } + /* Factory that ADOPTS an already-owned ("new") reference. + * Unlike the PythonData(const PyObject*) constructor, this does NOT + * increment the refcount: the caller's owned reference is taken over and + * released by the resulting PythonData's destructor. Use this to wrap the + * result of a Python C-API call that returns a new reference (e.g. + * PyObject_CallFunction/CallMethod) without leaking it. A nullptr is + * treated as Py_None. The caller must hold the GIL. */ + static inline PythonData fromOwned(PyObject* o) { + PythonData d; + d.setNull(); + if (o) + d.obj = o; // adopt the owned reference as-is (no INCREF) + else { + d.obj = Py_None; + Py_INCREF(Py_None); + } + return d; + } + /* Set the internal pointer to nullptr. */ inline void setNull() { if (obj) Py_DECREF(obj); diff --git a/requirements.txt b/requirements.txt index c848da87c0..eb6f187788 100644 --- a/requirements.txt +++ b/requirements.txt @@ -2,6 +2,7 @@ portend == 2.5 djangorestframework == 3.15.2 djangorestframework-bulk == 0.2.1 djangorestframework-filters == 0.10.2 +drf-spectacular == 0.27.2 django-bootstrap3 == 15.0.0 django-filter == 2.4.0 html5lib == 1.1 @@ -19,6 +20,7 @@ setuptools setuptools-rust == 0.12.1 paramiko == 4.0.0 channels == 4.0.0 +channels-redis == 4.2.0 attrs == 23.1.0 daphne == 4.0.0 Twisted[tls,http2] diff --git a/rust/frepple-forecast/.gitignore b/rust/frepple-forecast/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/rust/frepple-forecast/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rust/frepple-forecast/Cargo.lock b/rust/frepple-forecast/Cargo.lock new file mode 100644 index 0000000000..1758f8c3d8 --- /dev/null +++ b/rust/frepple-forecast/Cargo.lock @@ -0,0 +1,180 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "frepple-forecast" +version = "0.1.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/rust/frepple-forecast/Cargo.toml b/rust/frepple-forecast/Cargo.toml new file mode 100644 index 0000000000..03d745d589 --- /dev/null +++ b/rust/frepple-forecast/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "frepple-forecast" +version = "0.1.0" +edition = "2021" +description = "frePPLe Rust/PyO3 pilot (Engine track E4, slice 2): MovingAverage forecast method" +license = "MIT" + +[lib] +name = "frepple_forecast" +# cdylib for the PyO3 wheel (parity tests); rlib for `cargo test`; staticlib for +# the C-ABI link into libfrepple (Phase 7 engine integration). +crate-type = ["cdylib", "rlib", "staticlib"] + +# pyo3 optional + feature-gated so `cargo test` runs the pure-logic tests with no +# Python dependency; `maturin build --features extension-module` builds the wheel. +[dependencies] +pyo3 = { version = "0.22", optional = true } + +[features] +extension-module = ["dep:pyo3", "pyo3/extension-module"] diff --git a/rust/frepple-forecast/pyproject.toml b/rust/frepple-forecast/pyproject.toml new file mode 100644 index 0000000000..57db0b6ee6 --- /dev/null +++ b/rust/frepple-forecast/pyproject.toml @@ -0,0 +1,12 @@ +[build-system] +requires = ["maturin>=1.5,<2.0"] +build-backend = "maturin" + +[project] +name = "frepple-forecast" +version = "0.1.0" +requires-python = ">=3.12" +description = "frePPLe Rust/PyO3 pilot (Engine track E4, slice 2): MovingAverage forecast method" + +[tool.maturin] +features = ["extension-module"] diff --git a/rust/frepple-forecast/src/capi.rs b/rust/frepple-forecast/src/capi.rs new file mode 100644 index 0000000000..689f8df47d --- /dev/null +++ b/rust/frepple-forecast/src/capi.rs @@ -0,0 +1,157 @@ +//! C ABI for linking the Rust forecast methods into `libfrepple` (Engine track +//! E4, phase 7 integration). The numeric ports live in safe modules +//! (`#![forbid(unsafe_code)]`); THIS module is the only place with `unsafe` — the +//! FFI boundary (raw pointer in/out), exactly the small audited surface every +//! Python/C extension needs, vs. a C++ engine that is unsafe throughout. +//! +//! Contract: scalars are returned through out-pointers; variable-length outputs +//! (outlier indices, seasonal factors) are written into a caller-provided buffer +//! up to `*_cap`, with the true length returned via `*_len` (so the caller can +//! detect truncation). All functions return 0 on success. +//! +//! The matching header is `tools/rust-pilot/frepple_forecast.h`. + +use crate::common::Forecast; + +/// SAFETY: `history` must point to `count` readable f64s; the out-pointers must +/// be non-null and writable; `out_outliers` must be writable for `out_cap` +/// usizes. +unsafe fn write_scalar_result( + r: &Forecast, + out_smape: *mut f64, + out_stddev: *mut f64, + out_forecast: *mut f64, + out_outliers: *mut usize, + out_cap: usize, + out_len: *mut usize, +) { + *out_smape = r.smape; + *out_stddev = r.standarddeviation; + *out_forecast = r.forecast; + *out_len = r.outliers.len(); + for (k, &o) in r.outliers.iter().take(out_cap).enumerate() { + *out_outliers.add(k) = o; + } +} + +macro_rules! scalar_method { + ($name:ident, $body:expr) => { + /// # Safety + /// See the module contract: valid `history`/`count` and writable out-params. + #[no_mangle] + pub unsafe extern "C" fn $name( + history: *const f64, + count: usize, + out_smape: *mut f64, + out_stddev: *mut f64, + out_forecast: *mut f64, + out_outliers: *mut usize, + out_cap: usize, + out_len: *mut usize, + // method params follow via the closure capture below + p: *const f64, + np: usize, + ) -> i32 { + let h = std::slice::from_raw_parts(history, count); + let params = std::slice::from_raw_parts(p, np); + let r: Forecast = $body(h, params); + write_scalar_result(&r, out_smape, out_stddev, out_forecast, out_outliers, out_cap, out_len); + 0 + } + }; +} + +// Params are passed as a small f64 array (`p`) to keep one stable signature per +// method family; the order matches the header docs. +scalar_method!(frepple_moving_average, |h: &[f64], p: &[f64]| { + crate::forecast::moving_average(h, p[0] as u32, p[1], p[2], p[3] as u64) +}); +scalar_method!(frepple_single_exponential, |h: &[f64], p: &[f64]| { + crate::single_exp::single_exponential(h, p[0], p[1], p[2], p[3], p[4], p[5] as u64, p[6] as u64) +}); +scalar_method!(frepple_croston, |h: &[f64], p: &[f64]| { + crate::croston::croston(h, p[0], p[1], p[2], p[3], p[4], p[5] as u64, p[6] as u64) +}); + +/// DoubleExp can't use the scalar macro: `applyForecast` extrapolates per bucket, +/// so it also returns the level/trend components via two extra out-pointers. +/// # Safety +/// As the scalar contract (valid `history`/`count`, writable scalar/outlier +/// out-params), plus `out_constant`/`out_trend` must be non-null + writable. +#[no_mangle] +#[allow(clippy::too_many_arguments)] +pub unsafe extern "C" fn frepple_double_exponential( + history: *const f64, + count: usize, + out_smape: *mut f64, + out_stddev: *mut f64, + out_forecast: *mut f64, + out_outliers: *mut usize, + out_cap: usize, + out_len: *mut usize, + p: *const f64, + np: usize, + out_constant: *mut f64, + out_trend: *mut f64, +) -> i32 { + let h = std::slice::from_raw_parts(history, count); + let params = std::slice::from_raw_parts(p, np); + let r = crate::double_exp::double_exponential_state( + h, params[0], params[1], params[2], params[3], params[4], params[5], + params[6], params[7], params[8] as u64, params[9] as u64, + ); + write_scalar_result( + &r.base, out_smape, out_stddev, out_forecast, out_outliers, out_cap, out_len, + ); + *out_constant = r.constant; + *out_trend = r.trend; + 0 +} + +/// Seasonal has extra outputs (period, force, seasonal factors) and the +/// level/trend/cycle apply-state (l_i, t_i, cycleindex) the engine extrapolates +/// from. It has NO outlier detection (unlike MA/SingleExp/Croston), so there are +/// no outlier indices to return — the engine creates no ProblemOutlier for it. +/// # Safety +/// Valid `history`/`count`; writable scalar out-params; `out_s_i` writable for +/// `s_i_cap` f64s; `out_l_i`/`out_t_i`/`out_cycleindex` non-null + writable. +#[no_mangle] +#[allow(clippy::too_many_arguments)] +pub unsafe extern "C" fn frepple_seasonal( + history: *const f64, + count: usize, + p: *const f64, + np: usize, + out_smape: *mut f64, + out_stddev: *mut f64, + out_forecast: *mut f64, + out_period: *mut u32, + out_force: *mut i32, + out_s_i: *mut f64, + s_i_cap: usize, + out_s_i_len: *mut usize, + // apply-state for the engine's per-bucket extrapolation (phase 7). + out_l_i: *mut f64, + out_t_i: *mut f64, + out_cycleindex: *mut u32, +) -> i32 { + let h = std::slice::from_raw_parts(history, count); + let pr = std::slice::from_raw_parts(p, np); + let r = crate::seasonal::seasonal( + h, pr[0], pr[1], pr[2], pr[3], pr[4], pr[5], pr[6], pr[7] as usize, pr[8] as usize, + pr[9], pr[10], pr[11], pr[12] as u64, pr[13] as u64, + ); + *out_smape = r.smape; + *out_stddev = r.standarddeviation; + *out_forecast = r.forecast; + *out_period = r.period; + *out_force = r.force as i32; + *out_s_i_len = r.s_i.len(); + for (k, &s) in r.s_i.iter().take(s_i_cap).enumerate() { + *out_s_i.add(k) = s; + } + *out_l_i = r.l_i; + *out_t_i = r.t_i; + *out_cycleindex = r.cycleindex; + 0 +} diff --git a/rust/frepple-forecast/src/common.rs b/rust/frepple-forecast/src/common.rs new file mode 100644 index 0000000000..82368168e1 --- /dev/null +++ b/rust/frepple-forecast/src/common.rs @@ -0,0 +1,69 @@ +//! Shared numeric helpers for the forecast-method ports (Engine track E4). +//! Memory-safe by construction; mirrors the constants + `smapeWeight` from +//! `src/forecast/forecast.h` / `src/forecast/timeseries.cpp`. +#![forbid(unsafe_code)] + +pub const MAXBUCKETS: usize = 500; +pub const ROUNDING_ERROR: f64 = 0.000001; // include/frepple/utils.h:64 +pub const ACCURACY: f64 = 0.01; // timeseries.cpp:30 + +/// Result of a forecast-method evaluation — mirrors `ForecastSolver::Metrics` +/// plus the constant forecast value and the outlier indices the C++ would have +/// written as ProblemOutlier objects (relative to the series). +#[derive(Debug, Clone, PartialEq)] +pub struct Forecast { + pub smape: f64, + pub standarddeviation: f64, + pub forecast: f64, + pub outliers: Vec, +} + +/// The exponentially-decaying smape weight table (forecast.h:2627-2629): +/// weight[0] = 1, weight[i+1] = weight[i] * alfa. +pub fn weight_table(smape_alfa: f64) -> [f64; MAXBUCKETS] { + let mut w = [0.0f64; MAXBUCKETS]; + w[0] = 1.0; + for i in 0..MAXBUCKETS - 1 { + w[i + 1] = w[i] * smape_alfa; + } + w +} + +/// Bounds-safe weight accessor (forecast.h:3051-3054) — the `weight[]` OOB-read +/// site. The C++ needed a hand-written clamp; in Rust the indexing is +/// bounds-checked regardless and the clamp is one line. +pub fn smape_weight(weight: &[f64; MAXBUCKETS], idx: i64) -> f64 { + let i = idx.clamp(0, (MAXBUCKETS - 1) as i64) as usize; + weight[i] +} + +/// One 2D Levenberg-Marquardt step for the two-parameter methods (DoubleExp, +/// Seasonal): solve the 2x2 system [sum11 sum12; sum12 sum22] * delta = [sum13; +/// sum23] via Cramer's rule, with the `damping` added to the diagonal. Mirrors +/// timeseries.cpp:824-844: if the damped matrix is near-singular, retry undamped; +/// if still singular, return None (the caller stops iterating). +pub fn solve_2x2_marquardt( + sum11: f64, + sum12: f64, + sum22: f64, + sum13: f64, + sum23: f64, + damping: f64, +) -> Option<(f64, f64)> { + // Match the C++ bit-for-bit: it adds the damping then SUBTRACTS it on the + // singular retry ((x+d)-d), which is not always exactly x in f64. + let mut a11 = sum11 + damping; + let mut a22 = sum22 + damping; + let mut det = a11 * a22 - sum12 * sum12; + if det.abs() < ROUNDING_ERROR { + a11 -= damping; // try without the damping factor + a22 -= damping; + det = a11 * a22 - sum12 * sum12; + if det.abs() < ROUNDING_ERROR { + return None; // still singular + } + } + let delta1 = (sum13 * a22 - sum23 * sum12) / det; + let delta2 = (sum23 * a11 - sum13 * sum12) / det; + Some((delta1, delta2)) +} diff --git a/rust/frepple-forecast/src/croston.rs b/rust/frepple-forecast/src/croston.rs new file mode 100644 index 0000000000..7c2a3f8e5f --- /dev/null +++ b/rust/frepple-forecast/src/croston.rs @@ -0,0 +1,199 @@ +//! Memory-safe Rust port of the Croston intermittent-demand forecast method +//! (Engine track E4, phase 5). Faithful translation of +//! `ForecastSolver::Croston::generateForecast` (src/forecast/timeseries.cpp:1307-1463): +//! an `alfa` grid-search (not Marquardt) over the demand-magnitude `q_i` / +//! inter-demand-period `p_i` smoothing, with upper-only outlier clamping. Same +//! f64 operation order for tight parity; outlier writes -> returned indices. +//! +//! Quirk preserved verbatim: `between_demands` is NOT reset between grid +//! iterations/passes in the C++ (declared outside the loop), so it persists here +//! too. +#![forbid(unsafe_code)] + +use crate::common::{smape_weight, weight_table, Forecast, ROUNDING_ERROR}; + +#[allow(clippy::too_many_arguments)] +pub fn croston( + history: &[f64], + min_alfa: f64, + max_alfa: f64, + decay_rate: f64, + max_deviation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, +) -> Forecast { + let count = history.len(); + let mut timeseries = history.to_vec(); + timeseries.push(0.0); + let weight = weight_table(smape_alfa); + + let mut nonzero = 0.0f64; + let mut totalsum = 0.0f64; + let mut lastnonzero = 0usize; + for i in 0..count { + if timeseries[i] != 0.0 { + nonzero += 1.0; + totalsum += timeseries[i]; + lastnonzero = i; + } + } + if nonzero == 0.0 { + return Forecast { + smape: 0.0, + standarddeviation: 0.0, + forecast: 0.0, + outliers: Vec::new(), + }; + } + let periods_between_demands = count as f64 / nonzero; + + let mut alfa = min_alfa; + let mut f_i = 0.0f64; + let niter = iterations; + let delta = if niter > 1 { + (max_alfa - min_alfa) / (niter as f64 - 1.0) + } else { + 0.0 + }; + let mut between_demands: u32 = 1; // persists across iterations (verbatim) + let mut outliers: Vec = Vec::new(); + let mut best_error = f64::MAX; + let mut best_smape = 0.0f64; + let mut best_f_i = 0.0f64; + let mut best_standarddeviation = 0.0f64; + + let mut iteration: u64 = 0; + while iteration < niter { + let mut standarddeviation = 0.0f64; + let mut maxdeviation = 0.0f64; + let mut error_smape = 0.0f64; + let mut error_smape_weights = 0.0f64; + + for pass in 0..=1 { + error_smape = 0.0; + error_smape_weights = 0.0; + let mut q_i = totalsum / nonzero; + let mut p_i = count as f64 / nonzero; + f_i = (1.0 - alfa / 2.0) * q_i / p_i; + + let mut history_i = timeseries[0]; + let mut i = 1usize; + while i <= count { + let history_i_min_1 = history_i; + history_i = timeseries[i]; + if history_i_min_1 != 0.0 { + q_i = alfa * history_i_min_1 + (1.0 - alfa) * q_i; + p_i = alfa * between_demands as f64 + (1.0 - alfa) * p_i; + f_i = (1.0 - alfa / 2.0) * q_i / p_i; + between_demands = 1; + } else if i > lastnonzero + && between_demands as f64 > 2.0 * periods_between_demands + { + f_i *= 1.0 - decay_rate; + p_i = (1.0 - alfa / 2.0) * q_i / f_i; + } else { + between_demands += 1; + } + if i == count { + break; + } + if pass == 0 { + standarddeviation += (f_i - history_i) * (f_i - history_i); + if (history_i - f_i).abs() > maxdeviation { + maxdeviation = (f_i - history_i).abs(); + } + } else if history_i > f_i + max_deviation * standarddeviation { + // upper-only clamp (no lower limit for Croston) + history_i = f_i + max_deviation * standarddeviation; + if iteration == 1 { + outliers.push(i); + } + } + if (i as u64) >= skip && p_i > 0.0 && (f_i + history_i).abs() > ROUNDING_ERROR { + let w = smape_weight(&weight, (count - i) as i64); + error_smape += (f_i - history_i).abs() / (f_i + history_i).abs() * w; + error_smape_weights += w; + } + i += 1; + } + + if pass == 0 { + standarddeviation = if count > 1 { + (standarddeviation / (count as f64 - 1.0)).sqrt() + } else { + 0.0 + }; + if standarddeviation > ROUNDING_ERROR { + maxdeviation /= standarddeviation; + } + if maxdeviation < max_deviation { + break; + } + } + } + + // Equal smape is "better" for Croston (prefers higher alfa). + if error_smape <= best_error { + best_error = error_smape; + best_smape = if error_smape_weights != 0.0 { + error_smape / error_smape_weights + } else { + 0.0 + }; + best_f_i = f_i; + best_standarddeviation = standarddeviation; + } + + if delta != 0.0 { + alfa += delta; + } else { + break; + } + iteration += 1; + } + + Forecast { + smape: best_smape, + standarddeviation: best_standarddeviation, + forecast: best_f_i, + outliers, + } +} + +#[cfg(test)] +mod tests { + use super::*; + fn run(h: &[f64]) -> Forecast { + // engine defaults (timeseries.cpp:1301-1305) + croston(h, 0.03, 0.8, 0.1, 4.0, 0.95, 5, 15) + } + + #[test] + fn all_zero_history_is_zero() { + let r = run(&vec![0.0; 20]); + assert_eq!(r.smape, 0.0); + assert_eq!(r.forecast, 0.0); + } + + #[test] + fn intermittent_demand_is_finite_positive() { + let h = vec![ + 5.0, 0.0, 0.0, 8.0, 0.0, 0.0, 0.0, 6.0, 0.0, 3.0, 0.0, 0.0, 7.0, 0.0, 0.0, 4.0, + 0.0, 9.0, 0.0, 0.0, + ]; + let r = run(&h); + assert!(r.smape.is_finite(), "smape={}", r.smape); + assert!(r.forecast.is_finite() && r.forecast > 0.0, "forecast={}", r.forecast); + } + + #[test] + fn finite_on_long_oob_series() { + // intermittent, > MAXBUCKETS + let long: Vec = (0..800) + .map(|x| if x % 4 == 0 { 10.0 + (x % 7) as f64 } else { 0.0 }) + .collect(); + let r = run(&long); + assert!(r.smape.is_finite() && r.standarddeviation.is_finite()); + } +} diff --git a/rust/frepple-forecast/src/double_exp.rs b/rust/frepple-forecast/src/double_exp.rs new file mode 100644 index 0000000000..fbaaed0abc --- /dev/null +++ b/rust/frepple-forecast/src/double_exp.rs @@ -0,0 +1,309 @@ +//! Memory-safe Rust port of the DoubleExponential forecast method (Engine track +//! E4, phase 4). Faithful translation of +//! `ForecastSolver::DoubleExponential::generateForecast` +//! (src/forecast/timeseries.cpp:633-892): Holt-Winters level+trend smoothing with +//! a 2D Levenberg-Marquardt optimisation of (alfa, gamma) via a 2x2 Hessian +//! (shared `common::solve_2x2_marquardt`). Same f64 operation order for tight +//! parity; outlier ProblemOutlier writes -> returned indices. +#![forbid(unsafe_code)] + +use crate::common::{smape_weight, solve_2x2_marquardt, weight_table, Forecast, ACCURACY, ROUNDING_ERROR}; + +/// DoubleExp result carrying the level+trend state `applyForecast` needs (phase +/// 7): the engine extrapolates `constant + k*trend*damp` per bucket, so it needs +/// the two components, not just their one-step sum (`base.forecast`). +pub struct DoubleExpState { + pub base: Forecast, + pub constant: f64, + pub trend: f64, +} + +#[allow(clippy::too_many_arguments)] +pub fn double_exponential_state( + history: &[f64], + initial_alfa: f64, + min_alfa: f64, + max_alfa: f64, + initial_gamma: f64, + min_gamma: f64, + max_gamma: f64, + max_deviation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, +) -> DoubleExpState { + let count = history.len(); + if (count as u64) < skip + 5 { + return DoubleExpState { + base: Forecast { + smape: f64::MAX, + standarddeviation: f64::MAX, + forecast: 0.0, + outliers: Vec::new(), + }, + constant: 0.0, + trend: 0.0, + }; + } + + let mut timeseries = history.to_vec(); + timeseries.push(0.0); + let weight = weight_table(smape_alfa); + + // No constructor clamp (forecast.h:2046): alfa/gamma start at the inits. + let mut alfa = initial_alfa; + let mut gamma = initial_gamma; + let mut constant_i = 0.0f64; + let mut trend_i = 0.0f64; + let mut outliers: Vec = Vec::new(); + + let mut best_error = f64::MAX; + let mut best_smape = 0.0f64; + let mut best_constant_i = 0.0f64; + let mut best_trend_i = 0.0f64; + let mut best_standarddeviation = 0.0f64; + let mut boundarytested = 0u32; + + let mut iteration: u64 = 1; + while iteration <= iterations { + let mut standarddeviation = 0.0f64; + let mut maxdeviation = 0.0f64; + // read after the outlier loop for the Marquardt step + let mut error = 0.0f64; + let mut error_smape = 0.0f64; + let mut error_smape_weights = 0.0f64; + let mut sum11 = 0.0f64; + let mut sum12 = 0.0f64; + let mut sum22 = 0.0f64; + let mut sum13 = 0.0f64; + let mut sum23 = 0.0f64; + + for pass in 0..=1 { + error = 0.0; + error_smape = 0.0; + error_smape_weights = 0.0; + sum11 = 0.0; + sum12 = 0.0; + sum22 = 0.0; + sum13 = 0.0; + sum23 = 0.0; + let mut d_constant_d_alfa = 0.0f64; + let mut d_constant_d_gamma = 0.0f64; + let mut d_trend_d_alfa = 0.0f64; + let mut d_trend_d_gamma = 0.0f64; + let mut d_forecast_d_alfa = 0.0f64; + let mut d_forecast_d_gamma = 0.0f64; + + let history_0 = timeseries[0]; + let history_1 = timeseries[1]; + let history_2 = timeseries[2]; + let history_3 = timeseries[3]; + constant_i = (history_0 + history_1 + history_2) / 3.0; + trend_i = (history_3 - history_0) / 3.0; + if pass == 1 { + let md = max_deviation * standarddeviation; + let t1a = if history_0 > constant_i + md { + constant_i + md + } else if history_0 < constant_i - md { + constant_i - md + } else { + history_0 + }; + let mut t1 = t1a; + let mut t2 = -t1a; + if history_1 > constant_i + trend_i + md { + t1 += constant_i + trend_i + md; + } else if history_1 < constant_i + trend_i - md { + t1 += constant_i + trend_i - md; + } else { + t1 += history_1; + } + if history_2 > constant_i + 2.0 * trend_i + md { + t1 += constant_i + 2.0 * trend_i + md; + t2 += constant_i + 2.0 * trend_i + md; + } else if history_2 < constant_i + 2.0 * trend_i - md { + t1 += constant_i + 2.0 * trend_i - md; + t2 += constant_i + 2.0 * trend_i - md; + } else { + t1 += history_2; + t2 += history_2; + } + constant_i = t1 / 3.0; + trend_i = t2 / 3.0; + } + + let mut history_i = history_0; + let mut i = 1usize; + while i <= count { + let history_i_min_1 = history_i; + history_i = timeseries[i]; + let constant_i_prev = constant_i; + let trend_i_prev = trend_i; + constant_i = history_i_min_1 * alfa + (1.0 - alfa) * (constant_i_prev + trend_i_prev); + trend_i = gamma * (constant_i - constant_i_prev) + (1.0 - gamma) * trend_i_prev; + if i == count { + break; + } + if pass == 0 { + let e = constant_i + trend_i - history_i; + standarddeviation += e * e; + if e.abs() > maxdeviation { + maxdeviation = e.abs(); + } + } else { + let md = max_deviation * standarddeviation; + if history_i > constant_i + trend_i + md { + history_i = constant_i + trend_i + md; + if iteration == 1 { + outliers.push(i); + } + } else if history_i < constant_i + trend_i - md { + history_i = constant_i + trend_i - md; + if iteration == 1 { + outliers.push(i); + } + } + } + let d_constant_d_gamma_prev = d_constant_d_gamma; + let d_constant_d_alfa_prev = d_constant_d_alfa; + d_constant_d_alfa = + history_i_min_1 - constant_i_prev - trend_i_prev + (1.0 - alfa) * d_forecast_d_alfa; + d_constant_d_gamma = (1.0 - alfa) * d_forecast_d_gamma; + d_trend_d_alfa = + gamma * (d_constant_d_alfa - d_constant_d_alfa_prev) + (1.0 - gamma) * d_trend_d_alfa; + d_trend_d_gamma = constant_i - constant_i_prev - trend_i_prev + + gamma * (d_constant_d_gamma - d_constant_d_gamma_prev) + + (1.0 - gamma) * d_trend_d_gamma; + d_forecast_d_alfa = d_constant_d_alfa + d_trend_d_alfa; + d_forecast_d_gamma = d_constant_d_gamma + d_trend_d_gamma; + let w = smape_weight(&weight, (count - i) as i64); + sum11 += w * d_forecast_d_alfa * d_forecast_d_alfa; + sum12 += w * d_forecast_d_alfa * d_forecast_d_gamma; + sum22 += w * d_forecast_d_gamma * d_forecast_d_gamma; + sum13 += w * d_forecast_d_alfa * (history_i - constant_i - trend_i); + sum23 += w * d_forecast_d_gamma * (history_i - constant_i - trend_i); + if (i as u64) >= skip { + error += (constant_i + trend_i - history_i) * (constant_i + trend_i - history_i) * w; + if (constant_i + trend_i + history_i).abs() > ROUNDING_ERROR { + error_smape += (constant_i + trend_i - history_i).abs() + / (constant_i + trend_i + history_i).abs() + * w; + error_smape_weights += w; + } + } + i += 1; + } + + if pass == 0 { + standarddeviation = (standarddeviation / (count as f64 - 1.0)).sqrt(); + maxdeviation /= standarddeviation; + if maxdeviation < max_deviation { + break; + } + } + } + + if error < best_error { + best_error = error; + best_smape = if error_smape_weights != 0.0 { + error_smape / error_smape_weights + } else { + 0.0 + }; + best_constant_i = constant_i; + best_trend_i = trend_i; + best_standarddeviation = standarddeviation; + } + + let delta = solve_2x2_marquardt(sum11, sum12, sum22, sum13, sum23, error / iteration as f64); + let (delta_alfa, delta_gamma) = match delta { + Some(d) => d, + None => break, // singular + }; + if delta_alfa.abs() + delta_gamma.abs() < 2.0 * ACCURACY && iteration > 3 { + break; + } + alfa += delta_alfa; + gamma += delta_gamma; + if alfa > max_alfa { + alfa = max_alfa; + } else if alfa < min_alfa { + alfa = min_alfa; + } + if gamma > max_gamma { + gamma = max_gamma; + } else if gamma < min_gamma { + gamma = min_gamma; + } + if (gamma == min_gamma || gamma == max_gamma) && (alfa == min_alfa || alfa == max_alfa) { + boundarytested += 1; + if boundarytested > 5 { + break; + } + } + iteration += 1; + } + + DoubleExpState { + base: Forecast { + smape: best_smape, + standarddeviation: best_standarddeviation, + forecast: best_constant_i + best_trend_i, + outliers, + }, + constant: best_constant_i, + trend: best_trend_i, + } +} + +/// Thin wrapper for the PyO3 export + parity test (one-step forecast only). +#[allow(clippy::too_many_arguments)] +pub fn double_exponential( + history: &[f64], + initial_alfa: f64, + min_alfa: f64, + max_alfa: f64, + initial_gamma: f64, + min_gamma: f64, + max_gamma: f64, + max_deviation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, +) -> Forecast { + double_exponential_state( + history, initial_alfa, min_alfa, max_alfa, initial_gamma, min_gamma, + max_gamma, max_deviation, smape_alfa, skip, iterations, + ) + .base +} + +#[cfg(test)] +mod tests { + use super::*; + fn run(h: &[f64]) -> Forecast { + // engine defaults (timeseries.cpp:625-631) + double_exponential(h, 0.2, 0.02, 1.0, 0.2, 0.05, 1.0, 4.0, 0.95, 5, 15) + } + + #[test] + fn too_short_returns_max() { + assert_eq!(run(&[1.0, 2.0, 3.0, 4.0]).smape, f64::MAX); + } + + #[test] + fn tracks_a_linear_trend_with_low_error() { + let h: Vec = (1..=30).map(|x| x as f64).collect(); + let r = run(&h); + assert!(r.smape.is_finite() && r.forecast.is_finite()); + // a clean linear trend should forecast well above the last value's level + assert!(r.forecast > 20.0, "forecast={}", r.forecast); + } + + #[test] + fn finite_on_long_oob_series() { + let long: Vec = (0..800).map(|x| 100.0 + (x % 13) as f64).collect(); + let r = run(&long); + assert!(r.smape.is_finite() && r.standarddeviation.is_finite()); + } +} diff --git a/rust/frepple-forecast/src/forecast.rs b/rust/frepple-forecast/src/forecast.rs new file mode 100644 index 0000000000..0ca02452c2 --- /dev/null +++ b/rust/frepple-forecast/src/forecast.rs @@ -0,0 +1,172 @@ +//! Memory-safe Rust port of the MovingAverage forecast method — Engine track E4, +//! slice 2. A faithful translation of `ForecastSolver::MovingAverage:: +//! generateForecast` (src/forecast/timeseries.cpp:294-384) and `smapeWeight` +//! (src/forecast/forecast.h:3041-3054), the exact `weight[]` out-of-bounds-read +//! site fixed earlier in the C++. +//! +//! The numeric core is ported verbatim (same f64 operation order, for tight +//! parity); the engine-model coupling (the two `new ProblemOutlier(...)` writes) +//! is replaced by returning the outlier indices. `#![forbid(unsafe_code)]` makes +//! the `weight[]` OOB read impossible (bounds-checked indexing + the clamp). +#![forbid(unsafe_code)] + +use crate::common::{smape_weight, weight_table, Forecast, ROUNDING_ERROR}; + +/// Moving-average forecast + SMAPE error over a history series. `history` is the +/// raw demand history (length = count); a trailing sentinel 0 is appended to +/// match `computeBaselineForecast`. Defaults in the engine: order=5, +/// max_deviation=4.0, smape_alfa=0.95, skip=5. +// `maxdeviation = 0.0` in the count<=1 branch is written then never read (it +// mirrors the C++ verbatim, which is dead there too) - keep it for a faithful +// port rather than diverge from timeseries.cpp. +#[allow(unused_assignments)] +pub fn moving_average( + history: &[f64], + order: u32, + max_deviation: f64, + smape_alfa: f64, + skip: u64, +) -> Forecast { + let order = order.max(1); + let order_f = order as f64; + let count = history.len(); + + // timeseries = history + trailing sentinel (timeseries.cpp:76-92). + let mut timeseries = Vec::with_capacity(count + 1); + timeseries.extend_from_slice(history); + timeseries.push(0.0); + + let weight = weight_table(smape_alfa); + let mut clean_history = vec![0.0f64; count + 1]; + let mut standarddeviation = 0.0f64; + let mut maxdeviation = 0.0f64; + let mut avg = 0.0f64; + let mut error_smape = 0.0f64; + let mut error_smape_weights = 0.0f64; + let mut outliers: Vec = Vec::new(); + + // Two passes: 0 = scan (compute stddev), 1 = filter (clean outliers). + for pass in 0..=1 { + if pass == 1 { + clean_history[0] = timeseries[0]; + } + error_smape = 0.0; + error_smape_weights = 0.0; + + let mut i = 1usize; + while i <= count { + let actual = timeseries[i]; + if pass == 0 { + let mut sum = 0.0; + let mut j = 0u32; + while j < order && (j as usize) < i { + sum += timeseries[i - j as usize - 1]; + j += 1; + } + avg = sum / order_f; + if i == count { + break; + } + standarddeviation += (avg - actual) * (avg - actual); + if (avg - actual).abs() > maxdeviation { + maxdeviation = (avg - actual).abs(); + } + } else { + let mut sum = 0.0; + let mut j = 0u32; + while j < order && (j as usize) < i { + sum += clean_history[i - j as usize - 1]; + j += 1; + } + avg = sum / order_f; + if i == count { + break; + } + if actual > avg + max_deviation * standarddeviation { + clean_history[i] = avg + max_deviation * standarddeviation; + outliers.push(i); + } else if actual < avg - max_deviation * standarddeviation { + clean_history[i] = avg - max_deviation * standarddeviation; + outliers.push(i); + } else { + clean_history[i] = actual; + } + } + + if i >= skip as usize && i < count && (avg + actual).abs() > ROUNDING_ERROR { + let w = smape_weight(&weight, (count - i) as i64); + error_smape += (avg - actual).abs() / (avg + actual).abs() * w; + error_smape_weights += w; + } + i += 1; + } + + if pass == 0 { + if count > 1 { + standarddeviation = (standarddeviation / (count as f64 - 1.0)).sqrt(); + maxdeviation /= standarddeviation; + if maxdeviation < max_deviation { + break; // no outliers -> skip the filter pass + } + } else { + standarddeviation = standarddeviation.sqrt(); + maxdeviation = 0.0; + break; + } + } + } + + if error_smape_weights != 0.0 { + error_smape /= error_smape_weights; + } + Forecast { + smape: error_smape, + standarddeviation, + forecast: avg, + outliers, + } +} + +#[cfg(test)] +mod tests { + use super::*; + const ORDER: u32 = 5; + const MAXDEV: f64 = 4.0; + const ALFA: f64 = 0.95; + const SKIP: u64 = 5; + + #[test] + fn constant_series_has_zero_error() { + let h = vec![10.0; 30]; + let r = moving_average(&h, ORDER, MAXDEV, ALFA, SKIP); + assert!(r.smape.abs() < 1e-12, "smape={}", r.smape); + assert!((r.forecast - 10.0).abs() < 1e-9, "avg={}", r.forecast); + assert!(r.outliers.is_empty()); + } + + #[test] + fn forecast_is_average_of_last_order_values() { + // Last 5 values: 6,7,8,9,10 -> avg 8.0 is the forecast. + let h: Vec = (1..=10).map(|x| x as f64).collect(); + let r = moving_average(&h, ORDER, MAXDEV, ALFA, SKIP); + assert!((r.forecast - 8.0).abs() < 1e-9, "avg={}", r.forecast); + } + + #[test] + fn detects_an_injected_outlier() { + let mut h = vec![10.0; 30]; + h[20] = 500.0; // a spike well beyond 4 sigma + let r = moving_average(&h, ORDER, MAXDEV, ALFA, SKIP); + assert!(r.outliers.contains(&20), "outliers={:?}", r.outliers); + } + + #[test] + fn long_series_past_maxbuckets_is_safe() { + // count - i exceeds MAXBUCKETS=500 -> the exact OOB-read case. Must not + // panic and must produce a finite smape (bounds-checked + clamped). + let h: Vec = (0..800).map(|x| 100.0 + (x % 7) as f64).collect(); + let r = moving_average(&h, ORDER, MAXDEV, ALFA, SKIP); + assert!(r.smape.is_finite(), "smape={}", r.smape); + assert!(r.standarddeviation.is_finite()); + } +} diff --git a/rust/frepple-forecast/src/lib.rs b/rust/frepple-forecast/src/lib.rs new file mode 100644 index 0000000000..8b342134a9 --- /dev/null +++ b/rust/frepple-forecast/src/lib.rs @@ -0,0 +1,166 @@ +//! frePPLe Rust/PyO3 pilot — forecast slice (Engine track E4). +//! +//! Pure numeric ports (memory-safe, `#![forbid(unsafe_code)]`, `cargo test`ed +//! with no Python) live in the method modules; the PyO3 bindings below are +//! compiled only into the wheel (`maturin build --features extension-module`) so +//! the parity tests can diff them against the verbatim C++ references. + +pub mod capi; // C ABI for the engine link (phase 7) +pub mod common; +pub mod croston; // Croston (phase 5) +pub mod double_exp; // DoubleExponential (phase 4) +pub mod forecast; // MovingAverage (slice 2) +pub mod seasonal; // Seasonal / Holt-Winters (phase 6) +pub mod single_exp; // SingleExponential (phase 3) + +#[cfg(feature = "extension-module")] +mod bindings { + use crate::croston as croston_mod; + use crate::seasonal as seasonal_mod; + use crate::{double_exp, forecast, single_exp}; + use pyo3::prelude::*; + + /// MovingAverage -> (smape, standarddeviation, forecast, outlier_indices). + #[pyfunction] + #[pyo3(signature = (history, order=5, max_deviation=4.0, smape_alfa=0.95, skip=5))] + fn moving_average( + history: Vec, + order: u32, + max_deviation: f64, + smape_alfa: f64, + skip: u64, + ) -> (f64, f64, f64, Vec) { + let r = forecast::moving_average(&history, order, max_deviation, smape_alfa, skip); + (r.smape, r.standarddeviation, r.forecast, r.outliers) + } + + /// SingleExponential -> (smape, standarddeviation, forecast, outlier_indices). + #[pyfunction] + #[pyo3(signature = ( + history, initial_alfa=0.2, min_alfa=0.03, max_alfa=1.0, + max_deviation=4.0, smape_alfa=0.95, skip=5, iterations=15 + ))] + #[allow(clippy::too_many_arguments)] + fn single_exponential( + history: Vec, + initial_alfa: f64, + min_alfa: f64, + max_alfa: f64, + max_deviation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, + ) -> (f64, f64, f64, Vec) { + let r = single_exp::single_exponential( + &history, + initial_alfa, + min_alfa, + max_alfa, + max_deviation, + smape_alfa, + skip, + iterations, + ); + (r.smape, r.standarddeviation, r.forecast, r.outliers) + } + + /// DoubleExponential -> (smape, standarddeviation, forecast, outlier_indices). + #[pyfunction] + #[pyo3(signature = ( + history, initial_alfa=0.2, min_alfa=0.02, max_alfa=1.0, + initial_gamma=0.2, min_gamma=0.05, max_gamma=1.0, + max_deviation=4.0, smape_alfa=0.95, skip=5, iterations=15 + ))] + #[allow(clippy::too_many_arguments)] + fn double_exponential( + history: Vec, + initial_alfa: f64, + min_alfa: f64, + max_alfa: f64, + initial_gamma: f64, + min_gamma: f64, + max_gamma: f64, + max_deviation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, + ) -> (f64, f64, f64, Vec) { + let r = double_exp::double_exponential( + &history, initial_alfa, min_alfa, max_alfa, initial_gamma, min_gamma, + max_gamma, max_deviation, smape_alfa, skip, iterations, + ); + (r.smape, r.standarddeviation, r.forecast, r.outliers) + } + + /// Croston -> (smape, standarddeviation, forecast, outlier_indices). + #[pyfunction] + #[pyo3(signature = ( + history, min_alfa=0.03, max_alfa=0.8, decay_rate=0.1, + max_deviation=4.0, smape_alfa=0.95, skip=5, iterations=15 + ))] + #[allow(clippy::too_many_arguments)] + fn croston( + history: Vec, + min_alfa: f64, + max_alfa: f64, + decay_rate: f64, + max_deviation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, + ) -> (f64, f64, f64, Vec) { + let r = croston_mod::croston( + &history, min_alfa, max_alfa, decay_rate, max_deviation, smape_alfa, skip, + iterations, + ); + (r.smape, r.standarddeviation, r.forecast, r.outliers) + } + + /// Seasonal -> (smape, standarddeviation, forecast, period, force, s_i[]). + #[pyfunction] + #[pyo3(signature = ( + history, initial_alfa=0.2, min_alfa=0.02, max_alfa=1.0, + initial_beta=0.2, min_beta=0.2, max_beta=1.0, gamma=0.05, + min_period=2, max_period=14, min_autocorrelation=0.5, max_autocorrelation=0.8, + smape_alfa=0.95, skip=5, iterations=15 + ))] + #[allow(clippy::too_many_arguments)] + fn seasonal( + history: Vec, + initial_alfa: f64, + min_alfa: f64, + max_alfa: f64, + initial_beta: f64, + min_beta: f64, + max_beta: f64, + gamma: f64, + min_period: usize, + max_period: usize, + min_autocorrelation: f64, + max_autocorrelation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, + ) -> (f64, f64, f64, u32, bool, Vec, f64, f64, u32) { + let r = seasonal_mod::seasonal( + &history, initial_alfa, min_alfa, max_alfa, initial_beta, min_beta, + max_beta, gamma, min_period, max_period, min_autocorrelation, + max_autocorrelation, smape_alfa, skip, iterations, + ); + // ...+ apply-state (l_i, t_i, cycleindex) for the phase-7 parity check. + ( + r.smape, r.standarddeviation, r.forecast, r.period, r.force, r.s_i, + r.l_i, r.t_i, r.cycleindex, + ) + } + + #[pymodule] + fn frepple_forecast(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(moving_average, m)?)?; + m.add_function(wrap_pyfunction!(single_exponential, m)?)?; + m.add_function(wrap_pyfunction!(double_exponential, m)?)?; + m.add_function(wrap_pyfunction!(croston, m)?)?; + m.add_function(wrap_pyfunction!(seasonal, m)?)?; + Ok(()) + } +} diff --git a/rust/frepple-forecast/src/seasonal.rs b/rust/frepple-forecast/src/seasonal.rs new file mode 100644 index 0000000000..2ac87f330a --- /dev/null +++ b/rust/frepple-forecast/src/seasonal.rs @@ -0,0 +1,396 @@ +//! Memory-safe Rust port of the Seasonal (Holt-Winters multiplicative) forecast +//! method (Engine track E4, phase 6) — the most entangled method. Faithful +//! translation of `ForecastSolver::Seasonal::detectCycle` +//! (src/forecast/timeseries.cpp:942-1002) + `generateForecast` +//! (timeseries.cpp:1004-1262): autocorrelation cycle detection, then a +//! seasonal-index Holt-Winters with a 2D Marquardt over (alfa, beta) (shared +//! `common::solve_2x2_marquardt`). No outlier detection. Same f64 op order for +//! tight parity. +//! +//! The seasonal state (L_i, T_i, S_i[period], period) flows generate->apply in +//! the C++ via member fields; here the generate result carries it explicitly so +//! the apply step (engine writes) can be reconstructed when integrated. +#![forbid(unsafe_code)] + +use crate::common::{smape_weight, solve_2x2_marquardt, weight_table, ACCURACY, ROUNDING_ERROR}; + +#[derive(Debug, Clone, PartialEq)] +pub struct SeasonalResult { + pub smape: f64, + pub standarddeviation: f64, + pub forecast: f64, + pub period: u32, + pub force: bool, + pub s_i: Vec, + // Apply-state (phase 7): the engine's Seasonal::applyForecast extrapolates per + // bucket as `L_i += T_i; T_i *= damp; fcst = L_i * S_i[cycleindex]` with + // `cycleindex` wrapping at `period`. So it needs the level/trend + the cycle + // position, not just the one-step `forecast`. cycleindex starts at count%period + // (the slot the C++ uses for the first forecast bucket). + pub l_i: f64, + pub t_i: f64, + pub cycleindex: u32, +} + +/// Autocorrelation cycle detection (timeseries.cpp:942-1002). Returns +/// (period, autocorrelation); period 0 means no seasonality. +#[allow(clippy::needless_range_loop)] +fn detect_cycle( + ts: &[f64], + count: usize, + min_period: usize, + max_period: usize, + min_autocorrelation: f64, +) -> (usize, f64) { + if count < min_period * 2 { + return (0, min_autocorrelation); + } + let mut average = 0.0; + for i in 0..count { + average += ts[i]; + } + average /= count as f64; + let mut variance = 0.0; + for i in 0..count { + variance += (ts[i] - average) * (ts[i] - average); + } + variance /= count as f64; + + let mut best_period = 0usize; + let mut best_autocorrelation = min_autocorrelation; + let mut correlations = [10.0f64; 7]; + let mut p = min_period; + while p <= max_period && p < count / 2 { + for i in (1..=6).rev() { + correlations[i] = correlations[i - 1]; + } + correlations[0] = 0.0; + for i in p..count { + correlations[0] += (ts[i - p] - average) * (ts[i] - average); + } + correlations[0] /= (count - p) as f64; + correlations[0] /= variance; + + if p > min_period + 1 + && correlations[1] > correlations[2] * 1.1 + && correlations[1] > correlations[0] * 1.1 + && correlations[1] > best_autocorrelation + { + best_autocorrelation = correlations[1]; + best_period = p - 1; + } + if p > min_period + 4 + && correlations[2] > best_autocorrelation + && correlations[2] > (correlations[0] + correlations[1]) / 2.0 + && correlations[2] > (correlations[3] + correlations[4]) / 2.0 + { + best_autocorrelation = correlations[2]; + best_period = p - 2; + } + if p > min_period + 6 + && correlations[3] > best_autocorrelation + && correlations[3] > (correlations[0] + correlations[1] + correlations[2]) / 3.0 + && correlations[3] > (correlations[4] + correlations[5] + correlations[6]) / 3.0 + { + best_autocorrelation = correlations[3]; + best_period = p - 3; + } + p += 1; + } + (best_period, best_autocorrelation) +} + +#[allow(clippy::too_many_arguments, clippy::needless_range_loop)] +pub fn seasonal( + history: &[f64], + initial_alfa: f64, + min_alfa: f64, + max_alfa: f64, + initial_beta: f64, + min_beta: f64, + max_beta: f64, + gamma: f64, + min_period: usize, + max_period: usize, + min_autocorrelation: f64, + max_autocorrelation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, +) -> SeasonalResult { + let count = history.len(); + let mut timeseries = history.to_vec(); + timeseries.push(0.0); + + let (period, autocorrelation) = + detect_cycle(×eries, count, min_period, max_period, min_autocorrelation); + if period == 0 { + return SeasonalResult { + smape: f64::MAX, + standarddeviation: f64::MAX, + forecast: 0.0, + period: 0, + force: false, + s_i: Vec::new(), + l_i: 0.0, + t_i: 0.0, + cycleindex: 0, + }; + } + + let weight = weight_table(smape_alfa); + let pf = period as f64; + let mut alfa = initial_alfa; + let mut beta = initial_beta; + + // Initial L_i, T_i, S_i (timeseries.cpp:1035-1057). + let mut l_i_initial = 0.0; + let mut t_i_initial = 0.0; + let mut initial_s_i = vec![0.0f64; period]; + for i in 0..period { + l_i_initial += timeseries[i]; + t_i_initial += timeseries[i + period] - timeseries[i]; + initial_s_i[i] = 0.0; + } + t_i_initial /= pf; + l_i_initial /= pf; + let mut cyclecount = 0u32; + let mut i = 0usize; + while i + period <= count { + cyclecount += 1; + let mut cyclesum = 0.0; + for j in 0..period { + cyclesum += timeseries[i + j]; + } + if cyclesum != 0.0 { + for j in 0..period { + initial_s_i[j] += timeseries[i + j] / cyclesum * pf; + } + } + i += period; + } + for s in initial_s_i.iter_mut() { + *s /= cyclecount as f64; + } + + let mut s_i = vec![0.0f64; period]; + let mut d_s_d_alfa = vec![0.0f64; period]; + let mut d_s_d_beta = vec![0.0f64; period]; + let mut best_s_i = vec![0.0f64; period]; + let mut best_l_i = l_i_initial; + let mut best_t_i = t_i_initial; + let mut l_i; + let mut t_i; + let mut best_error = f64::MAX; + let mut best_smape = 0.0f64; + let mut best_standarddeviation = 0.0f64; + let mut boundarytested = 0u32; + + let mut iteration: u64 = 1; + while iteration <= iterations { + let mut error = 0.0f64; + let mut error_smape = 0.0f64; + let mut error_smape_weights = 0.0f64; + let mut sum11 = 0.0f64; + let mut sum12 = 0.0f64; + let mut sum13 = 0.0f64; + let mut sum22 = 0.0f64; + let mut sum23 = 0.0f64; + let mut standarddeviation = 0.0f64; + let mut d_l_d_alfa = 0.0f64; + let mut d_l_d_beta = 0.0f64; + let mut d_t_d_alfa = 0.0f64; + let mut d_t_d_beta = 0.0f64; + l_i = l_i_initial; + t_i = t_i_initial; + let mut cyclesum = 0.0f64; + for ii in 0..period { + s_i[ii] = initial_s_i[ii]; + d_s_d_alfa[ii] = 0.0; + d_s_d_beta[ii] = 0.0; + if ii != 0 { + cyclesum += timeseries[ii - 1]; + } + } + + let mut prevcycleindex = period - 1; + let mut cycleindex = 0usize; + let mut i = period; + while i <= count { + let l_i_prev = l_i; + let actual = if i == count { 0.0 } else { timeseries[i] }; + cyclesum += timeseries[i - 1]; + if i > period { + cyclesum -= timeseries[i - period - 1]; + } + l_i = alfa * cyclesum / pf + (1.0 - alfa) * (l_i + t_i); + if l_i < 0.0 { + l_i = 0.0; + } + t_i = beta * (l_i - l_i_prev) + (1.0 - beta) * t_i; + let mut factor = -s_i[prevcycleindex]; + if l_i != 0.0 { + s_i[prevcycleindex] = + gamma * timeseries[i - 1] / l_i + (1.0 - gamma) * s_i[prevcycleindex]; + } + if s_i[prevcycleindex] < 0.0 { + s_i[prevcycleindex] = 0.0; + } + factor = pf / (pf + factor + s_i[prevcycleindex]); + for s in s_i.iter_mut() { + *s *= factor; + } + if i == count { + break; + } + let d_l_d_alfa_prev = d_l_d_alfa; + let d_l_d_beta_prev = d_l_d_beta; + let d_t_d_alfa_prev = d_t_d_alfa; + let d_t_d_beta_prev = d_t_d_beta; + let d_s_d_alfa_prev = d_s_d_alfa[prevcycleindex]; + let d_s_d_beta_prev = d_s_d_beta[prevcycleindex]; + d_l_d_alfa = + cyclesum / pf - (l_i + t_i) + (1.0 - alfa) * (d_l_d_alfa_prev + d_t_d_alfa_prev); + d_l_d_beta = (1.0 - alfa) * (d_l_d_beta_prev + d_t_d_beta_prev); + if l_i > ROUNDING_ERROR { + d_s_d_alfa[prevcycleindex] = + -gamma * timeseries[i - 1] / l_i / l_i * d_l_d_alfa_prev + + (1.0 - gamma) * d_s_d_alfa_prev; + d_s_d_beta[prevcycleindex] = + -gamma * timeseries[i - 1] / l_i / l_i * d_l_d_beta_prev + + (1.0 - gamma) * d_s_d_beta_prev; + } else { + d_s_d_alfa[prevcycleindex] = (1.0 - gamma) * d_s_d_alfa_prev; + d_s_d_beta[prevcycleindex] = (1.0 - gamma) * d_s_d_beta_prev; + } + d_t_d_alfa = beta * (d_l_d_alfa - d_l_d_alfa_prev) + (1.0 - beta) * d_t_d_alfa_prev; + d_t_d_beta = (l_i - l_i_prev) + beta * (d_l_d_beta - d_l_d_beta_prev) - t_i + + (1.0 - beta) * d_t_d_beta_prev; + let d_forecast_d_alfa = + (d_l_d_alfa + d_t_d_alfa) * s_i[cycleindex] + (l_i + t_i) * d_s_d_alfa[cycleindex]; + let d_forecast_d_beta = + (d_l_d_beta + d_t_d_beta) * s_i[cycleindex] + (l_i + t_i) * d_s_d_beta[cycleindex]; + let forecast_i = (l_i + t_i) * s_i[cycleindex]; + let w = smape_weight(&weight, (count - i) as i64); + sum11 += w * d_forecast_d_alfa * d_forecast_d_alfa; + sum12 += w * d_forecast_d_alfa * d_forecast_d_beta; + sum22 += w * d_forecast_d_beta * d_forecast_d_beta; + sum13 += w * d_forecast_d_alfa * (actual - forecast_i); + sum23 += w * d_forecast_d_beta * (actual - forecast_i); + if (i as u64) >= skip { + let fcst = (l_i + t_i) * s_i[cycleindex]; + error += (fcst - actual) * (fcst - actual) * w; + if (fcst + actual).abs() > ROUNDING_ERROR { + error_smape += (fcst - actual).abs() / (fcst + actual).abs() * w; + error_smape_weights += w; + standarddeviation += (fcst - actual) * (fcst - actual); + } + } + cycleindex += 1; + if cycleindex >= period { + cycleindex = 0; + } + prevcycleindex += 1; + if prevcycleindex >= period { + prevcycleindex = 0; + } + i += 1; + } + + if error < best_error { + best_error = error; + best_smape = if error_smape_weights != 0.0 { + error_smape / error_smape_weights + } else { + 0.0 + }; + best_l_i = l_i; + best_t_i = t_i; + best_standarddeviation = (standarddeviation / (count as f64 - pf - 1.0)).sqrt(); + best_s_i[..period].copy_from_slice(&s_i[..period]); + } + + let delta = solve_2x2_marquardt(sum11, sum12, sum22, sum13, sum23, error / iteration as f64); + let (delta_alfa, delta_beta) = match delta { + Some(d) => d, + None => break, + }; + if (delta_alfa.abs() + delta_beta.abs()) < 3.0 * ACCURACY && iteration > 3 { + break; + } + alfa += delta_alfa; + beta += delta_beta; + if alfa > max_alfa { + alfa = max_alfa; + } else if alfa < min_alfa { + alfa = min_alfa; + } + if beta > max_beta { + beta = max_beta; + } else if beta < min_beta { + beta = min_beta; + } + if (beta == min_beta || beta == max_beta) && (alfa == min_alfa || alfa == max_alfa) { + boundarytested += 1; + if boundarytested > 5 { + break; + } + } + iteration += 1; + } + + if (period as u64) > skip { + best_smape *= count as f64 - skip as f64; + best_smape /= count as f64 - pf; + } + + // Restore best + the logged forecast value (timeseries.cpp:1242-1253). + let l_i = best_l_i; + let t_i = best_t_i; + let forecast = (l_i + t_i / pf) * best_s_i[count % period]; + SeasonalResult { + smape: best_smape, + standarddeviation: best_standarddeviation, + forecast, + period: period as u32, + force: autocorrelation > max_autocorrelation, + s_i: best_s_i, + // applyForecast resumes from here: the C++ leaves cycleindex at count%period + // (incremented from 0 each smoothing pass), and L_i/T_i = the best snapshot. + l_i, + t_i, + cycleindex: (count % period) as u32, + } +} + +#[cfg(test)] +mod tests { + use super::*; + #[allow(clippy::too_many_arguments)] + fn run(h: &[f64]) -> SeasonalResult { + // engine defaults (timeseries.cpp:929-940) + seasonal(h, 0.2, 0.02, 1.0, 0.2, 0.2, 1.0, 0.05, 2, 14, 0.5, 0.8, 0.95, 5, 15) + } + + #[test] + fn no_cycle_returns_max() { + // monotone trend, no seasonality -> period 0 -> MAX + let h: Vec = (1..=40).map(|x| x as f64).collect(); + let r = run(&h); + assert_eq!(r.period, 0); + assert_eq!(r.smape, f64::MAX); + } + + #[test] + fn strong_seasonal_is_finite() { + // a clear period-7 cycle repeated; if detected, outputs must be finite + let cycle = [10.0, 25.0, 40.0, 55.0, 40.0, 25.0, 10.0]; + let h: Vec = (0..70).map(|x| cycle[x % 7]).collect(); + let r = run(&h); + assert!(r.smape.is_finite() && r.standarddeviation.is_finite()); + assert!(r.forecast.is_finite()); + if r.period != 0 { + assert_eq!(r.s_i.len(), r.period as usize); + } + } +} diff --git a/rust/frepple-forecast/src/single_exp.rs b/rust/frepple-forecast/src/single_exp.rs new file mode 100644 index 0000000000..ebab2f0720 --- /dev/null +++ b/rust/frepple-forecast/src/single_exp.rs @@ -0,0 +1,230 @@ +//! Memory-safe Rust port of the SingleExponential forecast method (Engine track +//! E4, phase 3). Faithful translation of +//! `ForecastSolver::SingleExponential::generateForecast` +//! (src/forecast/timeseries.cpp:420-593): single exponential smoothing with a 1D +//! Levenberg-Marquardt optimisation of `alfa`, the two-pass outlier scan/filter, +//! and the weighted SMAPE. Same f64 operation order as the C++ for tight parity; +//! outlier ProblemOutlier writes are replaced by returned indices. +#![forbid(unsafe_code)] + +use crate::common::{smape_weight, weight_table, Forecast, ACCURACY, ROUNDING_ERROR}; + +#[allow(clippy::too_many_arguments)] +pub fn single_exponential( + history: &[f64], + initial_alfa: f64, + min_alfa: f64, + max_alfa: f64, + max_deviation: f64, + smape_alfa: f64, + skip: u64, + iterations: u64, +) -> Forecast { + let count = history.len(); + // Needs at least skip+5 buckets (timeseries.cpp:426-427). + if (count as u64) < skip + 5 { + return Forecast { + smape: f64::MAX, + standarddeviation: f64::MAX, + forecast: 0.0, + outliers: Vec::new(), + }; + } + + let mut timeseries = history.to_vec(); + timeseries.push(0.0); // trailing sentinel + let weight = weight_table(smape_alfa); + + // Constructor clamp (forecast.h: SingleExponential(a): alfa(a) then >= min). + let mut alfa = if initial_alfa < min_alfa { + min_alfa + } else { + initial_alfa + }; + let mut f_i = 0.0f64; + let mut outliers: Vec = Vec::new(); + let mut upper_tested = false; + let mut lower_tested = false; + + let mut best_error = f64::MAX; + let mut best_f_i = 0.0f64; + let mut best_smape = 0.0f64; + let mut best_standarddeviation = 0.0f64; + + let mut iteration: u64 = 1; + while iteration <= iterations { + let mut standarddeviation = 0.0f64; + let mut maxdeviation = 0.0f64; + // Read after the outlier loop (last pass wins) for the Marquardt step. + let mut sum_11 = 0.0f64; + let mut sum_12 = 0.0f64; + let mut error = 0.0f64; + let mut error_smape = 0.0f64; + let mut error_smape_weights = 0.0f64; + + for pass in 0..=1 { + let mut df_dalfa_i = 0.0f64; + sum_11 = 0.0; + sum_12 = 0.0; + error_smape = 0.0; + error_smape_weights = 0.0; + error = 0.0; + + // Initialise f_i with the average of the first 3 values. + let history_0 = timeseries[0]; + let history_1 = timeseries[1]; + let history_2 = timeseries[2]; + f_i = (history_0 + history_1 + history_2) / 3.0; + if pass == 1 { + let mut t = 0.0; + for &h in &[history_0, history_1, history_2] { + if h > f_i + max_deviation * standarddeviation { + t += f_i + max_deviation * standarddeviation; + } else if h < f_i - max_deviation * standarddeviation { + t += f_i - max_deviation * standarddeviation; + } else { + t += h; + } + } + f_i = t / 3.0; + } + + let mut history_i = history_0; + let mut i = 1usize; + while i <= count { + let history_i_min_1 = history_i; + history_i = timeseries[i]; + df_dalfa_i = history_i_min_1 - f_i + (1.0 - alfa) * df_dalfa_i; + f_i = history_i_min_1 * alfa + (1.0 - alfa) * f_i; + if i == count { + break; + } + if pass == 0 { + standarddeviation += (f_i - history_i) * (f_i - history_i); + if (f_i - history_i).abs() > maxdeviation { + maxdeviation = (f_i - history_i).abs(); + } + } else if history_i > f_i + max_deviation * standarddeviation { + history_i = f_i + max_deviation * standarddeviation; + if iteration == 1 { + outliers.push(i); + } + } else if history_i < f_i - max_deviation * standarddeviation { + history_i = f_i - max_deviation * standarddeviation; + if iteration == 1 { + outliers.push(i); + } + } + let w = smape_weight(&weight, (count - i) as i64); + sum_12 += df_dalfa_i * (history_i - f_i) * w; + sum_11 += df_dalfa_i * df_dalfa_i * w; + if (i as u64) >= skip { + error += (f_i - history_i) * (f_i - history_i) * w; + // Note: the C++ divides by (f_i + history_i), NOT its abs. + if (f_i + history_i).abs() > ROUNDING_ERROR { + error_smape += (f_i - history_i).abs() / (f_i + history_i) * w; + error_smape_weights += w; + } + } + i += 1; + } + + if pass == 0 { + standarddeviation = (standarddeviation / (count as f64 - 1.0)).sqrt(); + maxdeviation /= standarddeviation; + if maxdeviation < max_deviation { + break; // no outliers -> skip the filter pass + } + } + } + + if error < best_error { + best_error = error; + best_smape = if error_smape_weights != 0.0 { + error_smape / error_smape_weights + } else { + 0.0 + }; + best_f_i = f_i; + best_standarddeviation = standarddeviation; + } + + // Levenberg-Marquardt damping + alfa update. + if (sum_11 + error / iteration as f64).abs() > ROUNDING_ERROR { + sum_11 += error / iteration as f64; + } + if sum_11.abs() < ROUNDING_ERROR { + break; + } + let delta = sum_12 / sum_11; + if delta.abs() < ACCURACY && iteration > 3 { + break; + } + alfa += delta; + if alfa > max_alfa { + alfa = max_alfa; + if upper_tested { + break; + } + upper_tested = true; + } else if alfa < min_alfa { + alfa = min_alfa; + if lower_tested { + break; + } + lower_tested = true; + } + iteration += 1; + } + + Forecast { + smape: best_smape, + standarddeviation: best_standarddeviation, + forecast: best_f_i, + outliers, + } +} + +#[cfg(test)] +mod tests { + use super::*; + // Engine defaults (timeseries.cpp:32-36, 416-418). + const INIT_ALFA: f64 = 0.2; + const MIN_ALFA: f64 = 0.03; + const MAX_ALFA: f64 = 1.0; + const MAXDEV: f64 = 4.0; + const SMAPE_ALFA: f64 = 0.95; + const SKIP: u64 = 5; + const ITERS: u64 = 15; + + fn run(h: &[f64]) -> Forecast { + single_exponential( + h, INIT_ALFA, MIN_ALFA, MAX_ALFA, MAXDEV, SMAPE_ALFA, SKIP, ITERS, + ) + } + + #[test] + fn too_short_returns_max() { + let r = run(&[1.0, 2.0, 3.0]); // < skip+5 + assert_eq!(r.smape, f64::MAX); + } + + #[test] + fn constant_series_is_near_zero_error() { + let r = run(&vec![10.0; 30]); + assert!(r.smape.abs() < 1e-9, "smape={}", r.smape); + assert!((r.forecast - 10.0).abs() < 1e-6, "forecast={}", r.forecast); + } + + #[test] + fn finite_on_trend_and_long_oob_series() { + let trend: Vec = (1..=40).map(|x| x as f64).collect(); + let rt = run(&trend); + assert!(rt.smape.is_finite() && rt.forecast.is_finite()); + + // > MAXBUCKETS -> the smapeWeight OOB site; must stay safe + finite. + let long: Vec = (0..800).map(|x| 100.0 + (x % 11) as f64).collect(); + let rl = run(&long); + assert!(rl.smape.is_finite() && rl.standarddeviation.is_finite()); + } +} diff --git a/rust/frepple-num/.gitignore b/rust/frepple-num/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/rust/frepple-num/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rust/frepple-num/Cargo.lock b/rust/frepple-num/Cargo.lock new file mode 100644 index 0000000000..af326dec66 --- /dev/null +++ b/rust/frepple-num/Cargo.lock @@ -0,0 +1,180 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "frepple-num" +version = "0.1.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "indoc" +version = "2.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79cf5c93f93228cf8efb3ba362535fb11199ac548a09ce117c9b1adc3030d706" +dependencies = [ + "rustversion", +] + +[[package]] +name = "libc" +version = "0.2.186" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" + +[[package]] +name = "memoffset" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f402062616ab18202ae8319da13fa4279883a2b8a9d9f83f20dbade813ce1884" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "once_cell", + "portable-atomic", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b14b5775b5ff446dd1056212d778012cbe8a0fbffd368029fd9e25b514479c38" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ab5bcf04a2cdcbb50c7d6105de943f543f9ed92af55818fd17b660390fc8636" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fd24d897903a9e6d80b968368a34e1525aeb719d568dba8b3d4bfa5dc67d453" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.22.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "36c011a03ba1e50152b4b394b479826cad97e7a21eb52df179cd91ac411cbfbe" +dependencies = [ + "heck", + "proc-macro2", + "pyo3-build-config", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "unindent" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3" diff --git a/rust/frepple-num/Cargo.toml b/rust/frepple-num/Cargo.toml new file mode 100644 index 0000000000..b813ac91fb --- /dev/null +++ b/rust/frepple-num/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "frepple-num" +version = "0.1.0" +edition = "2021" +description = "frePPLe Rust/PyO3 pilot (Engine track E4): memory-safe JSON number clamping" +license = "MIT" + +[lib] +name = "frepple_num" +crate-type = ["cdylib", "rlib"] + +# pyo3 is OPTIONAL + gated behind `extension-module` so `cargo test` runs the +# pure-logic unit tests with zero Python/toolchain dependency, while +# `maturin build --features extension-module` produces the importable wheel. +[dependencies] +pyo3 = { version = "0.22", optional = true } + +[features] +extension-module = ["dep:pyo3", "pyo3/extension-module"] diff --git a/rust/frepple-num/pyproject.toml b/rust/frepple-num/pyproject.toml new file mode 100644 index 0000000000..23a4f143db --- /dev/null +++ b/rust/frepple-num/pyproject.toml @@ -0,0 +1,13 @@ +[build-system] +requires = ["maturin>=1.5,<2.0"] +build-backend = "maturin" + +[project] +name = "frepple-num" +version = "0.1.0" +requires-python = ">=3.12" +description = "frePPLe Rust/PyO3 pilot: memory-safe JSON number clamping (Engine track E4)" + +[tool.maturin] +# Build the cdylib as a CPython extension module named `frepple_num`. +features = ["extension-module"] diff --git a/rust/frepple-num/src/lib.rs b/rust/frepple-num/src/lib.rs new file mode 100644 index 0000000000..f2ae8f899e --- /dev/null +++ b/rust/frepple-num/src/lib.rs @@ -0,0 +1,43 @@ +//! frePPLe Rust/PyO3 pilot (Engine track E4). +//! +//! The pure conversion logic lives in `num` (memory-safe, `#![forbid(unsafe_code)]`, +//! unit-tested by `cargo test` with no Python dependency). The PyO3 bindings below +//! are compiled only into the wheel (`maturin build --features extension-module`) +//! so the Python parity test can diff them against the C++ reference. + +pub mod num; + +#[cfg(feature = "extension-module")] +mod bindings { + use crate::num; + use pyo3::prelude::*; + + #[pyfunction] + fn clamp_to_long(x: f64) -> i64 { + num::clamp_to_long(x) + } + + #[pyfunction] + fn clamp_to_int(x: f64) -> i32 { + num::clamp_to_int(x) + } + + #[pyfunction] + fn clamp_to_unsigned_long(x: f64) -> u64 { + num::clamp_to_unsigned_long(x) + } + + #[pyfunction] + fn parse_long(s: &str) -> i64 { + num::parse_long(s) + } + + #[pymodule] + fn frepple_num(m: &Bound<'_, PyModule>) -> PyResult<()> { + m.add_function(wrap_pyfunction!(clamp_to_long, m)?)?; + m.add_function(wrap_pyfunction!(clamp_to_int, m)?)?; + m.add_function(wrap_pyfunction!(clamp_to_unsigned_long, m)?)?; + m.add_function(wrap_pyfunction!(parse_long, m)?)?; + Ok(()) + } +} diff --git a/rust/frepple-num/src/num.rs b/rust/frepple-num/src/num.rs new file mode 100644 index 0000000000..6ea98e72eb --- /dev/null +++ b/rust/frepple-num/src/num.rs @@ -0,0 +1,117 @@ +//! Pure, memory-safe number conversions — the Rust pilot port of the JSON +//! number getters in `src/utils/json.cpp` (`getLong` 790-815, `getUnsignedLong` +//! 817-841, `getInt` 865-890). These are the exact site of the inverted-bound +//! bug fixed earlier in the C++ (`< LONG_MIN` had been `> LONG_MIN`, which made +//! `getLong` return LONG_MIN for ordinary doubles). +//! +//! The whole point of the pilot: the C++ needs hand-written clamping (and got it +//! wrong). In Rust a float->int `as` cast is **saturating** and maps `NaN -> 0` +//! by language definition, so the bug class is impossible by construction — and +//! this module forbids `unsafe` entirely. +#![forbid(unsafe_code)] + +/// double -> signed 64-bit, clamped to [i64::MIN, i64::MAX], truncated toward +/// zero, NaN -> 0. Mirrors `JSONData::getLong()`'s JSON_DOUBLE branch. +pub fn clamp_to_long(x: f64) -> i64 { + x as i64 +} + +/// double -> signed 32-bit, clamped to [i32::MIN, i32::MAX]. Mirrors +/// `JSONData::getInt()`'s JSON_DOUBLE branch. +pub fn clamp_to_int(x: f64) -> i32 { + x as i32 +} + +/// double -> unsigned 64-bit, clamped to [0, u64::MAX]. Mirrors +/// `JSONData::getUnsignedLong()`'s JSON_DOUBLE branch, EXCEPT the C++ has no +/// lower clamp: a negative double there is cast to a signed long and reinterpreted +/// unsigned (wraps to a huge value / UB). Rust saturates negatives to 0 — a +/// strictly safer, defined result. (Flagged in the parity test + report.) +pub fn clamp_to_unsigned_long(x: f64) -> u64 { + x as u64 +} + +/// string -> signed 64-bit, `atol`-style: skip leading whitespace, an optional +/// sign, then consume digits until a non-digit; "" / no digits -> 0. Mirrors the +/// JSON_STRING branch (`atol`). Overflow saturates (C `atol` overflow is UB). +pub fn parse_long(s: &str) -> i64 { + let bytes = s.trim_start().as_bytes(); + let mut i = 0; + let neg = match bytes.first() { + Some(b'-') => { + i = 1; + true + } + Some(b'+') => { + i = 1; + false + } + _ => false, + }; + let mut val: i64 = 0; + while i < bytes.len() && bytes[i].is_ascii_digit() { + let d = (bytes[i] - b'0') as i64; + val = val.saturating_mul(10).saturating_add(d); + i += 1; + } + if neg { + val.saturating_neg() + } else { + val + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn truncates_toward_zero() { + assert_eq!(clamp_to_long(42.9), 42); + assert_eq!(clamp_to_long(-42.9), -42); + assert_eq!(clamp_to_int(7.9), 7); + assert_eq!(clamp_to_int(-7.9), -7); + } + + #[test] + fn regression_ordinary_double_is_not_clamped() { + // The C++ bug returned LONG_MIN for ordinary doubles (inverted bound). + // Saturating-cast Rust returns the real value - the bug can't exist here. + assert_eq!(clamp_to_long(5.0), 5); + assert_ne!(clamp_to_long(5.0), i64::MIN); + assert_eq!(clamp_to_int(5.0), 5); + } + + #[test] + fn saturates_out_of_range() { + assert_eq!(clamp_to_long(1e30), i64::MAX); + assert_eq!(clamp_to_long(-1e30), i64::MIN); + assert_eq!(clamp_to_int(1e30), i32::MAX); + assert_eq!(clamp_to_int(-1e30), i32::MIN); + assert_eq!(clamp_to_unsigned_long(1e30), u64::MAX); + } + + #[test] + fn nan_and_inf_are_defined() { + assert_eq!(clamp_to_long(f64::NAN), 0); // C++ static_cast(NaN) is UB + assert_eq!(clamp_to_long(f64::INFINITY), i64::MAX); + assert_eq!(clamp_to_long(f64::NEG_INFINITY), i64::MIN); + assert_eq!(clamp_to_unsigned_long(f64::NAN), 0); + } + + #[test] + fn negative_to_unsigned_saturates_to_zero() { + // C++ wraps this to a huge value; Rust saturates to 0 (safe + defined). + assert_eq!(clamp_to_unsigned_long(-5.0), 0); + } + + #[test] + fn parses_like_atol() { + assert_eq!(parse_long("42"), 42); + assert_eq!(parse_long("-17"), -17); + assert_eq!(parse_long("+9"), 9); + assert_eq!(parse_long(" 123abc"), 123); + assert_eq!(parse_long("abc"), 0); + assert_eq!(parse_long(""), 0); + } +} diff --git a/rust/solver-spike/.gitignore b/rust/solver-spike/.gitignore new file mode 100644 index 0000000000..ea8c4bf7f3 --- /dev/null +++ b/rust/solver-spike/.gitignore @@ -0,0 +1 @@ +/target diff --git a/rust/solver-spike/Cargo.lock b/rust/solver-spike/Cargo.lock new file mode 100644 index 0000000000..5d02a035f4 --- /dev/null +++ b/rust/solver-spike/Cargo.lock @@ -0,0 +1,301 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "autocfg" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" + +[[package]] +name = "bumpalo" +version = "3.20.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649" + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "good_lp" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "745190412d5ff4a54335cd16229a475ad3fb8f5474a5c1358292d62932187ea7" +dependencies = [ + "fnv", + "microlp", +] + +[[package]] +name = "js-sys" +version = "0.3.102" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03d04c30968dffe80775bd4d7fb676131cd04a1fb46d2686dbffbaec2d9dfd31" +dependencies = [ + "cfg-if", + "futures-util", + "wasm-bindgen", +] + +[[package]] +name = "log" +version = "0.4.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "953f07c43838f8e6f9758cab68bf5bed85465e7587ebe0b823f1bcd81978ad3a" + +[[package]] +name = "matrixmultiply" +version = "0.3.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a06de3016e9fae57a36fd14dba131fccf49f74b40b7fbdb472f96e361ec71a08" +dependencies = [ + "autocfg", + "rawpointer", +] + +[[package]] +name = "microlp" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "458ed987196f802dc47c69d4c5afcd19002d6c1c5f8f75c76d129bcf2425057a" +dependencies = [ + "log", + "sprs", + "web-time", +] + +[[package]] +name = "ndarray" +version = "0.17.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520080814a7a6b4a6e9070823bb24b4531daac8c4627e08ba5de8c5ef2f2752d" +dependencies = [ + "matrixmultiply", + "num-complex", + "num-integer", + "num-traits", + "portable-atomic", + "portable-atomic-util", + "rawpointer", +] + +[[package]] +name = "num-complex" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.21.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" + +[[package]] +name = "pin-project-lite" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" + +[[package]] +name = "portable-atomic" +version = "1.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" + +[[package]] +name = "portable-atomic-util" +version = "0.2.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c2a106d1259c23fac8e543272398ae0e3c0b8d33c88ed73d0cc71b0f1d902618" +dependencies = [ + "portable-atomic", +] + +[[package]] +name = "proc-macro2" +version = "1.0.106" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "quote" +version = "1.0.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rawpointer" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" + +[[package]] +name = "rustversion" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" + +[[package]] +name = "slab" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" + +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "solver-spike" +version = "0.1.0" +dependencies = [ + "good_lp", +] + +[[package]] +name = "sprs" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dca58a33be2188d4edc71534f8bafa826e787cc28ca1c47f31be3423f0d6e55" +dependencies = [ + "ndarray", + "num-complex", + "num-traits", + "smallvec", +] + +[[package]] +name = "syn" +version = "2.0.118" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9ae57f904213ebb649ce6895b8a66c66f0203b9319718f69a5612a065b1422" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "unicode-ident" +version = "1.0.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" + +[[package]] +name = "wasm-bindgen" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ddb3f79143bced6de84270411622a2699cee572fc0875aeaf1e7867cf9fca1a" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4e21a184b13fb19e157296e2c46056aec9092264fab83e4ba59e68c61b323c3d" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fecefd9c35bd935a20fc3fc344b5f29138961e4f47fb03297d88f2587afb5ebd" +dependencies = [ + "bumpalo", + "proc-macro2", + "quote", + "syn", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.125" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23939e44bb9a5d7576fa2b563dc2e136628f1224e88a8deed09e04858b77871f" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-time" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb" +dependencies = [ + "js-sys", + "wasm-bindgen", +] diff --git a/rust/solver-spike/Cargo.toml b/rust/solver-spike/Cargo.toml new file mode 100644 index 0000000000..9f72da7177 --- /dev/null +++ b/rust/solver-spike/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "solver-spike" +version = "0.1.0" +edition = "2021" +publish = false + +# Evidence-gathering spike (Engine track): can an advanced optimization engine, +# orchestrated from Rust, power a greenfield finite-capacity planning mode? We +# model a small capacitated production-planning LP with the `good_lp` modeling +# layer + the PURE-RUST `microlp` backend (no C++ HiGHS build). good_lp lets the +# same model swap to HiGHS/CBC/SCIP via a feature flag when scale demands it. +[dependencies] +good_lp = { version = "1.13", default-features = false, features = ["microlp"] } diff --git a/rust/solver-spike/src/main.rs b/rust/solver-spike/src/main.rs new file mode 100644 index 0000000000..a7b76d9acf --- /dev/null +++ b/rust/solver-spike/src/main.rs @@ -0,0 +1,167 @@ +//! Solver spike (Engine track): can an advanced optimization engine, driven from +//! Rust, power a greenfield *finite-capacity* planning mode for frePPLe? +//! +//! frePPLe's shipping MRP solver is a fast *constructive heuristic* — it pegs +//! demand to supply operation-by-operation and is excellent at feasibility, but +//! it does not optimise a global objective. The classic problem where that gap +//! shows is **capacitated production planning**: when a resource is tight, the +//! cheapest feasible plan pre-builds inventory in slack periods. A heuristic that +//! plans lot-for-lot can't see that trade-off; an optimiser finds it exactly. +//! +//! This spike models a small multi-period, multi-product capacitated lot-sizing +//! LP with the `good_lp` modelling layer (pure-Rust `microlp` backend) and +//! compares the optimal cost against a lot-for-lot heuristic — the kind of plan a +//! naive MRP pass produces. The same model object swaps to HiGHS / CBC / SCIP +//! behind a `good_lp` feature flag when scale demands a heavier solver. + +use good_lp::{constraint, variable, variables, Expression, Solution, SolverModel}; + +/// A tiny but non-trivial instance: 2 products over 4 periods, one shared +/// capacity-constrained resource. Demand spikes in the last period beyond what +/// the resource can make in a single period — so a feasible plan MUST pre-build. +struct Instance { + periods: usize, + products: usize, + demand: Vec>, // [product][period] + prod_cost: Vec, // per unit produced + hold_cost: Vec, // per unit of end-of-period inventory + res_per_unit: Vec, // resource units consumed per unit produced + capacity: Vec, // resource units available [period] +} + +fn demo_instance() -> Instance { + Instance { + periods: 4, + products: 2, + // P1 ramps to a spike in period 4; P2 steady. The spike exceeds one + // period's capacity once P2 is accounted for, forcing a pre-build. + demand: vec![ + vec![20.0, 20.0, 20.0, 60.0], // product 1 + vec![15.0, 15.0, 15.0, 15.0], // product 2 + ], + prod_cost: vec![1.0, 1.0], + hold_cost: vec![0.2, 0.2], + res_per_unit: vec![1.0, 1.0], + // 50 units/period of the shared resource. Period-4 demand = 60+15 = 75 > 50, + // so it can't all be made in period 4: a feasible plan builds ahead. + capacity: vec![50.0, 50.0, 50.0, 50.0], + } +} + +/// Solve the capacitated lot-sizing LP to optimality. Returns (total_cost, plan) +/// where plan[product][period] is the optimal production quantity. +fn solve_optimal(inst: &Instance) -> (f64, Vec>) { + let (np, nt) = (inst.products, inst.periods); + let mut vars = variables!(); + + // Decision vars: production x[p][t] >= 0 and end inventory inv[p][t] >= 0. + let x: Vec> = (0..np) + .map(|_| (0..nt).map(|_| vars.add(variable().min(0.0))).collect()) + .collect(); + let inv: Vec> = (0..np) + .map(|_| (0..nt).map(|_| vars.add(variable().min(0.0))).collect()) + .collect(); + + // Objective: minimise production + holding cost. + let mut objective = Expression::from(0.0); + for p in 0..np { + for t in 0..nt { + objective += inst.prod_cost[p] * x[p][t] + inst.hold_cost[p] * inv[p][t]; + } + } + + let mut model = vars.minimise(objective).using(good_lp::default_solver); + + // Inventory balance: inv[p][t] = inv[p][t-1] + x[p][t] - demand[p][t]. + for p in 0..np { + for t in 0..nt { + let prev: Expression = if t == 0 { 0.0.into() } else { inv[p][t - 1].into() }; + model = model.with(constraint!(inv[p][t] == prev + x[p][t] - inst.demand[p][t])); + } + } + // Finite capacity: sum_p res_per_unit[p] * x[p][t] <= capacity[t]. + for t in 0..nt { + let mut load = Expression::from(0.0); + for p in 0..np { + load += inst.res_per_unit[p] * x[p][t]; + } + model = model.with(constraint!(load <= inst.capacity[t])); + } + + let sol = model.solve().expect("LP should be feasible"); + let plan: Vec> = x + .iter() + .map(|row| row.iter().map(|v| sol.value(*v)).collect()) + .collect(); + let cost = total_cost(inst, &plan); + (cost, plan) +} + +/// The naive lot-for-lot plan: make exactly each period's demand, ignoring the +/// capacity-smoothing opportunity. This is what a feasibility-first MRP pass +/// produces. It may be INFEASIBLE on capacity (we report the overload). +fn lot_for_lot(inst: &Instance) -> (f64, Vec>, Vec) { + let plan: Vec> = inst.demand.clone(); + let mut overload = vec![0.0; inst.periods]; + for t in 0..inst.periods { + let load: f64 = (0..inst.products).map(|p| inst.res_per_unit[p] * plan[p][t]).sum(); + overload[t] = (load - inst.capacity[t]).max(0.0); + } + (total_cost(inst, &plan), plan, overload) +} + +/// Production + holding cost of a plan, carrying inventory forward. +fn total_cost(inst: &Instance, plan: &[Vec]) -> f64 { + let mut cost = 0.0; + for p in 0..inst.products { + let mut inv = 0.0; + for t in 0..inst.periods { + inv += plan[p][t] - inst.demand[p][t]; + if inv < 0.0 { + inv = 0.0; // unmet demand isn't held (lot-for-lot meets each period) + } + cost += inst.prod_cost[p] * plan[p][t] + inst.hold_cost[p] * inv; + } + } + cost +} + +fn main() { + let inst = demo_instance(); + + let (lfl_cost, lfl_plan, overload) = lot_for_lot(&inst); + let (opt_cost, opt_plan) = solve_optimal(&inst); + + println!("Capacitated production-planning spike (good_lp + microlp)\n"); + println!( + "Instance: {} products x {} periods, shared capacity {:?}\n", + inst.products, inst.periods, inst.capacity + ); + + println!("Lot-for-lot (feasibility-first heuristic, like a naive MRP pass):"); + print_plan(&inst, &lfl_plan); + let infeasible: f64 = overload.iter().sum(); + if infeasible > 0.0 { + println!(" ! capacity OVERLOAD per period: {:?}", overload); + println!(" -> the lot-for-lot plan is INFEASIBLE on the tight resource."); + } + println!(" cost (if it were feasible): {:.2}\n", lfl_cost); + + println!("Optimal (capacitated LP, microlp):"); + print_plan(&inst, &opt_plan); + println!(" cost: {:.2}", opt_cost); + println!(" -> capacity-feasible, pre-builds ahead of the period-4 spike.\n"); + + println!( + "Optimiser vs heuristic: feasible where lot-for-lot overloads by {:.0} units; \ + optimum {:.2} vs naive {:.2}.", + infeasible, opt_cost, lfl_cost + ); +} + +fn print_plan(inst: &Instance, plan: &[Vec]) { + for p in 0..inst.products { + let row: Vec = plan[p].iter().map(|v| format!("{:5.1}", v)).collect(); + println!(" product {}: produce [{}]", p + 1, row.join(", ")); + } +} diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 65a085fb13..6e69475817 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -94,6 +94,33 @@ endif() add_dependencies(forecast solver) +# Phase 7: build the Rust forecast staticlib (cargo) and link it into the +# forecast lib. The numeric method bodies in timeseries.cpp then dispatch to the +# extern "C" Rust functions behind FREPPLE_RUST_FORECAST. Default OFF => no cargo, +# no link, the C++ engine is byte-for-byte unchanged. +if(FREPPLE_RUST_FORECAST) + find_program(CARGO cargo REQUIRED) + set(RUST_FORECAST_DIR "${CMAKE_SOURCE_DIR}/rust/frepple-forecast") + set(RUST_FORECAST_LIB "${RUST_FORECAST_DIR}/target/release/libfrepple_forecast.a") + add_custom_command( + OUTPUT "${RUST_FORECAST_LIB}" + COMMAND "${CARGO}" build --release --manifest-path "${RUST_FORECAST_DIR}/Cargo.toml" + # rustc emits the staticlib without a symbol-table index on some toolchains + # (seen on aarch64), which GNU ld rejects ("archive has no index"). ranlib + # adds/refreshes the index; a no-op where one already exists (x86 CI). + COMMAND "${CMAKE_RANLIB}" "${RUST_FORECAST_LIB}" + WORKING_DIRECTORY "${CMAKE_SOURCE_DIR}" + COMMENT "Building Rust forecast staticlib (phase 7)" + VERBATIM + ) + add_custom_target(frepple_forecast_rust DEPENDS "${RUST_FORECAST_LIB}") + add_dependencies(forecast frepple_forecast_rust) + # Dispatch to Rust + match rustc's FP (no FMA contraction) for byte parity. + target_compile_definitions(forecast PRIVATE FREPPLE_RUST_FORECAST=1) + target_compile_options(forecast PRIVATE -ffp-contract=off) + target_link_libraries(forecast PUBLIC "${RUST_FORECAST_LIB}") +endif() + # Main shared library add_library(frepple SHARED dllmain.cpp diff --git a/src/forecast/forecast.h b/src/forecast/forecast.h index 8634d80192..bb7b200176 100644 --- a/src/forecast/forecast.h +++ b/src/forecast/forecast.h @@ -2621,9 +2621,11 @@ class ForecastSolver : public Solver { } Forecast_SmapeAlfa = t; - // Initialize the smape weight array + // Initialize the smape weight array. + // Must fill the full array (matching ForecastSolver::initialize), otherwise + // entries above the old hardcoded bound stay stale when this setter runs. weight[0] = 1.0; - for (int i = 0; i < 299; ++i) + for (int i = 0; i < MAXBUCKETS - 1; ++i) weight[i + 1] = weight[i] * Forecast_SmapeAlfa; } @@ -3041,6 +3043,17 @@ class ForecastSolver : public Solver { /* An array with weights for history buckets. */ static double weight[MAXBUCKETS]; + /* Bounds-safe accessor for the smape weight array. + * The number of history buckets can exceed MAXBUCKETS (e.g. weekly or daily + * buckets over the default 10-year horizon). Because the weights decay + * exponentially, weight[>= MAXBUCKETS] is numerically ~0, so clamping the + * index is behavior-preserving and avoids an out-of-bounds read. */ + static inline double smapeWeight(long idx) { + if (idx < 0) idx = 0; + if (idx >= MAXBUCKETS) idx = MAXBUCKETS - 1; + return weight[idx]; + } + /* Number of warmup periods. * These periods are used for the initialization of the algorithm * and don't count towards measuring the forecast error. diff --git a/src/forecast/timeseries.cpp b/src/forecast/timeseries.cpp index 103600f996..b768c34f1a 100644 --- a/src/forecast/timeseries.cpp +++ b/src/forecast/timeseries.cpp @@ -25,6 +25,31 @@ #include "forecast.h" +#if FREPPLE_RUST_FORECAST +#include +// C ABI of the Rust forecast staticlib (rust/frepple-forecast, linked by CMake +// when -DFREPPLE_RUST_FORECAST=ON). See tools/rust-pilot/frepple_forecast.h. +// Scalars via out-pointers; outlier indices into a caller buffer up to *_cap +// with the true count via *_len. Params are a small f64 array (order documented +// in the header). All return 0 on success. +extern "C" { +int frepple_moving_average(const double*, size_t, double*, double*, double*, + size_t*, size_t, size_t*, const double*, size_t); +int frepple_single_exponential(const double*, size_t, double*, double*, double*, + size_t*, size_t, size_t*, const double*, size_t); +int frepple_croston(const double*, size_t, double*, double*, double*, size_t*, + size_t, size_t*, const double*, size_t); +// DoubleExp returns the level/trend state too (out_constant, out_trend). +int frepple_double_exponential(const double*, size_t, double*, double*, double*, + size_t*, size_t, size_t*, const double*, size_t, + double*, double*); +// Seasonal returns period/force/s_i + the level/trend/cycle apply-state. +int frepple_seasonal(const double*, size_t, const double*, size_t, double*, + double*, double*, unsigned int*, int*, double*, size_t, + size_t*, double*, double*, unsigned int*); +} +#endif + namespace frepple { #define ACCURACY 0.01 @@ -295,6 +320,30 @@ ForecastSolver::Metrics ForecastSolver::MovingAverage::generateForecast( const Forecast* fcst, vector& bucketdata, short firstbckt, vector& timeseries, unsigned int count, ForecastSolver* solver) { +#if FREPPLE_RUST_FORECAST + // Phase 7: the numeric core runs in Rust (rust/frepple-forecast). timeseries + // holds `count` data points at [0..count-1] (a trailing 0 placeholder at + // [count]) - exactly what the Rust port + parity reference consume. The engine + // model mutation (outlier problems, applyForecast) stays in C++ below. + double params[4] = {static_cast(order), + ForecastSolver::Forecast_maxDeviation, + ForecastSolver::Forecast_SmapeAlfa, + static_cast(solver->getForecastSkip())}; + vector outl(count + 1); + size_t olen = 0; + double r_smape = 0.0, r_stddev = 0.0, r_avg = 0.0; + frepple_moving_average(timeseries.data(), count, &r_smape, &r_stddev, &r_avg, + outl.data(), outl.size(), &olen, params, 4); + avg = r_avg; + for (size_t k = 0; k < olen && k < outl.size(); ++k) + new ProblemOutlier( + bucketdata[outl[k] + firstbckt].getOrCreateForecastBucket(), this, true); + if (solver->getLogLevel() > 0) + logger << (fcst ? fcst->getName() : "") << ": moving average (rust) : " + << "smape " << r_smape << ", forecast " << avg + << ", standard deviation " << r_stddev << '\n'; + return ForecastSolver::Metrics(r_smape, r_stddev, false); +#else double error_smape, error_smape_weights; auto* clean_history = new double[count + 1]; @@ -352,8 +401,8 @@ ForecastSolver::Metrics ForecastSolver::MovingAverage::generateForecast( if (i >= solver->getForecastSkip() && i < count && fabs(avg + actual) > ROUNDING_ERROR) { error_smape += - fabs(avg - actual) / fabs(avg + actual) * weight[count - i]; - error_smape_weights += weight[count - i]; + fabs(avg - actual) / fabs(avg + actual) * smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); } } @@ -381,6 +430,7 @@ ForecastSolver::Metrics ForecastSolver::MovingAverage::generateForecast( << ", standard deviation " << standarddeviation << '\n'; delete[] clean_history; return ForecastSolver::Metrics(error_smape, standarddeviation, false); +#endif } void ForecastSolver::MovingAverage::applyForecast( @@ -421,6 +471,31 @@ ForecastSolver::Metrics ForecastSolver::SingleExponential::generateForecast( const Forecast* fcst, vector& bucketdata, short firstbckt, vector& timeseries, unsigned int count, ForecastSolver* solver) { +#if FREPPLE_RUST_FORECAST + // Phase 7: numeric core in Rust. Constant forecast (f_i) so the scalar ABI + // suffices; applyForecast writes f_i to every bucket as in the C++ path. + double params[7] = {initial_alfa, + min_alfa, + max_alfa, + ForecastSolver::Forecast_maxDeviation, + ForecastSolver::Forecast_SmapeAlfa, + static_cast(solver->getForecastSkip()), + static_cast(solver->getForecastIterations())}; + vector outl(count + 1); + size_t olen = 0; + double r_smape = 0.0, r_stddev = 0.0, r_fc = 0.0; + frepple_single_exponential(timeseries.data(), count, &r_smape, &r_stddev, + &r_fc, outl.data(), outl.size(), &olen, params, 7); + f_i = r_fc; + for (size_t k = 0; k < olen && k < outl.size(); ++k) + new ProblemOutlier( + bucketdata[outl[k] + firstbckt].getOrCreateForecastBucket(), this, true); + if (solver->getLogLevel() > 0) + logger << (fcst ? fcst->getName() : "") << ": single exponential (rust) : " + << "smape " << r_smape << ", forecast " << f_i + << ", standard deviation " << r_stddev << '\n'; + return ForecastSolver::Metrics(r_smape, r_stddev, false); +#else // Verify whether this is a valid forecast method. // - We need at least 5 buckets after the warmup period. if (count < solver->getForecastSkip() + 5) @@ -513,14 +588,14 @@ ForecastSolver::Metrics ForecastSolver::SingleExponential::generateForecast( true); } } - sum_12 += df_dalfa_i * (history_i - f_i) * weight[count - i]; - sum_11 += df_dalfa_i * df_dalfa_i * weight[count - i]; + sum_12 += df_dalfa_i * (history_i - f_i) * smapeWeight(count - i); + sum_11 += df_dalfa_i * df_dalfa_i * smapeWeight(count - i); if (i >= solver->getForecastSkip()) { - error += (f_i - history_i) * (f_i - history_i) * weight[count - i]; + error += (f_i - history_i) * (f_i - history_i) * smapeWeight(count - i); if (fabs(f_i + history_i) > ROUNDING_ERROR) { error_smape += - fabs(f_i - history_i) / (f_i + history_i) * weight[count - i]; - error_smape_weights += weight[count - i]; + fabs(f_i - history_i) / (f_i + history_i) * smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); } } } @@ -590,6 +665,7 @@ ForecastSolver::Metrics ForecastSolver::SingleExponential::generateForecast( << ", forecast " << f_i << ", standard deviation " << best_standarddeviation << '\n'; return ForecastSolver::Metrics(best_smape, best_standarddeviation, false); +#endif } void ForecastSolver::SingleExponential::applyForecast( @@ -634,6 +710,36 @@ ForecastSolver::Metrics ForecastSolver::DoubleExponential::generateForecast( const Forecast* fcst, vector& bucketdata, short firstbckt, vector& timeseries, unsigned int count, ForecastSolver* solver) { +#if FREPPLE_RUST_FORECAST + // Phase 7: numeric core in Rust. Level+trend: applyForecast extrapolates per + // bucket, so the Rust returns constant_i + trend_i (not just their sum). + double params[10] = {initial_alfa, + min_alfa, + max_alfa, + initial_gamma, + min_gamma, + max_gamma, + ForecastSolver::Forecast_maxDeviation, + ForecastSolver::Forecast_SmapeAlfa, + static_cast(solver->getForecastSkip()), + static_cast(solver->getForecastIterations())}; + vector outl(count + 1); + size_t olen = 0; + double r_smape = 0.0, r_stddev = 0.0, r_fc = 0.0, r_const = 0.0, r_trend = 0.0; + frepple_double_exponential(timeseries.data(), count, &r_smape, &r_stddev, + &r_fc, outl.data(), outl.size(), &olen, params, 10, + &r_const, &r_trend); + constant_i = r_const; + trend_i = r_trend; + for (size_t k = 0; k < olen && k < outl.size(); ++k) + new ProblemOutlier( + bucketdata[outl[k] + firstbckt].getOrCreateForecastBucket(), this, true); + if (solver->getLogLevel() > 0) + logger << (fcst ? fcst->getName() : "") << ": double exponential (rust) : " + << "smape " << r_smape << ", forecast " << (constant_i + trend_i) + << ", standard deviation " << r_stddev << '\n'; + return ForecastSolver::Metrics(r_smape, r_stddev, false); +#else // Verify whether this is a valid forecast method. // - We need at least 5 buckets after the warmup period. if (count < solver->getForecastSkip() + 5) @@ -778,24 +884,24 @@ ForecastSolver::Metrics ForecastSolver::DoubleExponential::generateForecast( (1 - gamma) * d_trend_d_gamma; d_forecast_d_alfa = d_constant_d_alfa + d_trend_d_alfa; d_forecast_d_gamma = d_constant_d_gamma + d_trend_d_gamma; - sum11 += weight[count - i] * d_forecast_d_alfa * d_forecast_d_alfa; - sum12 += weight[count - i] * d_forecast_d_alfa * d_forecast_d_gamma; - sum22 += weight[count - i] * d_forecast_d_gamma * d_forecast_d_gamma; - sum13 += weight[count - i] * d_forecast_d_alfa * + sum11 += smapeWeight(count - i) * d_forecast_d_alfa * d_forecast_d_alfa; + sum12 += smapeWeight(count - i) * d_forecast_d_alfa * d_forecast_d_gamma; + sum22 += smapeWeight(count - i) * d_forecast_d_gamma * d_forecast_d_gamma; + sum13 += smapeWeight(count - i) * d_forecast_d_alfa * (history_i - constant_i - trend_i); - sum23 += weight[count - i] * d_forecast_d_gamma * + sum23 += smapeWeight(count - i) * d_forecast_d_gamma * (history_i - constant_i - trend_i); if (i >= solver ->getForecastSkip()) // Don't measure during the warmup period { error += (constant_i + trend_i - history_i) * - (constant_i + trend_i - history_i) * weight[count - i]; + (constant_i + trend_i - history_i) * smapeWeight(count - i); if (fabs(constant_i + trend_i + history_i) > ROUNDING_ERROR) { error_smape += fabs(constant_i + trend_i - history_i) / fabs(constant_i + trend_i + history_i) * - weight[count - i]; - error_smape_weights += weight[count - i]; + smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); } } } @@ -889,6 +995,7 @@ ForecastSolver::Metrics ForecastSolver::DoubleExponential::generateForecast( << ", forecast " << (trend_i + constant_i) << ", standard deviation " << best_standarddeviation << '\n'; return ForecastSolver::Metrics(best_smape, best_standarddeviation, false); +#endif } void ForecastSolver::DoubleExponential::applyForecast( @@ -1006,6 +1113,47 @@ ForecastSolver::Metrics // this method (const Forecast* fcst, vector&, short, vector& timeseries, unsigned int count, ForecastSolver* solver) { +#if FREPPLE_RUST_FORECAST + // Phase 7: numeric core in Rust. Seasonal carries level/trend/cycle state into + // applyForecast (L_i/T_i/cycleindex/S_i), so the Rust returns it - byte-parity + // verified against the C++ reference (test_forecast_parity). No outlier + // detection in this method, so nothing model-mutating to recreate here. + // No seasonality (period 0) comes back as smape=DBL_MAX, so the dispatch loop + // never selects Seasonal and applyForecast is never reached with period 0. + double params[14] = {initial_alfa, + min_alfa, + max_alfa, + initial_beta, + min_beta, + max_beta, + gamma, + static_cast(min_period), + static_cast(max_period), + min_autocorrelation, + max_autocorrelation, + ForecastSolver::Forecast_SmapeAlfa, + static_cast(solver->getForecastSkip()), + static_cast(solver->getForecastIterations())}; + double s_i_buf[80]; + size_t s_i_len = 0; + double r_smape = 0.0, r_stddev = 0.0, r_fc = 0.0, r_l = 0.0, r_t = 0.0; + unsigned int r_period = 0, r_cycle = 0; + int r_force = 0; + frepple_seasonal(timeseries.data(), count, params, 14, &r_smape, &r_stddev, + &r_fc, &r_period, &r_force, s_i_buf, 80, &s_i_len, &r_l, &r_t, + &r_cycle); + period = static_cast(r_period); + L_i = r_l; + T_i = r_t; + cycleindex = r_cycle; + for (size_t i = 0; i < s_i_len && i < 80; ++i) S_i[i] = s_i_buf[i]; + if (solver->getLogLevel() > 0) + logger << (fcst ? fcst->getName() : "") << ": seasonal (rust) : " + << "smape " << r_smape << ", forecast " << r_fc + << ", standard deviation " << r_stddev << ", period " << period + << '\n'; + return ForecastSolver::Metrics(r_smape, r_stddev, r_force != 0); +#else // Check for seasonal cycles detectCycle(timeseries, count); @@ -1139,20 +1287,20 @@ ForecastSolver::Metrics d_forecast_d_beta = (d_L_d_beta + d_T_d_beta) * S_i[cycleindex] + (L_i + T_i) * d_S_d_beta[cycleindex]; forecast_i = (L_i + T_i) * S_i[cycleindex]; - sum11 += weight[count - i] * d_forecast_d_alfa * d_forecast_d_alfa; - sum12 += weight[count - i] * d_forecast_d_alfa * d_forecast_d_beta; - sum22 += weight[count - i] * d_forecast_d_beta * d_forecast_d_beta; - sum13 += weight[count - i] * d_forecast_d_alfa * (actual - forecast_i); - sum23 += weight[count - i] * d_forecast_d_beta * (actual - forecast_i); + sum11 += smapeWeight(count - i) * d_forecast_d_alfa * d_forecast_d_alfa; + sum12 += smapeWeight(count - i) * d_forecast_d_alfa * d_forecast_d_beta; + sum22 += smapeWeight(count - i) * d_forecast_d_beta * d_forecast_d_beta; + sum13 += smapeWeight(count - i) * d_forecast_d_alfa * (actual - forecast_i); + sum23 += smapeWeight(count - i) * d_forecast_d_beta * (actual - forecast_i); if (i >= solver->getForecastSkip()) // Don't measure during the warmup period { double fcst = (L_i + T_i) * S_i[cycleindex]; - error += (fcst - actual) * (fcst - actual) * weight[count - i]; + error += (fcst - actual) * (fcst - actual) * smapeWeight(count - i); if (fabs(fcst + actual) > ROUNDING_ERROR) { error_smape += - fabs(fcst - actual) / fabs(fcst + actual) * weight[count - i]; - error_smape_weights += weight[count - i]; + fabs(fcst - actual) / fabs(fcst + actual) * smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); standarddeviation += (fcst - actual) * (fcst - actual); } } @@ -1259,6 +1407,7 @@ ForecastSolver::Metrics // This enforces the use of the seasonal method. return ForecastSolver::Metrics(best_smape, best_standarddeviation, autocorrelation > max_autocorrelation); +#endif } void ForecastSolver::Seasonal::applyForecast( @@ -1308,6 +1457,31 @@ ForecastSolver::Metrics ForecastSolver::Croston::generateForecast( const Forecast* fcst, vector& bucketdata, short firstbckt, vector& timeseries, unsigned int count, ForecastSolver* solver) { +#if FREPPLE_RUST_FORECAST + // Phase 7: numeric core in Rust. Constant forecast (f_i); upper-only outliers + // are returned as indices and recreated as ProblemOutlier here. + double params[7] = {min_alfa, + max_alfa, + decay_rate, + ForecastSolver::Forecast_maxDeviation, + ForecastSolver::Forecast_SmapeAlfa, + static_cast(solver->getForecastSkip()), + static_cast(solver->getForecastIterations())}; + vector outl(count + 1); + size_t olen = 0; + double r_smape = 0.0, r_stddev = 0.0, r_fc = 0.0; + frepple_croston(timeseries.data(), count, &r_smape, &r_stddev, &r_fc, + outl.data(), outl.size(), &olen, params, 7); + f_i = r_fc; + for (size_t k = 0; k < olen && k < outl.size(); ++k) + new ProblemOutlier( + bucketdata[outl[k] + firstbckt].getOrCreateForecastBucket(), this, true); + if (solver->getLogLevel() > 0) + logger << (fcst ? fcst->getName() : "") << ": croston (rust) : " + << "smape " << r_smape << ", forecast " << f_i + << ", standard deviation " << r_stddev << '\n'; + return ForecastSolver::Metrics(r_smape, r_stddev, false); +#else // Count non-zero buckets double nonzero = 0.0; double totalsum = 0.0; @@ -1402,8 +1576,8 @@ ForecastSolver::Metrics ForecastSolver::Croston::generateForecast( { if (fabs(f_i + history_i) > ROUNDING_ERROR) { error_smape += fabs(f_i - history_i) / fabs(f_i + history_i) * - weight[count - i]; - error_smape_weights += weight[count - i]; + smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); } } } @@ -1460,6 +1634,7 @@ ForecastSolver::Metrics ForecastSolver::Croston::generateForecast( << ", forecast " << f_i << ", standard deviation " << best_standarddeviation << '\n'; return ForecastSolver::Metrics(best_smape, best_standarddeviation, false); +#endif } void ForecastSolver::Croston::applyForecast( diff --git a/src/model/buffer.cpp b/src/model/buffer.cpp index d69c12362c..cad5c28c27 100644 --- a/src/model/buffer.cpp +++ b/src/model/buffer.cpp @@ -452,9 +452,10 @@ void Buffer::setMaximum(double m) { // Update existing event static_cast(&*oo)->setMax(max_val); } else { - // Delete existing event - flowplans.erase(&(*oo)); - delete &(*(oo++)); + // Delete existing event (capture before erase; see setMaximumCalendar) + flowplanlist::Event* tmp = &*oo; + flowplans.erase(tmp); + delete tmp; } return; } @@ -474,12 +475,17 @@ void Buffer::setMaximumCalendar(Calendar* cal) { setChanged(); // Delete previous events. - for (auto oo = flowplans.begin(); oo != flowplans.end();) - if (oo->getEventType() == 4) { - flowplans.erase(&(*oo)); - delete &(*(oo++)); - } else - ++oo; + // Capture the node and advance the iterator BEFORE erasing: erase() nulls the + // node's next/prev, so "oo++" after the erase would read freed memory and jump + // to end() (stopping after the first deletion). Mirrors setMinimumCalendar. + for (auto oo = flowplans.begin(); oo != flowplans.end();) { + flowplanlist::Event* tmp = &*oo; + ++oo; + if (tmp->getEventType() == 4) { + flowplans.erase(tmp); + delete tmp; + } + } // Null pointer passed. Change back to time independent max. if (!cal) { @@ -590,8 +596,15 @@ void Buffer::followPegging(PeggingIterator& iter, FlowPlan* curflowplan, } if (f->getCumulativeProduced() > endQty) newqty -= f->getCumulativeProduced() - endQty; - OperationPlan* opplan = - dynamic_cast(&(*f))->getOperationPlan(); + // Skip non-flowplan timeline events (min/max/onhand have no + // operationplan; dynamic_cast returns null for them and + // dereferencing it segfaults on buffers with such events). + auto* fp = dynamic_cast(&(*f)); + if (!fp) { + ++f; + continue; + } + OperationPlan* opplan = fp->getOperationPlan(); OperationPlan* topopplan = opplan->getTopOwner(); if (topopplan->getOperation()->hasType() || (iter.getMaxLevel() > 0)) { @@ -630,8 +643,13 @@ void Buffer::followPegging(PeggingIterator& iter, FlowPlan* curflowplan, } if (f->getCumulativeProduced() > endQty) newqty -= f->getCumulativeProduced() - endQty; - OperationPlan* opplan = - dynamic_cast(&(*f))->getOperationPlan(); + // Skip non-flowplan timeline events (see CASE 1A above). + auto* fp = dynamic_cast(&(*f)); + if (!fp) { + --f; + continue; + } + OperationPlan* opplan = fp->getOperationPlan(); OperationPlan* topopplan = opplan->getTopOwner(); if (topopplan->getOperation()->hasType() || (iter.getMaxLevel() > 0)) { @@ -686,8 +704,13 @@ void Buffer::followPegging(PeggingIterator& iter, FlowPlan* curflowplan, } if (f->getCumulativeConsumed() > endQty) newqty -= f->getCumulativeConsumed() - endQty; - OperationPlan* opplan = - dynamic_cast(&(*f))->getOperationPlan(); + // Skip non-flowplan timeline events (see CASE 1A above). + auto* fp = dynamic_cast(&(*f)); + if (!fp) { + ++f; + continue; + } + OperationPlan* opplan = fp->getOperationPlan(); OperationPlan* topopplan = opplan->getTopOwner(); if (topopplan->getOperation()->hasType() || (iter.getMaxLevel() > 0)) { @@ -745,7 +768,13 @@ void Buffer::followPegging(PeggingIterator& iter, FlowPlan* curflowplan, startQty - (f->getCumulativeConsumed() + f->getQuantity()); if (f->getCumulativeConsumed() > endQty) newqty -= f->getCumulativeConsumed() - endQty; - auto opplan = dynamic_cast(&(*f))->getOperationPlan(); + // Skip non-flowplan timeline events (see CASE 1A above). + auto* fp = dynamic_cast(&(*f)); + if (!fp) { + --f; + continue; + } + auto opplan = fp->getOperationPlan(); OperationPlan* topopplan = opplan->getTopOwner(); if (topopplan->getOperation()->hasType() || (iter.getMaxLevel() > 0)) { diff --git a/src/model/calendar.cpp b/src/model/calendar.cpp index dfd0e4f927..6ac3ae0bb5 100644 --- a/src/model/calendar.cpp +++ b/src/model/calendar.cpp @@ -482,7 +482,13 @@ Calendar::EventIterator& Calendar::EventIterator::operator--() { curDate = Date::infinitePast; } else { curDate = cacheiter->first; - --cacheiter; + if (cacheiter == theCalendar->eventlist.begin()) + // Stepping back before the first event. Decrementing begin() is undefined + // behaviour for a std::map iterator (and trips AddressSanitizer); represent + // it explicitly as end(), which the extension logic below already expects. + cacheiter = theCalendar->eventlist.end(); + else + --cacheiter; if (cacheiter == theCalendar->eventlist.end()) { auto firstDate = theCalendar->eventlist.begin()->first; if (!theCalendar->eventlist.empty() && firstDate != Date::infinitePast) { diff --git a/src/model/operationdependency.cpp b/src/model/operationdependency.cpp index e3e0755869..1178bf641c 100644 --- a/src/model/operationdependency.cpp +++ b/src/model/operationdependency.cpp @@ -96,7 +96,10 @@ void OperationDependency::setOperation(Operation* o) { if (!oper && o) { oper = o; oper->addDependency(this); - blockedby->addDependency(this); + // blockedby may still be null here; addDependency() no-ops on an + // incomplete dependency anyway, so guard the call to avoid the UB + // null member-call (UBSan) - behaviour is identical either way. + if (blockedby) blockedby->addDependency(this); } else if (o && blockedby) { oper = o; oper->addDependency(this); @@ -119,7 +122,10 @@ void OperationDependency::setBlockedBy(Operation* o) { if (!blockedby && o) { blockedby = o; blockedby->addDependency(this); - oper->addDependency(this); + // oper may still be null here; addDependency() no-ops on an incomplete + // dependency anyway, so guard the call to avoid the UB null member-call + // (UBSan) - behaviour is identical either way. + if (oper) oper->addDependency(this); } else if (o && oper) { blockedby = o; blockedby->addDependency(this); diff --git a/src/model/operationplan.cpp b/src/model/operationplan.cpp index abbd0bc2c3..127d8c9e22 100644 --- a/src/model/operationplan.cpp +++ b/src/model/operationplan.cpp @@ -34,6 +34,7 @@ const MetaCategory* OperationPlan::metacategory; const MetaClass* OperationPlan::InterruptionIterator::metadata; const MetaCategory* OperationPlan::InterruptionIterator::metacategory; unsigned long OperationPlan::counterMin = 1; +atomic OperationPlan::sequenceCounter{0}; string OperationPlan::referenceMax; bool OperationPlan::propagatesetups = true; @@ -1043,9 +1044,10 @@ bool OperationPlan::operator<(const OperationPlan& a) const { // Use the reference (without auto-generating new ones) return getName() < a.getName(); - // Using a pointer comparison as tie breaker. This can give - // results that are not reproducible across platforms and runs. - return this < &a; + // Final tie-breaker: the monotonic creation sequence. Deterministic for a + // single-threaded solve and reproducible across platforms/runs, unlike the + // pointer comparison this replaced. + return sequence < a.sequence; } void OperationPlan::createFlowLoads( diff --git a/src/model/problem.cpp b/src/model/problem.cpp index 54c5046881..383188205f 100644 --- a/src/model/problem.cpp +++ b/src/model/problem.cpp @@ -355,9 +355,11 @@ HasProblems::EntityIterator::~EntityIterator() { } HasProblems::EntityIterator::EntityIterator(const EntityIterator& o) { - // Delete old iterator - this->~EntityIterator(); - // Populate new values + // Note: this is a constructor, so there is no previous iterator to delete. + // Calling ~EntityIterator() here would read the still-uninitialized 'type' + // and delete a garbage union pointer (undefined behaviour). The assignment + // operator below does call the destructor, which is correct there because + // 'type' is already initialized. type = o.type; if (type == 0) bufIter = new Buffer::iterator(*(o.bufIter)); diff --git a/src/solver/operatordelete.cpp b/src/solver/operatordelete.cpp index 6068a302d1..46c26bf268 100644 --- a/src/solver/operatordelete.cpp +++ b/src/solver/operatordelete.cpp @@ -75,8 +75,10 @@ PyObject* OperatorDelete::create(PyTypeObject*, PyObject*, PyObject* kwds) { }; } - // Return the object - Py_INCREF(s); + // Return the object. No Py_INCREF: as in the sibling SolverCreate::create + // factory, the freshly-created object already has a refcount of 1 and must + // stay collectable by the garbage collector. An extra INCREF would pin it + // forever (a leak of one OperatorDelete per construction). return static_cast(s); } catch (...) { PythonType::evalException(); diff --git a/src/solver/solveroperation.cpp b/src/solver/solveroperation.cpp index 279ad23713..34fcf9022a 100644 --- a/src/solver/solveroperation.cpp +++ b/src/solver/solveroperation.cpp @@ -47,7 +47,21 @@ void SolverCreate::checkOperationCapacity(OperationPlan* opplan, Date orig_q_date_max = data.state->q_date_max; bool first_iteration = true; bool delayed_reply = false; + // Snapshot the incoming cost and penalty. The capacity loop below may re-solve + // the same loadplans on every recheck pass (when moving the operationplan for + // one load forces re-checking the others). The resource solver only ever adds + // to a_cost/a_penalty, so without resetting to this baseline each pass the + // additions accumulate across passes - inflating the penalty/cost and + // corrupting cost-based alternate selection. This brackets them around the + // loop, the same way the alternate-search path does (see beforeCost/ + // beforePenalty further down). + double beforePenalty = data.state->a_penalty; + double beforeCost = data.state->a_cost; do { + // Each recheck pass recomputes capacity from scratch, so discard whatever + // the previous (now superseded) pass added and re-accumulate from baseline. + data.state->a_penalty = beforePenalty; + data.state->a_cost = beforeCost; if (getLogLevel() > 1) { if (!first_iteration) logger << indentlevel << " Rechecking capacity\n"; @@ -120,7 +134,8 @@ void SolverCreate::checkOperationCapacity(OperationPlan* opplan, data.constrainedPlanning && ((data.state->a_qty == 0.0 && data.state->a_date < orig_q_date_max) || recheck)); - // TODO doesn't this loop increment a_penalty incorrectly??? + // (Previously this loop double-counted a_penalty/a_cost across recheck passes; + // fixed by resetting them to beforePenalty/beforeCost at the top of each pass.) // Restore original flags data.logConstraints = backuplogconstraints; // restore the original value diff --git a/src/utils/json.cpp b/src/utils/json.cpp index 9bcfd6ea1f..4587e81ab0 100644 --- a/src/utils/json.cpp +++ b/src/utils/json.cpp @@ -802,7 +802,7 @@ long JSONData::getLong() const { case JSON_DOUBLE: if (data_double > LONG_MAX) return LONG_MAX; - else if (data_double > LONG_MIN) + else if (data_double < LONG_MIN) return LONG_MIN; else return static_cast(data_double); @@ -877,7 +877,7 @@ int JSONData::getInt() const { case JSON_DOUBLE: if (data_double > INT_MAX) return INT_MAX; - else if (data_double > INT_MIN) + else if (data_double < INT_MIN) return INT_MIN; else return static_cast(data_double); diff --git a/src/utils/python.cpp b/src/utils/python.cpp index 86067af010..5c1d3a6b5e 100644 --- a/src/utils/python.cpp +++ b/src/utils/python.cpp @@ -170,58 +170,64 @@ void PythonInterpreter::initialize() { // Capture global lock auto pythonstate = PyGILState_Ensure(); + try { + if (!init) { + // Create the logging function. + // In Python3 this also creates the frepple module, by calling the + // createModule callback. + PyRun_SimpleString( + "import frepple, sys\n" + "class redirect:\n" + "\tdef write(self,str):\n" + "\t\tfrepple.log(str)\n" + "\tdef flush(self):\n" + "\t\tpass\n" + "sys.stdout = redirect()\n" + "sys.stderr = redirect()"); + } - if (!init) { - // Create the logging function. - // In Python3 this also creates the frepple module, by calling the - // createModule callback. - PyRun_SimpleString( - "import frepple, sys\n" - "class redirect:\n" - "\tdef write(self,str):\n" - "\t\tfrepple.log(str)\n" - "\tdef flush(self):\n" - "\t\tpass\n" - "sys.stdout = redirect()\n" - "sys.stderr = redirect()"); - } - - if (!module) { - PyGILState_Release(pythonstate); - throw RuntimeException("Can't initialize Python interpreter"); + if (!module) + throw RuntimeException("Can't initialize Python interpreter"); + + // Make the datetime types available + PyDateTime_IMPORT; + + // Create python exception types + int nok = 0; + PythonLogicException = + PyErr_NewException((char*)"frepple.LogicException", nullptr, nullptr); + Py_IncRef(PythonLogicException); + nok += PyModule_AddObject(module, "LogicException", PythonLogicException); + PythonDataException = + PyErr_NewException((char*)"frepple.DataException", nullptr, nullptr); + Py_IncRef(PythonDataException); + nok += PyModule_AddObject(module, "DataException", PythonDataException); + PythonRuntimeException = + PyErr_NewException((char*)"frepple.RuntimeException", nullptr, nullptr); + Py_IncRef(PythonRuntimeException); + nok += PyModule_AddObject(module, "RuntimeException", PythonRuntimeException); + + // Add a string constant for the version + nok += PyModule_AddStringConstant(module, "version", + PACKAGE_VERSION "." PACKAGE_BRANCH); + + // Redirect the stderr and stdout streams of Python + registerGlobalMethod("log", python_log, METH_VARARGS, + "Prints a string to the frePPLe log file.", false); + + // A final check, performed while we still hold the GIL. + if (nok) throw RuntimeException("Can't initialize Python interpreter"); + } catch (...) { + // Mirror the success-path release exactly before propagating. In embedded + // mode (init == false) the main thread intentionally retains the GIL for + // the process lifetime, so it must NOT be released here. + if (init) PyGILState_Release(pythonstate); + throw; } - // Make the datetime types available - PyDateTime_IMPORT; - - // Create python exception types - int nok = 0; - PythonLogicException = - PyErr_NewException((char*)"frepple.LogicException", nullptr, nullptr); - Py_IncRef(PythonLogicException); - nok += PyModule_AddObject(module, "LogicException", PythonLogicException); - PythonDataException = - PyErr_NewException((char*)"frepple.DataException", nullptr, nullptr); - Py_IncRef(PythonDataException); - nok += PyModule_AddObject(module, "DataException", PythonDataException); - PythonRuntimeException = - PyErr_NewException((char*)"frepple.RuntimeException", nullptr, nullptr); - Py_IncRef(PythonRuntimeException); - nok += PyModule_AddObject(module, "RuntimeException", PythonRuntimeException); - - // Add a string constant for the version - nok += PyModule_AddStringConstant(module, "version", - PACKAGE_VERSION "." PACKAGE_BRANCH); - - // Redirect the stderr and stdout streams of Python - registerGlobalMethod("log", python_log, METH_VARARGS, - "Prints a string to the frePPLe log file.", false); - - // Release the lock + // Release the lock (success path). In embedded mode the GIL is intentionally + // kept by the main thread. if (init) PyGILState_Release(pythonstate); - - // A final check... - if (nok) throw RuntimeException("Can't initialize Python interpreter"); } void PythonInterpreter::execute(const char* cmd) { @@ -458,7 +464,8 @@ Duration PythonData::getDuration() const { Py_DECREF(utf8_string); return t; } else if (obj && PyDelta_Check(obj)) { - PythonData r = PyObject_CallMethod(obj, "total_seconds", nullptr); + // PyObject_CallMethod returns a new reference; adopt it without leaking. + PythonData r = PythonData::fromOwned(PyObject_CallMethod(obj, "total_seconds", nullptr)); return r.getDouble(); } else if (PyLong_Check(obj)) { long result = PyLong_AsLong(obj); @@ -1032,8 +1039,11 @@ PythonData PythonFunction::call() const { << (func ? PyEval_GetFuncName(func) : "nullptr") << "'\n"; if (PyErr_Occurred()) PyErr_PrintEx(0); } + // Adopt the owned reference under the GIL (PyObject_CallFunction returns a + // new reference; wrapping it directly in PythonData would leak it). + PythonData ret = PythonData::fromOwned(result); PyGILState_Release(pythonstate); - return PythonData(result); + return ret; } PythonData PythonFunction::call(const PyObject* p) const { @@ -1045,8 +1055,11 @@ PythonData PythonFunction::call(const PyObject* p) const { << (func ? PyEval_GetFuncName(func) : "nullptr") << "'\n"; if (PyErr_Occurred()) PyErr_PrintEx(0); } + // Adopt the owned reference under the GIL (PyObject_CallFunction returns a + // new reference; wrapping it directly in PythonData would leak it). + PythonData ret = PythonData::fromOwned(result); PyGILState_Release(pythonstate); - return PythonData(result); + return ret; } PythonData PythonFunction::call(const PyObject* p, const PyObject* q) const { @@ -1058,8 +1071,11 @@ PythonData PythonFunction::call(const PyObject* p, const PyObject* q) const { << (func ? PyEval_GetFuncName(func) : "nullptr") << "'\n"; if (PyErr_Occurred()) PyErr_PrintEx(0); } + // Adopt the owned reference under the GIL (PyObject_CallFunction returns a + // new reference; wrapping it directly in PythonData would leak it). + PythonData ret = PythonData::fromOwned(result); PyGILState_Release(pythonstate); - return PythonData(result); + return ret; } extern "C" PyObject* getattro_handler(PyObject* self, PyObject* name) { diff --git a/test/forecast_11/forecast_11.xml b/test/forecast_11/forecast_11.xml new file mode 100644 index 0000000000..3abf16e696 --- /dev/null +++ b/test/forecast_11/forecast_11.xml @@ -0,0 +1,2640 @@ + + + Forecast long-history test (weight[] bounds) + + Regression scenario for the weight[] out-of-bounds read: a weekly + forecast with >500 history buckets forces the SMAPE weight index + (count - i) above MAXBUCKETS. Under an AddressSanitizer build the + pre-fix binary reports a global-buffer-overflow on weight; the + smapeWeight() clamp makes it clean. No golden .expect on purpose: + the test passes iff frepple processes the model without error. + + 2022-12-26T00:00:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2013-01-07T00:00:00 + 100 + + + 2013-01-14T00:00:00 + 105 + + + 2013-01-21T00:00:00 + 110 + + + 2013-01-28T00:00:00 + 115 + + + 2013-02-04T00:00:00 + 120 + + + 2013-02-11T00:00:00 + 125 + + + 2013-02-18T00:00:00 + 130 + + + 2013-02-25T00:00:00 + 135 + + + 2013-03-04T00:00:00 + 140 + + + 2013-03-11T00:00:00 + 145 + + + 2013-03-18T00:00:00 + 150 + + + 2013-03-25T00:00:00 + 155 + + + 2013-04-01T00:00:00 + 160 + + + 2013-04-08T00:00:00 + 100 + + + 2013-04-15T00:00:00 + 105 + + + 2013-04-22T00:00:00 + 110 + + + 2013-04-29T00:00:00 + 115 + + + 2013-05-06T00:00:00 + 120 + + + 2013-05-13T00:00:00 + 125 + + + 2013-05-20T00:00:00 + 130 + + + 2013-05-27T00:00:00 + 135 + + + 2013-06-03T00:00:00 + 140 + + + 2013-06-10T00:00:00 + 145 + + + 2013-06-17T00:00:00 + 150 + + + 2013-06-24T00:00:00 + 155 + + + 2013-07-01T00:00:00 + 160 + + + 2013-07-08T00:00:00 + 100 + + + 2013-07-15T00:00:00 + 105 + + + 2013-07-22T00:00:00 + 110 + + + 2013-07-29T00:00:00 + 115 + + + 2013-08-05T00:00:00 + 120 + + + 2013-08-12T00:00:00 + 125 + + + 2013-08-19T00:00:00 + 130 + + + 2013-08-26T00:00:00 + 135 + + + 2013-09-02T00:00:00 + 140 + + + 2013-09-09T00:00:00 + 145 + + + 2013-09-16T00:00:00 + 150 + + + 2013-09-23T00:00:00 + 155 + + + 2013-09-30T00:00:00 + 160 + + + 2013-10-07T00:00:00 + 100 + + + 2013-10-14T00:00:00 + 105 + + + 2013-10-21T00:00:00 + 110 + + + 2013-10-28T00:00:00 + 115 + + + 2013-11-04T00:00:00 + 120 + + + 2013-11-11T00:00:00 + 125 + + + 2013-11-18T00:00:00 + 130 + + + 2013-11-25T00:00:00 + 135 + + + 2013-12-02T00:00:00 + 140 + + + 2013-12-09T00:00:00 + 145 + + + 2013-12-16T00:00:00 + 150 + + + 2013-12-23T00:00:00 + 155 + + + 2013-12-30T00:00:00 + 160 + + + 2014-01-06T00:00:00 + 100 + + + 2014-01-13T00:00:00 + 105 + + + 2014-01-20T00:00:00 + 110 + + + 2014-01-27T00:00:00 + 115 + + + 2014-02-03T00:00:00 + 120 + + + 2014-02-10T00:00:00 + 125 + + + 2014-02-17T00:00:00 + 130 + + + 2014-02-24T00:00:00 + 135 + + + 2014-03-03T00:00:00 + 140 + + + 2014-03-10T00:00:00 + 145 + + + 2014-03-17T00:00:00 + 150 + + + 2014-03-24T00:00:00 + 155 + + + 2014-03-31T00:00:00 + 160 + + + 2014-04-07T00:00:00 + 100 + + + 2014-04-14T00:00:00 + 105 + + + 2014-04-21T00:00:00 + 110 + + + 2014-04-28T00:00:00 + 115 + + + 2014-05-05T00:00:00 + 120 + + + 2014-05-12T00:00:00 + 125 + + + 2014-05-19T00:00:00 + 130 + + + 2014-05-26T00:00:00 + 135 + + + 2014-06-02T00:00:00 + 140 + + + 2014-06-09T00:00:00 + 145 + + + 2014-06-16T00:00:00 + 150 + + + 2014-06-23T00:00:00 + 155 + + + 2014-06-30T00:00:00 + 160 + + + 2014-07-07T00:00:00 + 100 + + + 2014-07-14T00:00:00 + 105 + + + 2014-07-21T00:00:00 + 110 + + + 2014-07-28T00:00:00 + 115 + + + 2014-08-04T00:00:00 + 120 + + + 2014-08-11T00:00:00 + 125 + + + 2014-08-18T00:00:00 + 130 + + + 2014-08-25T00:00:00 + 135 + + + 2014-09-01T00:00:00 + 140 + + + 2014-09-08T00:00:00 + 145 + + + 2014-09-15T00:00:00 + 150 + + + 2014-09-22T00:00:00 + 155 + + + 2014-09-29T00:00:00 + 160 + + + 2014-10-06T00:00:00 + 100 + + + 2014-10-13T00:00:00 + 105 + + + 2014-10-20T00:00:00 + 110 + + + 2014-10-27T00:00:00 + 115 + + + 2014-11-03T00:00:00 + 120 + + + 2014-11-10T00:00:00 + 125 + + + 2014-11-17T00:00:00 + 130 + + + 2014-11-24T00:00:00 + 135 + + + 2014-12-01T00:00:00 + 140 + + + 2014-12-08T00:00:00 + 145 + + + 2014-12-15T00:00:00 + 150 + + + 2014-12-22T00:00:00 + 155 + + + 2014-12-29T00:00:00 + 160 + + + 2015-01-05T00:00:00 + 100 + + + 2015-01-12T00:00:00 + 105 + + + 2015-01-19T00:00:00 + 110 + + + 2015-01-26T00:00:00 + 115 + + + 2015-02-02T00:00:00 + 120 + + + 2015-02-09T00:00:00 + 125 + + + 2015-02-16T00:00:00 + 130 + + + 2015-02-23T00:00:00 + 135 + + + 2015-03-02T00:00:00 + 140 + + + 2015-03-09T00:00:00 + 145 + + + 2015-03-16T00:00:00 + 150 + + + 2015-03-23T00:00:00 + 155 + + + 2015-03-30T00:00:00 + 160 + + + 2015-04-06T00:00:00 + 100 + + + 2015-04-13T00:00:00 + 105 + + + 2015-04-20T00:00:00 + 110 + + + 2015-04-27T00:00:00 + 115 + + + 2015-05-04T00:00:00 + 120 + + + 2015-05-11T00:00:00 + 125 + + + 2015-05-18T00:00:00 + 130 + + + 2015-05-25T00:00:00 + 135 + + + 2015-06-01T00:00:00 + 140 + + + 2015-06-08T00:00:00 + 145 + + + 2015-06-15T00:00:00 + 150 + + + 2015-06-22T00:00:00 + 155 + + + 2015-06-29T00:00:00 + 160 + + + 2015-07-06T00:00:00 + 100 + + + 2015-07-13T00:00:00 + 105 + + + 2015-07-20T00:00:00 + 110 + + + 2015-07-27T00:00:00 + 115 + + + 2015-08-03T00:00:00 + 120 + + + 2015-08-10T00:00:00 + 125 + + + 2015-08-17T00:00:00 + 130 + + + 2015-08-24T00:00:00 + 135 + + + 2015-08-31T00:00:00 + 140 + + + 2015-09-07T00:00:00 + 145 + + + 2015-09-14T00:00:00 + 150 + + + 2015-09-21T00:00:00 + 155 + + + 2015-09-28T00:00:00 + 160 + + + 2015-10-05T00:00:00 + 100 + + + 2015-10-12T00:00:00 + 105 + + + 2015-10-19T00:00:00 + 110 + + + 2015-10-26T00:00:00 + 115 + + + 2015-11-02T00:00:00 + 120 + + + 2015-11-09T00:00:00 + 125 + + + 2015-11-16T00:00:00 + 130 + + + 2015-11-23T00:00:00 + 135 + + + 2015-11-30T00:00:00 + 140 + + + 2015-12-07T00:00:00 + 145 + + + 2015-12-14T00:00:00 + 150 + + + 2015-12-21T00:00:00 + 155 + + + 2015-12-28T00:00:00 + 160 + + + 2016-01-04T00:00:00 + 100 + + + 2016-01-11T00:00:00 + 105 + + + 2016-01-18T00:00:00 + 110 + + + 2016-01-25T00:00:00 + 115 + + + 2016-02-01T00:00:00 + 120 + + + 2016-02-08T00:00:00 + 125 + + + 2016-02-15T00:00:00 + 130 + + + 2016-02-22T00:00:00 + 135 + + + 2016-02-29T00:00:00 + 140 + + + 2016-03-07T00:00:00 + 145 + + + 2016-03-14T00:00:00 + 150 + + + 2016-03-21T00:00:00 + 155 + + + 2016-03-28T00:00:00 + 160 + + + 2016-04-04T00:00:00 + 100 + + + 2016-04-11T00:00:00 + 105 + + + 2016-04-18T00:00:00 + 110 + + + 2016-04-25T00:00:00 + 115 + + + 2016-05-02T00:00:00 + 120 + + + 2016-05-09T00:00:00 + 125 + + + 2016-05-16T00:00:00 + 130 + + + 2016-05-23T00:00:00 + 135 + + + 2016-05-30T00:00:00 + 140 + + + 2016-06-06T00:00:00 + 145 + + + 2016-06-13T00:00:00 + 150 + + + 2016-06-20T00:00:00 + 155 + + + 2016-06-27T00:00:00 + 160 + + + 2016-07-04T00:00:00 + 100 + + + 2016-07-11T00:00:00 + 105 + + + 2016-07-18T00:00:00 + 110 + + + 2016-07-25T00:00:00 + 115 + + + 2016-08-01T00:00:00 + 120 + + + 2016-08-08T00:00:00 + 125 + + + 2016-08-15T00:00:00 + 130 + + + 2016-08-22T00:00:00 + 135 + + + 2016-08-29T00:00:00 + 140 + + + 2016-09-05T00:00:00 + 145 + + + 2016-09-12T00:00:00 + 150 + + + 2016-09-19T00:00:00 + 155 + + + 2016-09-26T00:00:00 + 160 + + + 2016-10-03T00:00:00 + 100 + + + 2016-10-10T00:00:00 + 105 + + + 2016-10-17T00:00:00 + 110 + + + 2016-10-24T00:00:00 + 115 + + + 2016-10-31T00:00:00 + 120 + + + 2016-11-07T00:00:00 + 125 + + + 2016-11-14T00:00:00 + 130 + + + 2016-11-21T00:00:00 + 135 + + + 2016-11-28T00:00:00 + 140 + + + 2016-12-05T00:00:00 + 145 + + + 2016-12-12T00:00:00 + 150 + + + 2016-12-19T00:00:00 + 155 + + + 2016-12-26T00:00:00 + 160 + + + 2017-01-02T00:00:00 + 100 + + + 2017-01-09T00:00:00 + 105 + + + 2017-01-16T00:00:00 + 110 + + + 2017-01-23T00:00:00 + 115 + + + 2017-01-30T00:00:00 + 120 + + + 2017-02-06T00:00:00 + 125 + + + 2017-02-13T00:00:00 + 130 + + + 2017-02-20T00:00:00 + 135 + + + 2017-02-27T00:00:00 + 140 + + + 2017-03-06T00:00:00 + 145 + + + 2017-03-13T00:00:00 + 150 + + + 2017-03-20T00:00:00 + 155 + + + 2017-03-27T00:00:00 + 160 + + + 2017-04-03T00:00:00 + 100 + + + 2017-04-10T00:00:00 + 105 + + + 2017-04-17T00:00:00 + 110 + + + 2017-04-24T00:00:00 + 115 + + + 2017-05-01T00:00:00 + 120 + + + 2017-05-08T00:00:00 + 125 + + + 2017-05-15T00:00:00 + 130 + + + 2017-05-22T00:00:00 + 135 + + + 2017-05-29T00:00:00 + 140 + + + 2017-06-05T00:00:00 + 145 + + + 2017-06-12T00:00:00 + 150 + + + 2017-06-19T00:00:00 + 155 + + + 2017-06-26T00:00:00 + 160 + + + 2017-07-03T00:00:00 + 100 + + + 2017-07-10T00:00:00 + 105 + + + 2017-07-17T00:00:00 + 110 + + + 2017-07-24T00:00:00 + 115 + + + 2017-07-31T00:00:00 + 120 + + + 2017-08-07T00:00:00 + 125 + + + 2017-08-14T00:00:00 + 130 + + + 2017-08-21T00:00:00 + 135 + + + 2017-08-28T00:00:00 + 140 + + + 2017-09-04T00:00:00 + 145 + + + 2017-09-11T00:00:00 + 150 + + + 2017-09-18T00:00:00 + 155 + + + 2017-09-25T00:00:00 + 160 + + + 2017-10-02T00:00:00 + 100 + + + 2017-10-09T00:00:00 + 105 + + + 2017-10-16T00:00:00 + 110 + + + 2017-10-23T00:00:00 + 115 + + + 2017-10-30T00:00:00 + 120 + + + 2017-11-06T00:00:00 + 125 + + + 2017-11-13T00:00:00 + 130 + + + 2017-11-20T00:00:00 + 135 + + + 2017-11-27T00:00:00 + 140 + + + 2017-12-04T00:00:00 + 145 + + + 2017-12-11T00:00:00 + 150 + + + 2017-12-18T00:00:00 + 155 + + + 2017-12-25T00:00:00 + 160 + + + 2018-01-01T00:00:00 + 100 + + + 2018-01-08T00:00:00 + 105 + + + 2018-01-15T00:00:00 + 110 + + + 2018-01-22T00:00:00 + 115 + + + 2018-01-29T00:00:00 + 120 + + + 2018-02-05T00:00:00 + 125 + + + 2018-02-12T00:00:00 + 130 + + + 2018-02-19T00:00:00 + 135 + + + 2018-02-26T00:00:00 + 140 + + + 2018-03-05T00:00:00 + 145 + + + 2018-03-12T00:00:00 + 150 + + + 2018-03-19T00:00:00 + 155 + + + 2018-03-26T00:00:00 + 160 + + + 2018-04-02T00:00:00 + 100 + + + 2018-04-09T00:00:00 + 105 + + + 2018-04-16T00:00:00 + 110 + + + 2018-04-23T00:00:00 + 115 + + + 2018-04-30T00:00:00 + 120 + + + 2018-05-07T00:00:00 + 125 + + + 2018-05-14T00:00:00 + 130 + + + 2018-05-21T00:00:00 + 135 + + + 2018-05-28T00:00:00 + 140 + + + 2018-06-04T00:00:00 + 145 + + + 2018-06-11T00:00:00 + 150 + + + 2018-06-18T00:00:00 + 155 + + + 2018-06-25T00:00:00 + 160 + + + 2018-07-02T00:00:00 + 100 + + + 2018-07-09T00:00:00 + 105 + + + 2018-07-16T00:00:00 + 110 + + + 2018-07-23T00:00:00 + 115 + + + 2018-07-30T00:00:00 + 120 + + + 2018-08-06T00:00:00 + 125 + + + 2018-08-13T00:00:00 + 130 + + + 2018-08-20T00:00:00 + 135 + + + 2018-08-27T00:00:00 + 140 + + + 2018-09-03T00:00:00 + 145 + + + 2018-09-10T00:00:00 + 150 + + + 2018-09-17T00:00:00 + 155 + + + 2018-09-24T00:00:00 + 160 + + + 2018-10-01T00:00:00 + 100 + + + 2018-10-08T00:00:00 + 105 + + + 2018-10-15T00:00:00 + 110 + + + 2018-10-22T00:00:00 + 115 + + + 2018-10-29T00:00:00 + 120 + + + 2018-11-05T00:00:00 + 125 + + + 2018-11-12T00:00:00 + 130 + + + 2018-11-19T00:00:00 + 135 + + + 2018-11-26T00:00:00 + 140 + + + 2018-12-03T00:00:00 + 145 + + + 2018-12-10T00:00:00 + 150 + + + 2018-12-17T00:00:00 + 155 + + + 2018-12-24T00:00:00 + 160 + + + 2018-12-31T00:00:00 + 100 + + + 2019-01-07T00:00:00 + 105 + + + 2019-01-14T00:00:00 + 110 + + + 2019-01-21T00:00:00 + 115 + + + 2019-01-28T00:00:00 + 120 + + + 2019-02-04T00:00:00 + 125 + + + 2019-02-11T00:00:00 + 130 + + + 2019-02-18T00:00:00 + 135 + + + 2019-02-25T00:00:00 + 140 + + + 2019-03-04T00:00:00 + 145 + + + 2019-03-11T00:00:00 + 150 + + + 2019-03-18T00:00:00 + 155 + + + 2019-03-25T00:00:00 + 160 + + + 2019-04-01T00:00:00 + 100 + + + 2019-04-08T00:00:00 + 105 + + + 2019-04-15T00:00:00 + 110 + + + 2019-04-22T00:00:00 + 115 + + + 2019-04-29T00:00:00 + 120 + + + 2019-05-06T00:00:00 + 125 + + + 2019-05-13T00:00:00 + 130 + + + 2019-05-20T00:00:00 + 135 + + + 2019-05-27T00:00:00 + 140 + + + 2019-06-03T00:00:00 + 145 + + + 2019-06-10T00:00:00 + 150 + + + 2019-06-17T00:00:00 + 155 + + + 2019-06-24T00:00:00 + 160 + + + 2019-07-01T00:00:00 + 100 + + + 2019-07-08T00:00:00 + 105 + + + 2019-07-15T00:00:00 + 110 + + + 2019-07-22T00:00:00 + 115 + + + 2019-07-29T00:00:00 + 120 + + + 2019-08-05T00:00:00 + 125 + + + 2019-08-12T00:00:00 + 130 + + + 2019-08-19T00:00:00 + 135 + + + 2019-08-26T00:00:00 + 140 + + + 2019-09-02T00:00:00 + 145 + + + 2019-09-09T00:00:00 + 150 + + + 2019-09-16T00:00:00 + 155 + + + 2019-09-23T00:00:00 + 160 + + + 2019-09-30T00:00:00 + 100 + + + 2019-10-07T00:00:00 + 105 + + + 2019-10-14T00:00:00 + 110 + + + 2019-10-21T00:00:00 + 115 + + + 2019-10-28T00:00:00 + 120 + + + 2019-11-04T00:00:00 + 125 + + + 2019-11-11T00:00:00 + 130 + + + 2019-11-18T00:00:00 + 135 + + + 2019-11-25T00:00:00 + 140 + + + 2019-12-02T00:00:00 + 145 + + + 2019-12-09T00:00:00 + 150 + + + 2019-12-16T00:00:00 + 155 + + + 2019-12-23T00:00:00 + 160 + + + 2019-12-30T00:00:00 + 100 + + + 2020-01-06T00:00:00 + 105 + + + 2020-01-13T00:00:00 + 110 + + + 2020-01-20T00:00:00 + 115 + + + 2020-01-27T00:00:00 + 120 + + + 2020-02-03T00:00:00 + 125 + + + 2020-02-10T00:00:00 + 130 + + + 2020-02-17T00:00:00 + 135 + + + 2020-02-24T00:00:00 + 140 + + + 2020-03-02T00:00:00 + 145 + + + 2020-03-09T00:00:00 + 150 + + + 2020-03-16T00:00:00 + 155 + + + 2020-03-23T00:00:00 + 160 + + + 2020-03-30T00:00:00 + 100 + + + 2020-04-06T00:00:00 + 105 + + + 2020-04-13T00:00:00 + 110 + + + 2020-04-20T00:00:00 + 115 + + + 2020-04-27T00:00:00 + 120 + + + 2020-05-04T00:00:00 + 125 + + + 2020-05-11T00:00:00 + 130 + + + 2020-05-18T00:00:00 + 135 + + + 2020-05-25T00:00:00 + 140 + + + 2020-06-01T00:00:00 + 145 + + + 2020-06-08T00:00:00 + 150 + + + 2020-06-15T00:00:00 + 155 + + + 2020-06-22T00:00:00 + 160 + + + 2020-06-29T00:00:00 + 100 + + + 2020-07-06T00:00:00 + 105 + + + 2020-07-13T00:00:00 + 110 + + + 2020-07-20T00:00:00 + 115 + + + 2020-07-27T00:00:00 + 120 + + + 2020-08-03T00:00:00 + 125 + + + 2020-08-10T00:00:00 + 130 + + + 2020-08-17T00:00:00 + 135 + + + 2020-08-24T00:00:00 + 140 + + + 2020-08-31T00:00:00 + 145 + + + 2020-09-07T00:00:00 + 150 + + + 2020-09-14T00:00:00 + 155 + + + 2020-09-21T00:00:00 + 160 + + + 2020-09-28T00:00:00 + 100 + + + 2020-10-05T00:00:00 + 105 + + + 2020-10-12T00:00:00 + 110 + + + 2020-10-19T00:00:00 + 115 + + + 2020-10-26T00:00:00 + 120 + + + 2020-11-02T00:00:00 + 125 + + + 2020-11-09T00:00:00 + 130 + + + 2020-11-16T00:00:00 + 135 + + + 2020-11-23T00:00:00 + 140 + + + 2020-11-30T00:00:00 + 145 + + + 2020-12-07T00:00:00 + 150 + + + 2020-12-14T00:00:00 + 155 + + + 2020-12-21T00:00:00 + 160 + + + 2020-12-28T00:00:00 + 100 + + + 2021-01-04T00:00:00 + 105 + + + 2021-01-11T00:00:00 + 110 + + + 2021-01-18T00:00:00 + 115 + + + 2021-01-25T00:00:00 + 120 + + + 2021-02-01T00:00:00 + 125 + + + 2021-02-08T00:00:00 + 130 + + + 2021-02-15T00:00:00 + 135 + + + 2021-02-22T00:00:00 + 140 + + + 2021-03-01T00:00:00 + 145 + + + 2021-03-08T00:00:00 + 150 + + + 2021-03-15T00:00:00 + 155 + + + 2021-03-22T00:00:00 + 160 + + + 2021-03-29T00:00:00 + 100 + + + 2021-04-05T00:00:00 + 105 + + + 2021-04-12T00:00:00 + 110 + + + 2021-04-19T00:00:00 + 115 + + + 2021-04-26T00:00:00 + 120 + + + 2021-05-03T00:00:00 + 125 + + + 2021-05-10T00:00:00 + 130 + + + 2021-05-17T00:00:00 + 135 + + + 2021-05-24T00:00:00 + 140 + + + 2021-05-31T00:00:00 + 145 + + + 2021-06-07T00:00:00 + 150 + + + 2021-06-14T00:00:00 + 155 + + + 2021-06-21T00:00:00 + 160 + + + 2021-06-28T00:00:00 + 100 + + + 2021-07-05T00:00:00 + 105 + + + 2021-07-12T00:00:00 + 110 + + + 2021-07-19T00:00:00 + 115 + + + 2021-07-26T00:00:00 + 120 + + + 2021-08-02T00:00:00 + 125 + + + 2021-08-09T00:00:00 + 130 + + + 2021-08-16T00:00:00 + 135 + + + 2021-08-23T00:00:00 + 140 + + + 2021-08-30T00:00:00 + 145 + + + 2021-09-06T00:00:00 + 150 + + + 2021-09-13T00:00:00 + 155 + + + 2021-09-20T00:00:00 + 160 + + + 2021-09-27T00:00:00 + 100 + + + 2021-10-04T00:00:00 + 105 + + + 2021-10-11T00:00:00 + 110 + + + 2021-10-18T00:00:00 + 115 + + + 2021-10-25T00:00:00 + 120 + + + 2021-11-01T00:00:00 + 125 + + + 2021-11-08T00:00:00 + 130 + + + 2021-11-15T00:00:00 + 135 + + + 2021-11-22T00:00:00 + 140 + + + 2021-11-29T00:00:00 + 145 + + + 2021-12-06T00:00:00 + 150 + + + 2021-12-13T00:00:00 + 155 + + + 2021-12-20T00:00:00 + 160 + + + 2021-12-27T00:00:00 + 100 + + + 2022-01-03T00:00:00 + 105 + + + 2022-01-10T00:00:00 + 110 + + + 2022-01-17T00:00:00 + 115 + + + 2022-01-24T00:00:00 + 120 + + + 2022-01-31T00:00:00 + 125 + + + 2022-02-07T00:00:00 + 130 + + + 2022-02-14T00:00:00 + 135 + + + 2022-02-21T00:00:00 + 140 + + + 2022-02-28T00:00:00 + 145 + + + 2022-03-07T00:00:00 + 150 + + + 2022-03-14T00:00:00 + 155 + + + 2022-03-21T00:00:00 + 160 + + + 2022-03-28T00:00:00 + 100 + + + 2022-04-04T00:00:00 + 105 + + + 2022-04-11T00:00:00 + 110 + + + 2022-04-18T00:00:00 + 115 + + + 2022-04-25T00:00:00 + 120 + + + 2022-05-02T00:00:00 + 125 + + + 2022-05-09T00:00:00 + 130 + + + 2022-05-16T00:00:00 + 135 + + + 2022-05-23T00:00:00 + 140 + + + 2022-05-30T00:00:00 + 145 + + + 2022-06-06T00:00:00 + 150 + + + 2022-06-13T00:00:00 + 155 + + + 2022-06-20T00:00:00 + 160 + + + 2022-06-27T00:00:00 + 100 + + + 2022-07-04T00:00:00 + 105 + + + 2022-07-11T00:00:00 + 110 + + + 2022-07-18T00:00:00 + 115 + + + 2022-07-25T00:00:00 + 120 + + + 2022-08-01T00:00:00 + 125 + + + 2022-08-08T00:00:00 + 130 + + + 2022-08-15T00:00:00 + 135 + + + 2022-08-22T00:00:00 + 140 + + + 2022-08-29T00:00:00 + 145 + + + 2022-09-05T00:00:00 + 150 + + + 2022-09-12T00:00:00 + 155 + + + 2022-09-19T00:00:00 + 160 + + + 2022-09-26T00:00:00 + 100 + + + 2022-10-03T00:00:00 + 105 + + + 2022-10-10T00:00:00 + 110 + + + 2022-10-17T00:00:00 + 115 + + + 2022-10-24T00:00:00 + 120 + + + 2022-10-31T00:00:00 + 125 + + + 2022-11-07T00:00:00 + 130 + + + 2022-11-14T00:00:00 + 135 + + + 2022-11-21T00:00:00 + 140 + + + 2022-11-28T00:00:00 + 145 + + + 2022-12-05T00:00:00 + 150 + + + 2022-12-12T00:00:00 + 155 + + + 2022-12-19T00:00:00 + 160 + + + + + + + diff --git a/test/invariants.py b/test/invariants.py new file mode 100644 index 0000000000..0018189c37 --- /dev/null +++ b/test/invariants.py @@ -0,0 +1,82 @@ +# Structural-invariant checks for a solved frePPLe plan (Engine track E2). +# +# `check()` inspects the CURRENT in-memory plan and returns a list of violation +# strings (empty == all invariants hold). A test script (e.g. test/invariants_sweep) +# solves, calls check(), and raises on any violation - so the assertion is a boolean pass/fail, +# independent of output ORDER (which is environment-dependent for some models; see +# tools/modernization/engine-review-E1.md H4). That makes it a true "rewrite oracle" +# the byte-exact golden diff can't be: it catches capacity/material/temporal +# violations the diff would miss, and is robust across build environments. +# +# Deliberately CONSERVATIVE. A plan has many *legitimate* outcomes - late or short +# deliveries (lead-time/capacity), over-deliveries (lot sizing), WIP buffers going +# negative - so asserting "demand met by due date" or "buffer never negative" would +# false-positive on valid plans (empirically confirmed). We assert only what must +# hold on EVERY valid plan. Validated false-positive-free across the +# constraints_*/pegging_5/demand_policy/safety_stock/flow_alternate golden models; +# the overload check is proven to fire when the capacity constraint is removed. + +import frepple + +TOL = 1e-6 + + +def _infinite_buffer_type(): + # BufferInfinite is an unconstrained source; its onhand may go negative by + # design, so the material invariant skips it. Returns the type or None. + try: + return frepple.buffer_infinite + except AttributeError: + return None + + +def check(capacity_constrained=True): + """Invariant violations for the current solved plan (empty list == OK).""" + violations = [] + binf = _infinite_buffer_type() + + # Index problems once: resource overloads and the buffers frePPLe itself + # flagged with a material shortage. + overloaded_resources = [] + shortage_buffers = set() + for p in frepple.problems(): + name = p.name + owner = getattr(p, "owner", None) + owner_name = getattr(owner, "name", None) + if name == "overload": + overloaded_resources.append(owner_name or "?") + elif name == "material shortage" and owner_name: + shortage_buffers.add(owner_name) + + # I-1: operationplan temporal + quantity sanity (universal - holds always). + for op in frepple.operationplans(): + if op.start > op.end: + violations.append( + "operationplan start after end: %s (%s > %s)" + % (op.operation.name, op.start, op.end) + ) + if op.quantity < -TOL: + violations.append( + "operationplan negative quantity: %s (%s)" + % (op.operation.name, op.quantity) + ) + + # I-2: a capacity-constrained plan must leave no resource overloaded. + if capacity_constrained: + for name in overloaded_resources: + violations.append("resource overloaded under capacity constraint: %s" % name) + + # I-3: a finite buffer may dip negative only if frePPLe flags a material + # shortage for it - otherwise the material balance is silently broken. + if binf is not None: + for buf in frepple.buffers(): + if isinstance(buf, binf): + continue + worst = min((fp.onhand for fp in buf.flowplans), default=0.0) + if worst < -TOL and buf.name not in shortage_buffers: + violations.append( + "buffer negative without a shortage problem: %s (min onhand %.4f)" + % (buf.name, worst) + ) + + return violations diff --git a/test/invariants_sweep/invariants_sweep.1.expect b/test/invariants_sweep/invariants_sweep.1.expect new file mode 100644 index 0000000000..dacf426964 --- /dev/null +++ b/test/invariants_sweep/invariants_sweep.1.expect @@ -0,0 +1 @@ +INVARIANTS_OK diff --git a/test/invariants_sweep/invariants_sweep.py b/test/invariants_sweep/invariants_sweep.py new file mode 100644 index 0000000000..49266cd211 --- /dev/null +++ b/test/invariants_sweep/invariants_sweep.py @@ -0,0 +1,41 @@ +# Engine track E2 - structural-invariant sweep. Loads each model below DATA-ONLY +# (no embedded python), solves it fully constrained, and asserts the shared +# test/invariants.py invariants. One process, many models, full control of the +# solve mode - so every invariant applies. A boolean pass/fail oracle (emits a +# deterministic INVARIANTS_OK), robust to the environment-dependent output ORDER +# that blocks byte-exact golden coverage (engine-review-E1.md H4). Reset between +# models with frepple.erase(True). +import sys, os + +_testroot = os.path.dirname(os.getcwd()) # test/ (cwd is test/invariants_sweep) +if _testroot not in sys.path: + sys.path.insert(0, _testroot) +import invariants + +# Validated false-positive-free, solved constrained (see PR / E2 notes). +MODELS = [ + "constraints_resource_1", "constraints_resource_3", "constraints_resource_5", + "constraints_material_1", "constraints_material_3", + "constraints_combined_1", "constraints_leadtime_1", + "pegging_5", "demand_policy", "safety_stock", "flow_alternate_1", +] +verbose = bool(os.environ.get("INV_VERBOSE")) + +failures = [] +for m in MODELS: + model = os.path.join(_testroot, m, m + ".xml") + frepple.erase(True) + frepple.readXMLfile(model, False, False, None, False) + frepple.solver_mrp(plantype=1, constraints=15, loglevel=0).solve() + v = invariants.check(capacity_constrained=True) + if verbose: + print("%-26s %s" % (m, "OK" if not v else ("FAIL: " + "; ".join(v)))) + if v: + failures.append("%s -> %s" % (m, "; ".join(v))) + +if failures: + raise Exception("structural invariants violated in %d model(s): %s" + % (len(failures), " || ".join(failures))) + +with open("output.1.xml", "wt") as out: + print("INVARIANTS_OK", file=out) diff --git a/test/pegging_10/pegging_10.1.expect b/test/pegging_10/pegging_10.1.expect new file mode 100644 index 0000000000..ee7ef41cf7 --- /dev/null +++ b/test/pegging_10/pegging_10.1.expect @@ -0,0 +1,249 @@ +Upstream pegging of operationplan with id buffer A @ factory with quantity 10.0 of 'Inventory buffer A @ factory': + 0 buffer A @ factory Inventory buffer A @ factory 10.0 +Downstream pegging of operationplan with id buffer A @ factory with quantity 10.0 of 'Inventory buffer A @ factory': + 0 buffer A @ factory Inventory buffer A @ factory 10.0 + 1 1008 Ship buffer A @ factory 10.0 +Upstream pegging of operationplan with id buffer B @ factory with quantity 10.0 of 'Inventory buffer B @ factory': + 0 buffer B @ factory Inventory buffer B @ factory 10.0 +Downstream pegging of operationplan with id buffer B @ factory with quantity 10.0 of 'Inventory buffer B @ factory': + 0 buffer B @ factory Inventory buffer B @ factory 10.0 + 1 1009 Ship buffer B @ factory 10.0 +Upstream pegging of operationplan with id buffer C @ factory with quantity 10.0 of 'Inventory buffer C @ factory': + 0 buffer C @ factory Inventory buffer C @ factory 10.0 +Downstream pegging of operationplan with id buffer C @ factory with quantity 10.0 of 'Inventory buffer C @ factory': + 0 buffer C @ factory Inventory buffer C @ factory 10.0 + 1 1010 Ship buffer C @ factory 10.0 +Upstream pegging of operationplan with id buffer D @ factory with quantity 10.0 of 'Inventory buffer D @ factory': + 0 buffer D @ factory Inventory buffer D @ factory 10.0 +Downstream pegging of operationplan with id buffer D @ factory with quantity 10.0 of 'Inventory buffer D @ factory': + 0 buffer D @ factory Inventory buffer D @ factory 10.0 +Upstream pegging of operationplan with id buffer E @ factory with quantity 10.0 of 'Inventory buffer E @ factory': + 0 buffer E @ factory Inventory buffer E @ factory 10.0 +Downstream pegging of operationplan with id buffer E @ factory with quantity 10.0 of 'Inventory buffer E @ factory': + 0 buffer E @ factory Inventory buffer E @ factory 10.0 + 1 1011 Ship buffer E @ factory 10.0 +Upstream pegging of operationplan with id buffer F @ factory with quantity 15.0 of 'Inventory buffer F @ factory': + 0 buffer F @ factory Inventory buffer F @ factory 15.0 +Downstream pegging of operationplan with id buffer F @ factory with quantity 15.0 of 'Inventory buffer F @ factory': + 0 buffer F @ factory Inventory buffer F @ factory 15.0 + 1 1012 Ship buffer F @ factory 15.0 +Upstream pegging of operationplan with id buffer G @ factory with quantity 15.0 of 'Inventory buffer G @ factory': + 0 buffer G @ factory Inventory buffer G @ factory 15.0 +Downstream pegging of operationplan with id buffer G @ factory with quantity 15.0 of 'Inventory buffer G @ factory': + 0 buffer G @ factory Inventory buffer G @ factory 15.0 + 1 1013 Ship buffer G @ factory 15.0 +Upstream pegging of operationplan with id 1014 with quantity 20.0 of 'Purchase item H1 @ factory from Supplier for H': + 0 1014 Purchase item H1 @ factory from Supplier for H 20.0 +Downstream pegging of operationplan with id 1014 with quantity 20.0 of 'Purchase item H1 @ factory from Supplier for H': + 0 1014 Purchase item H1 @ factory from Supplier for H 20.0 + 1 1015 Ship item H1 @ factory 20.0 +Upstream pegging of operationplan with id 1016 with quantity 20.0 of 'Purchase item H2 @ factory from Supplier for H': + 0 1016 Purchase item H2 @ factory from Supplier for H 20.0 +Downstream pegging of operationplan with id 1016 with quantity 20.0 of 'Purchase item H2 @ factory from Supplier for H': + 0 1016 Purchase item H2 @ factory from Supplier for H 20.0 + 1 1017 Ship item H2 @ factory 20.0 +Upstream pegging of operationplan with id 1018 with quantity 20.0 of 'Purchase item H3 @ factory from Supplier for H': + 0 1018 Purchase item H3 @ factory from Supplier for H 20.0 +Downstream pegging of operationplan with id 1018 with quantity 20.0 of 'Purchase item H3 @ factory from Supplier for H': + 0 1018 Purchase item H3 @ factory from Supplier for H 20.0 + 1 1019 Ship item H3 @ factory 20.0 +Upstream pegging of operationplan with id 1020 with quantity 20.0 of 'Purchase item I1 @ factory from Supplier for I': + 0 1020 Purchase item I1 @ factory from Supplier for I 20.0 +Downstream pegging of operationplan with id 1020 with quantity 20.0 of 'Purchase item I1 @ factory from Supplier for I': + 0 1020 Purchase item I1 @ factory from Supplier for I 20.0 + 1 1021 Ship item I1 @ factory 20.0 +Upstream pegging of operationplan with id 1022 with quantity 20.0 of 'Purchase item I2 @ factory from Supplier for I': + 0 1022 Purchase item I2 @ factory from Supplier for I 20.0 +Downstream pegging of operationplan with id 1022 with quantity 20.0 of 'Purchase item I2 @ factory from Supplier for I': + 0 1022 Purchase item I2 @ factory from Supplier for I 20.0 + 1 1023 Ship item I2 @ factory 20.0 +Upstream pegging of operationplan with id 1024 with quantity 20.0 of 'Purchase item I3 @ factory from Supplier for I': + 0 1024 Purchase item I3 @ factory from Supplier for I 20.0 +Downstream pegging of operationplan with id 1024 with quantity 20.0 of 'Purchase item I3 @ factory from Supplier for I': + 0 1024 Purchase item I3 @ factory from Supplier for I 20.0 + 1 1025 Ship item I3 @ factory 20.0 +Upstream pegging of operationplan with id 1026 with quantity 20.0 of 'Purchase item J1 @ factory from Supplier for J': + 0 1026 Purchase item J1 @ factory from Supplier for J 20.0 +Downstream pegging of operationplan with id 1026 with quantity 20.0 of 'Purchase item J1 @ factory from Supplier for J': + 0 1026 Purchase item J1 @ factory from Supplier for J 20.0 + 1 1027 Ship item J1 @ factory 20.0 +Upstream pegging of operationplan with id 1028 with quantity 20.0 of 'Purchase item J2 @ factory from Supplier for J': + 0 1028 Purchase item J2 @ factory from Supplier for J 20.0 +Downstream pegging of operationplan with id 1028 with quantity 20.0 of 'Purchase item J2 @ factory from Supplier for J': + 0 1028 Purchase item J2 @ factory from Supplier for J 20.0 + 1 1029 Ship item J2 @ factory 20.0 +Upstream pegging of operationplan with id 1030 with quantity 20.0 of 'Purchase item J3 @ factory from Supplier for J': + 0 1030 Purchase item J3 @ factory from Supplier for J 20.0 +Downstream pegging of operationplan with id 1030 with quantity 20.0 of 'Purchase item J3 @ factory from Supplier for J': + 0 1030 Purchase item J3 @ factory from Supplier for J 20.0 + 1 1031 Ship item J3 @ factory 20.0 +Upstream pegging of operationplan with id 1008 with quantity 10.0 of 'Ship buffer A @ factory': + 0 1008 Ship buffer A @ factory 10.0 + 1 buffer A @ factory Inventory buffer A @ factory 10.0 +Downstream pegging of operationplan with id 1008 with quantity 10.0 of 'Ship buffer A @ factory': + 0 1008 Ship buffer A @ factory 10.0 +Upstream pegging of operationplan with id 1032 with quantity 10.0 of 'Ship buffer A @ factory': + 0 1032 Ship buffer A @ factory 10.0 + 1 1001 supply item A 10.0 +Downstream pegging of operationplan with id 1032 with quantity 10.0 of 'Ship buffer A @ factory': + 0 1032 Ship buffer A @ factory 10.0 +Upstream pegging of operationplan with id 1009 with quantity 10.0 of 'Ship buffer B @ factory': + 0 1009 Ship buffer B @ factory 10.0 + 1 buffer B @ factory Inventory buffer B @ factory 10.0 +Downstream pegging of operationplan with id 1009 with quantity 10.0 of 'Ship buffer B @ factory': + 0 1009 Ship buffer B @ factory 10.0 +Upstream pegging of operationplan with id 1010 with quantity 20.0 of 'Ship buffer C @ factory': + 0 1010 Ship buffer C @ factory 20.0 + 1 buffer C @ factory Inventory buffer C @ factory 10.0 + 1 1003 supply item C 10.0 +Downstream pegging of operationplan with id 1010 with quantity 20.0 of 'Ship buffer C @ factory': + 0 1010 Ship buffer C @ factory 20.0 +Upstream pegging of operationplan with id 1011 with quantity 10.0 of 'Ship buffer E @ factory': + 0 1011 Ship buffer E @ factory 10.0 + 1 buffer E @ factory Inventory buffer E @ factory 10.0 +Downstream pegging of operationplan with id 1011 with quantity 10.0 of 'Ship buffer E @ factory': + 0 1011 Ship buffer E @ factory 10.0 +Upstream pegging of operationplan with id 1012 with quantity 20.0 of 'Ship buffer F @ factory': + 0 1012 Ship buffer F @ factory 20.0 + 1 buffer F @ factory Inventory buffer F @ factory 15.0 + 1 1006 supply item F 5.0 +Downstream pegging of operationplan with id 1012 with quantity 20.0 of 'Ship buffer F @ factory': + 0 1012 Ship buffer F @ factory 20.0 +Upstream pegging of operationplan with id 1013 with quantity 30.0 of 'Ship buffer G @ factory': + 0 1013 Ship buffer G @ factory 30.0 + 1 buffer G @ factory Inventory buffer G @ factory 15.0 + 1 1007 supply item G 15.0 +Downstream pegging of operationplan with id 1013 with quantity 30.0 of 'Ship buffer G @ factory': + 0 1013 Ship buffer G @ factory 30.0 +Upstream pegging of operationplan with id 1015 with quantity 20.0 of 'Ship item H1 @ factory': + 0 1015 Ship item H1 @ factory 20.0 + 1 1014 Purchase item H1 @ factory from Supplier for H 20.0 +Downstream pegging of operationplan with id 1015 with quantity 20.0 of 'Ship item H1 @ factory': + 0 1015 Ship item H1 @ factory 20.0 +Upstream pegging of operationplan with id 1017 with quantity 20.0 of 'Ship item H2 @ factory': + 0 1017 Ship item H2 @ factory 20.0 + 1 1016 Purchase item H2 @ factory from Supplier for H 20.0 +Downstream pegging of operationplan with id 1017 with quantity 20.0 of 'Ship item H2 @ factory': + 0 1017 Ship item H2 @ factory 20.0 +Upstream pegging of operationplan with id 1019 with quantity 20.0 of 'Ship item H3 @ factory': + 0 1019 Ship item H3 @ factory 20.0 + 1 1018 Purchase item H3 @ factory from Supplier for H 20.0 +Downstream pegging of operationplan with id 1019 with quantity 20.0 of 'Ship item H3 @ factory': + 0 1019 Ship item H3 @ factory 20.0 +Upstream pegging of operationplan with id 1021 with quantity 20.0 of 'Ship item I1 @ factory': + 0 1021 Ship item I1 @ factory 20.0 + 1 1020 Purchase item I1 @ factory from Supplier for I 20.0 +Downstream pegging of operationplan with id 1021 with quantity 20.0 of 'Ship item I1 @ factory': + 0 1021 Ship item I1 @ factory 20.0 +Upstream pegging of operationplan with id 1023 with quantity 20.0 of 'Ship item I2 @ factory': + 0 1023 Ship item I2 @ factory 20.0 + 1 1022 Purchase item I2 @ factory from Supplier for I 20.0 +Downstream pegging of operationplan with id 1023 with quantity 20.0 of 'Ship item I2 @ factory': + 0 1023 Ship item I2 @ factory 20.0 +Upstream pegging of operationplan with id 1025 with quantity 20.0 of 'Ship item I3 @ factory': + 0 1025 Ship item I3 @ factory 20.0 + 1 1024 Purchase item I3 @ factory from Supplier for I 20.0 +Downstream pegging of operationplan with id 1025 with quantity 20.0 of 'Ship item I3 @ factory': + 0 1025 Ship item I3 @ factory 20.0 +Upstream pegging of operationplan with id 1027 with quantity 20.0 of 'Ship item J1 @ factory': + 0 1027 Ship item J1 @ factory 20.0 + 1 1026 Purchase item J1 @ factory from Supplier for J 20.0 +Downstream pegging of operationplan with id 1027 with quantity 20.0 of 'Ship item J1 @ factory': + 0 1027 Ship item J1 @ factory 20.0 +Upstream pegging of operationplan with id 1029 with quantity 20.0 of 'Ship item J2 @ factory': + 0 1029 Ship item J2 @ factory 20.0 + 1 1028 Purchase item J2 @ factory from Supplier for J 20.0 +Downstream pegging of operationplan with id 1029 with quantity 20.0 of 'Ship item J2 @ factory': + 0 1029 Ship item J2 @ factory 20.0 +Upstream pegging of operationplan with id 1031 with quantity 20.0 of 'Ship item J3 @ factory': + 0 1031 Ship item J3 @ factory 20.0 + 1 1030 Purchase item J3 @ factory from Supplier for J 20.0 +Downstream pegging of operationplan with id 1031 with quantity 20.0 of 'Ship item J3 @ factory': + 0 1031 Ship item J3 @ factory 20.0 +Upstream pegging of operationplan with id 1001 with quantity 10.0 of 'supply item A': + 0 1001 supply item A 10.0 +Downstream pegging of operationplan with id 1001 with quantity 10.0 of 'supply item A': + 0 1001 supply item A 10.0 + 1 1032 Ship buffer A @ factory 10.0 +Upstream pegging of operationplan with id 1002 with quantity 10.0 of 'supply item B': + 0 1002 supply item B 10.0 +Downstream pegging of operationplan with id 1002 with quantity 10.0 of 'supply item B': + 0 1002 supply item B 10.0 +Upstream pegging of operationplan with id 1003 with quantity 10.0 of 'supply item C': + 0 1003 supply item C 10.0 +Downstream pegging of operationplan with id 1003 with quantity 10.0 of 'supply item C': + 0 1003 supply item C 10.0 + 1 1010 Ship buffer C @ factory 10.0 +Upstream pegging of operationplan with id 1004 with quantity 10.0 of 'supply item D': + 0 1004 supply item D 10.0 +Downstream pegging of operationplan with id 1004 with quantity 10.0 of 'supply item D': + 0 1004 supply item D 10.0 +Upstream pegging of operationplan with id 1005 with quantity 10.0 of 'supply item E': + 0 1005 supply item E 10.0 +Downstream pegging of operationplan with id 1005 with quantity 10.0 of 'supply item E': + 0 1005 supply item E 10.0 +Upstream pegging of operationplan with id 1006 with quantity 10.0 of 'supply item F': + 0 1006 supply item F 10.0 +Downstream pegging of operationplan with id 1006 with quantity 10.0 of 'supply item F': + 0 1006 supply item F 10.0 + 1 1012 Ship buffer F @ factory 5.0 +Upstream pegging of operationplan with id 1007 with quantity 50.0 of 'supply item G': + 0 1007 supply item G 50.0 +Downstream pegging of operationplan with id 1007 with quantity 50.0 of 'supply item G': + 0 1007 supply item G 50.0 + 1 1013 Ship buffer G @ factory 15.0 +Pegging of demand ignore me with quantity 20.0: +Pegging of demand order A with quantity 20.0: + 0 1008 Ship buffer A @ factory 10.0 + 1 buffer A @ factory Inventory buffer A @ factory 10.0 + 0 1032 Ship buffer A @ factory 10.0 + 1 1001 supply item A 10.0 +Pegging of demand order B with quantity 20.0: + 0 1009 Ship buffer B @ factory 10.0 + 1 buffer B @ factory Inventory buffer B @ factory 10.0 +Pegging of demand order C with quantity 20.0: + 0 1010 Ship buffer C @ factory 20.0 + 1 buffer C @ factory Inventory buffer C @ factory 10.0 + 1 1003 supply item C 10.0 +Pegging of demand order D with quantity 20.0: +Pegging of demand order E with quantity 20.0: + 0 1011 Ship buffer E @ factory 10.0 + 1 buffer E @ factory Inventory buffer E @ factory 10.0 +Pegging of demand order F with quantity 20.0: + 0 1012 Ship buffer F @ factory 20.0 + 1 buffer F @ factory Inventory buffer F @ factory 15.0 + 1 1006 supply item F 5.0 +Pegging of demand order G with quantity 20.0: + 0 1013 Ship buffer G @ factory 30.0 + 1 buffer G @ factory Inventory buffer G @ factory 15.0 + 1 1007 supply item G 15.0 +Pegging of demand order H with quantity 0.0: +Pegging of demand order H1 with quantity 20.0: + 0 1015 Ship item H1 @ factory 20.0 + 1 1014 Purchase item H1 @ factory from Supplier for H 20.0 +Pegging of demand order H2 with quantity 20.0: + 0 1017 Ship item H2 @ factory 20.0 + 1 1016 Purchase item H2 @ factory from Supplier for H 20.0 +Pegging of demand order H3 with quantity 20.0: + 0 1019 Ship item H3 @ factory 20.0 + 1 1018 Purchase item H3 @ factory from Supplier for H 20.0 +Pegging of demand order I with quantity 0.0: +Pegging of demand order I1 with quantity 20.0: + 0 1021 Ship item I1 @ factory 20.0 + 1 1020 Purchase item I1 @ factory from Supplier for I 20.0 +Pegging of demand order I2 with quantity 20.0: + 0 1023 Ship item I2 @ factory 20.0 + 1 1022 Purchase item I2 @ factory from Supplier for I 20.0 +Pegging of demand order I3 with quantity 20.0: + 0 1025 Ship item I3 @ factory 20.0 + 1 1024 Purchase item I3 @ factory from Supplier for I 20.0 +Pegging of demand order J with quantity 0.0: +Pegging of demand order J1 with quantity 20.0: + 0 1027 Ship item J1 @ factory 20.0 + 1 1026 Purchase item J1 @ factory from Supplier for J 20.0 +Pegging of demand order J2 with quantity 20.0: + 0 1029 Ship item J2 @ factory 20.0 + 1 1028 Purchase item J2 @ factory from Supplier for J 20.0 +Pegging of demand order J3 with quantity 20.0: + 0 1031 Ship item J3 @ factory 20.0 + 1 1030 Purchase item J3 @ factory from Supplier for J 20.0 diff --git a/test/pegging_10/pegging_10.xml b/test/pegging_10/pegging_10.xml new file mode 100644 index 0000000000..daacc00b83 --- /dev/null +++ b/test/pegging_10/pegging_10.xml @@ -0,0 +1,432 @@ + + + actual plan + + Verify the demand policies. + The supply situation is such that half of the demand can be met in time, and + half of it late. + demand = 20 on due date 5 Jan + Supply = 10 available as inventory + + 10 arriving on 10 Jan + The demand policy controls how the demand is allowed to be planned: + A) The default policy is to allow demands to be planned without any limits + on the timing and quantity of the deliveries. + -> Delivery of 10 units on 5 Jan and a second delivery on 10 Jan + B) No lateness is allowed. + -> A delivery of 10 units on 5 Jan + C) Lateness is allowed, but we only accept a delivery for the full + requested quantity. + -> A delivery of 20 units on 10 Jan + D) No lateness is allowed, and we also only accept a delivery for the full + requested quantity. + -> No delivery planned + E) The maximum allowed delivery date is jan 7, without any restriction on + the delivered quantity. + -> A delivery of 10 units on 5 Jan + F) The minimum quantity shipped is 11, without any restriction on the + delivery date. + In this case the onhand on jan 5 is increased to 15. + -> A delivery of 20 units on 10 Jan + G) The minimum shipment quantity is higher than the order quantity. + We are forced to ship an excess quantity. + H) Example of demand group with independent policy + I) Example of demand group with alltogether policy + J) Example of demand group with inratio policy + + 2009-01-01T00:00:00 + + + + 0 + + + + 1 + + + + + + 0 + + + + 1 + + + + + + 0 + + + + 1 + + + + + + 0 + + + + 1 + + + + + + 0 + + + + 1 + + + + + + 0 + + + + 1 + + + + + + 0 + + + + 1 + + + + + + + + + 10 + + + + + 10 + + + + + 10 + + + + + 10 + + + + + 10 + + + + + 15 + + + + + 15 + + + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 10 + confirmed + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 10 + confirmed + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 10 + confirmed + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 10 + confirmed + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 10 + confirmed + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 10 + confirmed + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 50 + confirmed + + + + + + + + P14D + + + + + + + + P6D + + + + + + + + P30D + + + + + + + + P14D + + + + + + + + P6D + + + + + + + + P30D + + + + + + + + P14D + + + + + + + + P6D + + + + + + + + P30D + + + + + + + 20 + 2009-01-05T00:00:00 + 1 + + + + + 20 + 2009-01-05T00:00:00 + 1 + + + P0D + + + 20 + 2009-01-05T00:00:00 + 1 + + + 20 + + + 20 + 2009-01-05T00:00:00 + 1 + + + P0D + 20 + + + 20 + 2009-01-05T00:00:00 + 1 + + + P2D + + + 20 + 2009-01-05T00:00:00 + 1 + + + 11 + + + 20 + 2009-01-05T00:00:00 + 1 + + + 30 + + + + + 20 + 2009-01-05T00:00:00 + 1 + + + + + 20 + 2009-01-06T00:00:00 + 1 + + + + + 20 + 2009-01-05T00:00:00 + 1 + + + + + independent + + + + + 20 + 2009-01-05T00:00:00 + 1 + + + + + 20 + 2009-01-06T00:00:00 + 1 + + + + + 20 + 2009-01-05T00:00:00 + 1 + + + + + alltogether + + + + + 20 + 2009-01-05T00:00:00 + 1 + + + + + 20 + 2009-01-06T00:00:00 + 1 + + + + + 20 + 2009-01-05T00:00:00 + 1 + + + + + inratio + + + closed + 20 + 2009-01-05T00:00:00 + 1 + + + 30 + + + + + diff --git a/test/pegging_11/pegging_11.1.expect b/test/pegging_11/pegging_11.1.expect new file mode 100644 index 0000000000..594e0c15b4 --- /dev/null +++ b/test/pegging_11/pegging_11.1.expect @@ -0,0 +1,75 @@ +Upstream pegging of operationplan with id raw material buffer 1 with quantity 150.0 of 'Inventory raw material buffer 1': + 0 raw material buffer 1 Inventory raw material buffer 1 150.0 +Downstream pegging of operationplan with id raw material buffer 1 with quantity 150.0 of 'Inventory raw material buffer 1': + 0 raw material buffer 1 Inventory raw material buffer 1 150.0 + 1 1 make 1 500.0 + 2 2 delivery 1 500.0 + 1 3 make 1 1000.0 + 2 2 delivery 1 1000.0 +Upstream pegging of operationplan with id 2 with quantity 5000.0 of 'delivery 1': + 0 2 delivery 1 5000.0 + 1 3 make 1 1000.0 + 2 raw material buffer 1 Inventory raw material buffer 1 100.0 + 1 1 make 1 1000.0 + 2 raw material buffer 1 Inventory raw material buffer 1 50.0 + 2 4 supply 1 50.0 + 1 5 make 1 1000.0 + 2 4 supply 1 100.0 + 1 6 make 1 1000.0 + 2 4 supply 1 100.0 + 1 7 make 1 1000.0 + 2 4 supply 1 100.0 +Downstream pegging of operationplan with id 2 with quantity 5000.0 of 'delivery 1': + 0 2 delivery 1 5000.0 +Upstream pegging of operationplan with id 3 with quantity 1000.0 of 'make 1': + 0 3 make 1 1000.0 + 1 raw material buffer 1 Inventory raw material buffer 1 100.0 +Downstream pegging of operationplan with id 3 with quantity 1000.0 of 'make 1': + 0 3 make 1 1000.0 + 1 2 delivery 1 1000.0 +Upstream pegging of operationplan with id 1 with quantity 1000.0 of 'make 1': + 0 1 make 1 1000.0 + 1 raw material buffer 1 Inventory raw material buffer 1 50.0 + 1 4 supply 1 50.0 +Downstream pegging of operationplan with id 1 with quantity 1000.0 of 'make 1': + 0 1 make 1 1000.0 + 1 2 delivery 1 1000.0 +Upstream pegging of operationplan with id 5 with quantity 1000.0 of 'make 1': + 0 5 make 1 1000.0 + 1 4 supply 1 100.0 +Downstream pegging of operationplan with id 5 with quantity 1000.0 of 'make 1': + 0 5 make 1 1000.0 + 1 2 delivery 1 1000.0 +Upstream pegging of operationplan with id 6 with quantity 1000.0 of 'make 1': + 0 6 make 1 1000.0 + 1 4 supply 1 100.0 +Downstream pegging of operationplan with id 6 with quantity 1000.0 of 'make 1': + 0 6 make 1 1000.0 + 1 2 delivery 1 1000.0 +Upstream pegging of operationplan with id 7 with quantity 1000.0 of 'make 1': + 0 7 make 1 1000.0 + 1 4 supply 1 100.0 +Downstream pegging of operationplan with id 7 with quantity 1000.0 of 'make 1': + 0 7 make 1 1000.0 + 1 2 delivery 1 1000.0 +Upstream pegging of operationplan with id 4 with quantity 1000.0 of 'supply 1': + 0 4 supply 1 1000.0 +Downstream pegging of operationplan with id 4 with quantity 1000.0 of 'supply 1': + 0 4 supply 1 1000.0 + 1 7 make 1 1000.0 + 2 2 delivery 1 1000.0 + 1 6 make 1 1000.0 + 2 2 delivery 1 1000.0 + 1 5 make 1 1000.0 + 2 2 delivery 1 1000.0 + 1 1 make 1 500.0 + 2 2 delivery 1 500.0 +Pegging of demand order for item 1 with quantity 5000.0: + 0 2 delivery 1 5000.0 + 1 3 make 1 1000.0 + 2 raw material buffer 1 Inventory raw material buffer 1 150.0 + 1 1 make 1 1000.0 + 2 4 supply 1 350.0 + 1 5 make 1 1000.0 + 1 6 make 1 1000.0 + 1 7 make 1 1000.0 diff --git a/test/pegging_11/pegging_11.xml b/test/pegging_11/pegging_11.xml new file mode 100644 index 0000000000..93f58e86cd --- /dev/null +++ b/test/pegging_11/pegging_11.xml @@ -0,0 +1,78 @@ + + + Material constraint test model + + This model tests the solver code in situations where excess + material could be created. + + 2009-01-01T00:00:00 + + + + + + + + + 150 + + + + + + + -1 + + + + + 1 + + + + + -0.1 + + + + + 1 + + + + + 1 + + + + + + + + + + 5000 + 2009-01-30T00:00:00 + + + + + + + diff --git a/test/pegging_12/pegging_12.1.expect b/test/pegging_12/pegging_12.1.expect new file mode 100644 index 0000000000..eaaaecaad3 --- /dev/null +++ b/test/pegging_12/pegging_12.1.expect @@ -0,0 +1,387 @@ +Upstream pegging of operationplan with id 8 with quantity 1.0 of '1. produce end item': + 0 8 1. produce end item 1.0 + 1 9 1. produce intermediate item 2.0 + 2 10 Purchase 1. raw material @ 1. factory from 1. supplier 6.0 +Downstream pegging of operationplan with id 8 with quantity 1.0 of '1. produce end item': + 0 8 1. produce end item 1.0 + 1 11 Ship 1. end item @ 1. factory 1.0 +Upstream pegging of operationplan with id 9 with quantity 2.0 of '1. produce intermediate item': + 0 9 1. produce intermediate item 2.0 + 1 10 Purchase 1. raw material @ 1. factory from 1. supplier 6.0 +Downstream pegging of operationplan with id 9 with quantity 2.0 of '1. produce intermediate item': + 0 9 1. produce intermediate item 2.0 + 1 8 1. produce end item 1.0 + 2 11 Ship 1. end item @ 1. factory 1.0 +Upstream pegging of operationplan with id 12 with quantity 1.0 of '2. produce end item': + 0 12 2. produce end item 1.0 + 1 13 2. produce intermediate item 2.0 + 2 14 Purchase 2. raw material @ 2. factory from 2. supplier 6.0 +Downstream pegging of operationplan with id 12 with quantity 1.0 of '2. produce end item': + 0 12 2. produce end item 1.0 + 1 15 Ship 2. end item @ 2. factory 1.0 +Upstream pegging of operationplan with id 13 with quantity 2.0 of '2. produce intermediate item': + 0 13 2. produce intermediate item 2.0 + 1 14 Purchase 2. raw material @ 2. factory from 2. supplier 6.0 +Downstream pegging of operationplan with id 13 with quantity 2.0 of '2. produce intermediate item': + 0 13 2. produce intermediate item 2.0 + 1 12 2. produce end item 1.0 + 2 15 Ship 2. end item @ 2. factory 1.0 +Upstream pegging of operationplan with id 16 with quantity 1.0 of '3. produce end item': + 0 16 3. produce end item 1.0 + 1 17 3. produce intermediate item 2.0 + 2 18 Purchase 3. raw material @ 3. factory from 3. supplier 6.0 +Downstream pegging of operationplan with id 16 with quantity 1.0 of '3. produce end item': + 0 16 3. produce end item 1.0 + 1 19 Ship 3. end item @ 3. factory 1.0 +Upstream pegging of operationplan with id 17 with quantity 2.0 of '3. produce intermediate item': + 0 17 3. produce intermediate item 2.0 + 1 18 Purchase 3. raw material @ 3. factory from 3. supplier 6.0 +Downstream pegging of operationplan with id 17 with quantity 2.0 of '3. produce intermediate item': + 0 17 3. produce intermediate item 2.0 + 1 16 3. produce end item 1.0 + 2 19 Ship 3. end item @ 3. factory 1.0 +Upstream pegging of operationplan with id 20 with quantity 1.0 of '4. produce end item': + 0 20 4. produce end item 1.0 + 1 21 4. produce intermediate item 2.0 + 2 22 Purchase 4. raw material @ 4. factory from 4. supplier 6.0 +Downstream pegging of operationplan with id 20 with quantity 1.0 of '4. produce end item': + 0 20 4. produce end item 1.0 + 1 23 Ship 4. end item @ 4. factory 1.0 +Upstream pegging of operationplan with id 21 with quantity 2.0 of '4. produce intermediate item': + 0 21 4. produce intermediate item 2.0 + 1 22 Purchase 4. raw material @ 4. factory from 4. supplier 6.0 +Downstream pegging of operationplan with id 21 with quantity 2.0 of '4. produce intermediate item': + 0 21 4. produce intermediate item 2.0 + 1 20 4. produce end item 1.0 + 2 23 Ship 4. end item @ 4. factory 1.0 +Upstream pegging of operationplan with id 24 with quantity 1.0 of '5. produce end item': + 0 24 5. produce end item 1.0 + 1 25 5. produce intermediate item 2.0 + 2 26 Purchase 5. raw material @ 5. factory from 5. supplier 6.0 +Downstream pegging of operationplan with id 24 with quantity 1.0 of '5. produce end item': + 0 24 5. produce end item 1.0 + 1 27 Ship 5. end item @ 5. factory 1.0 +Upstream pegging of operationplan with id 25 with quantity 2.0 of '5. produce intermediate item': + 0 25 5. produce intermediate item 2.0 + 1 26 Purchase 5. raw material @ 5. factory from 5. supplier 6.0 +Downstream pegging of operationplan with id 25 with quantity 2.0 of '5. produce intermediate item': + 0 25 5. produce intermediate item 2.0 + 1 24 5. produce end item 1.0 + 2 27 Ship 5. end item @ 5. factory 1.0 +Upstream pegging of operationplan with id 28 with quantity 1.0 of '6. produce end item': + 0 28 6. produce end item 1.0 + 1 29 6. produce intermediate item 2.0 + 2 30 Purchase 6. raw material @ 6. factory from 6. supplier 6.0 +Downstream pegging of operationplan with id 28 with quantity 1.0 of '6. produce end item': + 0 28 6. produce end item 1.0 + 1 31 Ship 6. end item @ 6. factory 1.0 +Upstream pegging of operationplan with id 29 with quantity 2.0 of '6. produce intermediate item': + 0 29 6. produce intermediate item 2.0 + 1 30 Purchase 6. raw material @ 6. factory from 6. supplier 6.0 +Downstream pegging of operationplan with id 29 with quantity 2.0 of '6. produce intermediate item': + 0 29 6. produce intermediate item 2.0 + 1 28 6. produce end item 1.0 + 2 31 Ship 6. end item @ 6. factory 1.0 +Upstream pegging of operationplan with id 7. completed MO with quantity 4.0 of '7. produce intermediate item': + 0 7. completed MO 7. produce intermediate item 4.0 + 1 32 Purchase 7. raw material @ 7. factory from 7. supplier 12.0 +Downstream pegging of operationplan with id 7. completed MO with quantity 4.0 of '7. produce intermediate item': + 0 7. completed MO 7. produce intermediate item 4.0 +Upstream pegging of operationplan with id 7. approved MO with quantity 4.0 of '7. produce intermediate item': + 0 7. approved MO 7. produce intermediate item 4.0 + 1 32 Purchase 7. raw material @ 7. factory from 7. supplier 12.0 +Downstream pegging of operationplan with id 7. approved MO with quantity 4.0 of '7. produce intermediate item': + 0 7. approved MO 7. produce intermediate item 4.0 +Upstream pegging of operationplan with id 7. confirmed MO with quantity 4.0 of '7. produce intermediate item': + 0 7. confirmed MO 7. produce intermediate item 4.0 + 1 32 Purchase 7. raw material @ 7. factory from 7. supplier 12.0 +Downstream pegging of operationplan with id 7. confirmed MO with quantity 4.0 of '7. produce intermediate item': + 0 7. confirmed MO 7. produce intermediate item 4.0 +Upstream pegging of operationplan with id 33 with quantity 1.0 of '8. produce end item - alt 1': + 0 34 Replenish 8. end item @ 8. factory 1.0 + 1 33 8. produce end item - alt 1 1.0 +Downstream pegging of operationplan with id 33 with quantity 1.0 of '8. produce end item - alt 1': + 0 34 Replenish 8. end item @ 8. factory 1.0 + 1 33 8. produce end item - alt 1 1.0 + 2 35 Ship 8. end item @ 8. factory 1.0 +Upstream pegging of operationplan with id 36 with quantity 1.0 of '8. produce end item - alt 1': + 0 37 Replenish 8. end item @ 8. factory 1.0 + 1 36 8. produce end item - alt 1 1.0 +Downstream pegging of operationplan with id 36 with quantity 1.0 of '8. produce end item - alt 1': + 0 37 Replenish 8. end item @ 8. factory 1.0 + 1 36 8. produce end item - alt 1 1.0 + 2 38 Ship 8. end item @ 8. factory 1.0 +Upstream pegging of operationplan with id 39 with quantity 8.0 of '8. produce end item - alt 2': + 0 40 Replenish 8. end item @ 8. factory 8.0 + 1 39 8. produce end item - alt 2 8.0 + 2 41 8. produce end item - alt 2 - step B 8.0 + 2 42 8. produce end item - alt 2 - step A 8.0 + 3 43 8. produce intermediate item 8.0 + 4 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 +Downstream pegging of operationplan with id 39 with quantity 8.0 of '8. produce end item - alt 2': + 0 40 Replenish 8. end item @ 8. factory 8.0 + 1 39 8. produce end item - alt 2 8.0 + 2 41 8. produce end item - alt 2 - step B 8.0 + 3 45 Ship 8. end item @ 8. factory 8.0 + 2 42 8. produce end item - alt 2 - step A 8.0 +Upstream pegging of operationplan with id 42 with quantity 8.0 of '8. produce end item - alt 2 - step A': + 0 40 Replenish 8. end item @ 8. factory 8.0 + 1 39 8. produce end item - alt 2 8.0 + 2 41 8. produce end item - alt 2 - step B 8.0 + 2 42 8. produce end item - alt 2 - step A 8.0 + 3 43 8. produce intermediate item 8.0 + 4 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 +Downstream pegging of operationplan with id 42 with quantity 8.0 of '8. produce end item - alt 2 - step A': + 0 40 Replenish 8. end item @ 8. factory 8.0 + 1 39 8. produce end item - alt 2 8.0 + 2 41 8. produce end item - alt 2 - step B 8.0 + 3 45 Ship 8. end item @ 8. factory 8.0 + 2 42 8. produce end item - alt 2 - step A 8.0 +Upstream pegging of operationplan with id 41 with quantity 8.0 of '8. produce end item - alt 2 - step B': + 0 40 Replenish 8. end item @ 8. factory 8.0 + 1 39 8. produce end item - alt 2 8.0 + 2 41 8. produce end item - alt 2 - step B 8.0 + 2 42 8. produce end item - alt 2 - step A 8.0 + 3 43 8. produce intermediate item 8.0 + 4 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 +Downstream pegging of operationplan with id 41 with quantity 8.0 of '8. produce end item - alt 2 - step B': + 0 40 Replenish 8. end item @ 8. factory 8.0 + 1 39 8. produce end item - alt 2 8.0 + 2 41 8. produce end item - alt 2 - step B 8.0 + 3 45 Ship 8. end item @ 8. factory 8.0 + 2 42 8. produce end item - alt 2 - step A 8.0 +Upstream pegging of operationplan with id 43 with quantity 8.0 of '8. produce intermediate item': + 0 43 8. produce intermediate item 8.0 + 1 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 +Downstream pegging of operationplan with id 43 with quantity 8.0 of '8. produce intermediate item': + 0 43 8. produce intermediate item 8.0 + 1 40 Replenish 8. end item @ 8. factory 8.0 + 2 39 8. produce end item - alt 2 8.0 + 3 41 8. produce end item - alt 2 - step B 8.0 + 4 45 Ship 8. end item @ 8. factory 8.0 + 3 42 8. produce end item - alt 2 - step A 8.0 +Upstream pegging of operationplan with id 46 with quantity 1.0 of '9. produce end item': + 0 46 9. produce end item 1.0 + 1 47 Purchase 9. component 2 @ 9. factory from 9. supplier 2.0 + 1 48 Purchase 9. component 1 @ 9. factory from 9. supplier 2.0 +Downstream pegging of operationplan with id 46 with quantity 1.0 of '9. produce end item': + 0 46 9. produce end item 1.0 + 1 49 Ship 9. end item @ 9. factory 1.0 +Upstream pegging of operationplan with id 10 with quantity 6.0 of 'Purchase 1. raw material @ 1. factory from 1. supplier': + 0 10 Purchase 1. raw material @ 1. factory from 1. supplier 6.0 +Downstream pegging of operationplan with id 10 with quantity 6.0 of 'Purchase 1. raw material @ 1. factory from 1. supplier': + 0 10 Purchase 1. raw material @ 1. factory from 1. supplier 6.0 + 1 9 1. produce intermediate item 2.0 + 2 8 1. produce end item 1.0 + 3 11 Ship 1. end item @ 1. factory 1.0 +Upstream pegging of operationplan with id 14 with quantity 6.0 of 'Purchase 2. raw material @ 2. factory from 2. supplier': + 0 14 Purchase 2. raw material @ 2. factory from 2. supplier 6.0 +Downstream pegging of operationplan with id 14 with quantity 6.0 of 'Purchase 2. raw material @ 2. factory from 2. supplier': + 0 14 Purchase 2. raw material @ 2. factory from 2. supplier 6.0 + 1 13 2. produce intermediate item 2.0 + 2 12 2. produce end item 1.0 + 3 15 Ship 2. end item @ 2. factory 1.0 +Upstream pegging of operationplan with id 18 with quantity 6.0 of 'Purchase 3. raw material @ 3. factory from 3. supplier': + 0 18 Purchase 3. raw material @ 3. factory from 3. supplier 6.0 +Downstream pegging of operationplan with id 18 with quantity 6.0 of 'Purchase 3. raw material @ 3. factory from 3. supplier': + 0 18 Purchase 3. raw material @ 3. factory from 3. supplier 6.0 + 1 17 3. produce intermediate item 2.0 + 2 16 3. produce end item 1.0 + 3 19 Ship 3. end item @ 3. factory 1.0 +Upstream pegging of operationplan with id 22 with quantity 6.0 of 'Purchase 4. raw material @ 4. factory from 4. supplier': + 0 22 Purchase 4. raw material @ 4. factory from 4. supplier 6.0 +Downstream pegging of operationplan with id 22 with quantity 6.0 of 'Purchase 4. raw material @ 4. factory from 4. supplier': + 0 22 Purchase 4. raw material @ 4. factory from 4. supplier 6.0 + 1 21 4. produce intermediate item 2.0 + 2 20 4. produce end item 1.0 + 3 23 Ship 4. end item @ 4. factory 1.0 +Upstream pegging of operationplan with id 26 with quantity 6.0 of 'Purchase 5. raw material @ 5. factory from 5. supplier': + 0 26 Purchase 5. raw material @ 5. factory from 5. supplier 6.0 +Downstream pegging of operationplan with id 26 with quantity 6.0 of 'Purchase 5. raw material @ 5. factory from 5. supplier': + 0 26 Purchase 5. raw material @ 5. factory from 5. supplier 6.0 + 1 25 5. produce intermediate item 2.0 + 2 24 5. produce end item 1.0 + 3 27 Ship 5. end item @ 5. factory 1.0 +Upstream pegging of operationplan with id 30 with quantity 6.0 of 'Purchase 6. raw material @ 6. factory from 6. supplier': + 0 30 Purchase 6. raw material @ 6. factory from 6. supplier 6.0 +Downstream pegging of operationplan with id 30 with quantity 6.0 of 'Purchase 6. raw material @ 6. factory from 6. supplier': + 0 30 Purchase 6. raw material @ 6. factory from 6. supplier 6.0 + 1 29 6. produce intermediate item 2.0 + 2 28 6. produce end item 1.0 + 3 31 Ship 6. end item @ 6. factory 1.0 +Upstream pegging of operationplan with id 32 with quantity 100.0 of 'Purchase 7. raw material @ 7. factory from 7. supplier': + 0 32 Purchase 7. raw material @ 7. factory from 7. supplier 100.0 +Downstream pegging of operationplan with id 32 with quantity 100.0 of 'Purchase 7. raw material @ 7. factory from 7. supplier': + 0 32 Purchase 7. raw material @ 7. factory from 7. supplier 100.0 + 1 7. confirmed MO 7. produce intermediate item 4.0 + 1 7. approved MO 7. produce intermediate item 4.0 + 1 7. completed MO 7. produce intermediate item 4.0 +Upstream pegging of operationplan with id 44 with quantity 24.0 of 'Purchase 8. raw material @ 8. factory from 8. supplier': + 0 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 +Downstream pegging of operationplan with id 44 with quantity 24.0 of 'Purchase 8. raw material @ 8. factory from 8. supplier': + 0 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 + 1 43 8. produce intermediate item 8.0 + 2 40 Replenish 8. end item @ 8. factory 8.0 + 3 39 8. produce end item - alt 2 8.0 + 4 41 8. produce end item - alt 2 - step B 8.0 + 5 45 Ship 8. end item @ 8. factory 8.0 + 4 42 8. produce end item - alt 2 - step A 8.0 +Upstream pegging of operationplan with id 48 with quantity 2.0 of 'Purchase 9. component 1 @ 9. factory from 9. supplier': + 0 48 Purchase 9. component 1 @ 9. factory from 9. supplier 2.0 +Downstream pegging of operationplan with id 48 with quantity 2.0 of 'Purchase 9. component 1 @ 9. factory from 9. supplier': + 0 48 Purchase 9. component 1 @ 9. factory from 9. supplier 2.0 + 1 46 9. produce end item 1.0 + 2 49 Ship 9. end item @ 9. factory 1.0 +Upstream pegging of operationplan with id 47 with quantity 2.0 of 'Purchase 9. component 2 @ 9. factory from 9. supplier': + 0 47 Purchase 9. component 2 @ 9. factory from 9. supplier 2.0 +Downstream pegging of operationplan with id 47 with quantity 2.0 of 'Purchase 9. component 2 @ 9. factory from 9. supplier': + 0 47 Purchase 9. component 2 @ 9. factory from 9. supplier 2.0 + 1 46 9. produce end item 1.0 + 2 49 Ship 9. end item @ 9. factory 1.0 +Upstream pegging of operationplan with id 34 with quantity 1.0 of 'Replenish 8. end item @ 8. factory': + 0 34 Replenish 8. end item @ 8. factory 1.0 + 1 33 8. produce end item - alt 1 1.0 +Downstream pegging of operationplan with id 34 with quantity 1.0 of 'Replenish 8. end item @ 8. factory': + 0 34 Replenish 8. end item @ 8. factory 1.0 + 1 33 8. produce end item - alt 1 1.0 + 2 35 Ship 8. end item @ 8. factory 1.0 +Upstream pegging of operationplan with id 37 with quantity 1.0 of 'Replenish 8. end item @ 8. factory': + 0 37 Replenish 8. end item @ 8. factory 1.0 + 1 36 8. produce end item - alt 1 1.0 +Downstream pegging of operationplan with id 37 with quantity 1.0 of 'Replenish 8. end item @ 8. factory': + 0 37 Replenish 8. end item @ 8. factory 1.0 + 1 36 8. produce end item - alt 1 1.0 + 2 38 Ship 8. end item @ 8. factory 1.0 +Upstream pegging of operationplan with id 40 with quantity 8.0 of 'Replenish 8. end item @ 8. factory': + 0 40 Replenish 8. end item @ 8. factory 8.0 + 1 39 8. produce end item - alt 2 8.0 + 2 41 8. produce end item - alt 2 - step B 8.0 + 2 42 8. produce end item - alt 2 - step A 8.0 + 3 43 8. produce intermediate item 8.0 + 4 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 +Downstream pegging of operationplan with id 40 with quantity 8.0 of 'Replenish 8. end item @ 8. factory': + 0 40 Replenish 8. end item @ 8. factory 8.0 + 1 39 8. produce end item - alt 2 8.0 + 2 41 8. produce end item - alt 2 - step B 8.0 + 3 45 Ship 8. end item @ 8. factory 8.0 + 2 42 8. produce end item - alt 2 - step A 8.0 +Upstream pegging of operationplan with id 11 with quantity 1.0 of 'Ship 1. end item @ 1. factory': + 0 11 Ship 1. end item @ 1. factory 1.0 + 1 8 1. produce end item 1.0 + 2 9 1. produce intermediate item 2.0 + 3 10 Purchase 1. raw material @ 1. factory from 1. supplier 6.0 +Downstream pegging of operationplan with id 11 with quantity 1.0 of 'Ship 1. end item @ 1. factory': + 0 11 Ship 1. end item @ 1. factory 1.0 +Upstream pegging of operationplan with id 15 with quantity 1.0 of 'Ship 2. end item @ 2. factory': + 0 15 Ship 2. end item @ 2. factory 1.0 + 1 12 2. produce end item 1.0 + 2 13 2. produce intermediate item 2.0 + 3 14 Purchase 2. raw material @ 2. factory from 2. supplier 6.0 +Downstream pegging of operationplan with id 15 with quantity 1.0 of 'Ship 2. end item @ 2. factory': + 0 15 Ship 2. end item @ 2. factory 1.0 +Upstream pegging of operationplan with id 19 with quantity 1.0 of 'Ship 3. end item @ 3. factory': + 0 19 Ship 3. end item @ 3. factory 1.0 + 1 16 3. produce end item 1.0 + 2 17 3. produce intermediate item 2.0 + 3 18 Purchase 3. raw material @ 3. factory from 3. supplier 6.0 +Downstream pegging of operationplan with id 19 with quantity 1.0 of 'Ship 3. end item @ 3. factory': + 0 19 Ship 3. end item @ 3. factory 1.0 +Upstream pegging of operationplan with id 23 with quantity 1.0 of 'Ship 4. end item @ 4. factory': + 0 23 Ship 4. end item @ 4. factory 1.0 + 1 20 4. produce end item 1.0 + 2 21 4. produce intermediate item 2.0 + 3 22 Purchase 4. raw material @ 4. factory from 4. supplier 6.0 +Downstream pegging of operationplan with id 23 with quantity 1.0 of 'Ship 4. end item @ 4. factory': + 0 23 Ship 4. end item @ 4. factory 1.0 +Upstream pegging of operationplan with id 27 with quantity 1.0 of 'Ship 5. end item @ 5. factory': + 0 27 Ship 5. end item @ 5. factory 1.0 + 1 24 5. produce end item 1.0 + 2 25 5. produce intermediate item 2.0 + 3 26 Purchase 5. raw material @ 5. factory from 5. supplier 6.0 +Downstream pegging of operationplan with id 27 with quantity 1.0 of 'Ship 5. end item @ 5. factory': + 0 27 Ship 5. end item @ 5. factory 1.0 +Upstream pegging of operationplan with id 31 with quantity 1.0 of 'Ship 6. end item @ 6. factory': + 0 31 Ship 6. end item @ 6. factory 1.0 + 1 28 6. produce end item 1.0 + 2 29 6. produce intermediate item 2.0 + 3 30 Purchase 6. raw material @ 6. factory from 6. supplier 6.0 +Downstream pegging of operationplan with id 31 with quantity 1.0 of 'Ship 6. end item @ 6. factory': + 0 31 Ship 6. end item @ 6. factory 1.0 +Upstream pegging of operationplan with id 35 with quantity 1.0 of 'Ship 8. end item @ 8. factory': + 0 35 Ship 8. end item @ 8. factory 1.0 + 1 34 Replenish 8. end item @ 8. factory 1.0 + 2 33 8. produce end item - alt 1 1.0 +Downstream pegging of operationplan with id 35 with quantity 1.0 of 'Ship 8. end item @ 8. factory': + 0 35 Ship 8. end item @ 8. factory 1.0 +Upstream pegging of operationplan with id 38 with quantity 1.0 of 'Ship 8. end item @ 8. factory': + 0 38 Ship 8. end item @ 8. factory 1.0 + 1 37 Replenish 8. end item @ 8. factory 1.0 + 2 36 8. produce end item - alt 1 1.0 +Downstream pegging of operationplan with id 38 with quantity 1.0 of 'Ship 8. end item @ 8. factory': + 0 38 Ship 8. end item @ 8. factory 1.0 +Upstream pegging of operationplan with id 45 with quantity 8.0 of 'Ship 8. end item @ 8. factory': + 0 45 Ship 8. end item @ 8. factory 8.0 + 1 40 Replenish 8. end item @ 8. factory 8.0 + 2 39 8. produce end item - alt 2 8.0 + 3 41 8. produce end item - alt 2 - step B 8.0 + 3 42 8. produce end item - alt 2 - step A 8.0 + 4 43 8. produce intermediate item 8.0 + 5 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 +Downstream pegging of operationplan with id 45 with quantity 8.0 of 'Ship 8. end item @ 8. factory': + 0 45 Ship 8. end item @ 8. factory 8.0 +Upstream pegging of operationplan with id 49 with quantity 1.0 of 'Ship 9. end item @ 9. factory': + 0 49 Ship 9. end item @ 9. factory 1.0 + 1 46 9. produce end item 1.0 + 2 47 Purchase 9. component 2 @ 9. factory from 9. supplier 2.0 + 2 48 Purchase 9. component 1 @ 9. factory from 9. supplier 2.0 +Downstream pegging of operationplan with id 49 with quantity 1.0 of 'Ship 9. end item @ 9. factory': + 0 49 Ship 9. end item @ 9. factory 1.0 +Pegging of demand 1. order 1 with quantity 1.0: + 0 11 Ship 1. end item @ 1. factory 1.0 + 1 8 1. produce end item 1.0 + 2 9 1. produce intermediate item 2.0 + 3 10 Purchase 1. raw material @ 1. factory from 1. supplier 6.0 +Pegging of demand 2. order 1 with quantity 1.0: + 0 15 Ship 2. end item @ 2. factory 1.0 + 1 12 2. produce end item 1.0 + 2 13 2. produce intermediate item 2.0 + 3 14 Purchase 2. raw material @ 2. factory from 2. supplier 6.0 +Pegging of demand 3. order 1 with quantity 1.0: + 0 19 Ship 3. end item @ 3. factory 1.0 + 1 16 3. produce end item 1.0 + 2 17 3. produce intermediate item 2.0 + 3 18 Purchase 3. raw material @ 3. factory from 3. supplier 6.0 +Pegging of demand 4. order 1 with quantity 1.0: + 0 23 Ship 4. end item @ 4. factory 1.0 + 1 20 4. produce end item 1.0 + 2 21 4. produce intermediate item 2.0 + 3 22 Purchase 4. raw material @ 4. factory from 4. supplier 6.0 +Pegging of demand 5. order 1 with quantity 1.0: + 0 27 Ship 5. end item @ 5. factory 1.0 + 1 24 5. produce end item 1.0 + 2 25 5. produce intermediate item 2.0 + 3 26 Purchase 5. raw material @ 5. factory from 5. supplier 6.0 +Pegging of demand 6. order 1 with quantity 1.0: + 0 31 Ship 6. end item @ 6. factory 1.0 + 1 28 6. produce end item 1.0 + 2 29 6. produce intermediate item 2.0 + 3 30 Purchase 6. raw material @ 6. factory from 6. supplier 6.0 +Pegging of demand 8. order 1 with quantity 10.0: + 0 35 Ship 8. end item @ 8. factory 1.0 + 1 34 Replenish 8. end item @ 8. factory 1.0 + 2 33 8. produce end item - alt 1 1.0 + 0 38 Ship 8. end item @ 8. factory 1.0 + 1 37 Replenish 8. end item @ 8. factory 1.0 + 2 36 8. produce end item - alt 1 1.0 + 0 45 Ship 8. end item @ 8. factory 8.0 + 1 40 Replenish 8. end item @ 8. factory 8.0 + 2 39 8. produce end item - alt 2 8.0 + 3 41 8. produce end item - alt 2 - step B 8.0 + 3 42 8. produce end item - alt 2 - step A 8.0 + 4 43 8. produce intermediate item 8.0 + 5 44 Purchase 8. raw material @ 8. factory from 8. supplier 24.0 +Pegging of demand 9. order 1 with quantity 1.0: + 0 49 Ship 9. end item @ 9. factory 1.0 + 1 46 9. produce end item 1.0 + 2 47 Purchase 9. component 2 @ 9. factory from 9. supplier 2.0 + 2 48 Purchase 9. component 1 @ 9. factory from 9. supplier 2.0 diff --git a/test/pegging_12/pegging_12.xml b/test/pegging_12/pegging_12.xml new file mode 100644 index 0000000000..e2b98f9c28 --- /dev/null +++ b/test/pegging_12/pegging_12.xml @@ -0,0 +1,741 @@ + + + Test model for fixed flows + + This test verifies the solver behavior with offsets on + material flows: + - 1: simplest case + - 2: same as 1, but with a working + hour calendar hour calendar + - 3: same as 1, but with a negative offset + on the raw material consumption + - 4: same as 2, but with a negative + offset on the raw material consumption + - 5: same as 1, but with a + negative offset on the producing material + - 6: same as 1, but with a + positive offset on the consuming material + - 7: test approved, confirmed + and completed manufacturing orders + - 8: case with alternate and routing operations + - 9: case with material being consumed at the end + + 2020-01-01T00:00:00 + + + + + + + P3D + + + + 1 + P3D + + + + -2 + + + + + + + + 0 + + + + + + 10 + + 1 + + + + + + + P1D + + + + 1 + P3D + + + + -3 + + + + + + + + + + P7D + + + + + + + 1 + 2020-01-03T00:00:00 + 11 + + + + + + + + + + 1 + + + + + + + + + + + + P3D + + + + 1 + P3D + + + + -2 + + + + + + + + 0 + + + + + + 10 + + 1 + + + + + + + P1D + + + + 1 + P3D + + + + -3 + + + + + + + + + + P7D + + + + + + + 1 + 2020-01-03T00:00:00 + 11 + + + + + + + + + + + P3D + + + + 1 + P3D + + + + -2 + + + + + + + + 0 + + + + + + 10 + + 1 + + + + + + + P1D + + + + 1 + P3D + + + + -P1D + -3 + + + + + + + + + + P7D + + + + + + + 1 + 2020-01-03T00:00:00 + 11 + + + + + + + + + + 1 + + + + + + + + + + + + P3D + + + + 1 + P3D + + + + -2 + + + + + + + + 0 + + + + + + 10 + + 1 + + + + + + + P1D + + + + 1 + P3D + + + + -P1D + -3 + + + + + + + + + + P7D + + + + + + + 1 + 2020-01-03T00:00:00 + 11 + + + + + + + + + + + P3D + + + + 1 + -P1D + + + + -2 + + + + + + + + 0 + + + + + + 10 + + 1 + + + + + + + P1D + + + + 1 + -P1D + + + + -3 + + + + + + + + + + P7D + + + + + + + 1 + 2020-01-03T00:00:00 + 11 + + + + + + + + + + + P3D + + + + 1 + -P1D + + + + -2 + + + + + + + + 0 + + + + + + 10 + + 1 + + + + + + + P1D + + + + 1 + -P1D + + + + -3 + P1D + + + + + + + + + + P7D + + + + + + + 1 + 2020-01-03T00:00:00 + 11 + + + + + + + + + + + P3D + + + + 1 + P3D + + + + -2 + + + + + + + + 0 + + + + + + 10 + + 1 + + + + + + + P1D + + + + 1 + P3D + + + + -3 + + + + + + + + + + P7D + + + + + + + + + + + + + 4 + approved + 2020-01-03T00:00:00 + + + + 4 + confirmed + 2020-02-03T00:00:00 + + + + 4 + completed + 2019-12-03T00:00:00 + + + + + + + + + 1 + + + + 1 + P3D + + + + + + + 1 + + 1 + + + + + + + 2 + + + + + + + + -1 + + + + 2 + + + + + + + + 1 + P4D + + + + + + + 1 + + 1 + + + + 2 + + + + + + + P1D + + + + 1 + P3D + + + + -3 + + + + + + + + + + P7D + + + + + + + 10 + 2020-01-10T00:00:00 + 11 + + + + + + + + + + + P3D + + + + 1 + P3D + + + + -2 + + + + -P1D + -2 + + + + + + + 10 + + 1 + + + + + + + + + + P0D + + + + P10D + + + + + + + 1 + 2020-01-01T00:00:00 + 11 + + + + + + + diff --git a/test/pegging_3/pegging_3.1.expect b/test/pegging_3/pegging_3.1.expect new file mode 100644 index 0000000000..8d6c0c87f2 --- /dev/null +++ b/test/pegging_3/pegging_3.1.expect @@ -0,0 +1,496 @@ +Upstream pegging of operationplan with id 1 with quantity 15.0 of 'Purchase component 1 for option A @ location 1 from My supplier': + 0 1 Purchase component 1 for option A @ location 1 from My supplier 15.0 +Downstream pegging of operationplan with id 1 with quantity 15.0 of 'Purchase component 1 for option A @ location 1 from My supplier': + 0 1 Purchase component 1 for option A @ location 1 from My supplier 15.0 + 1 2 make item 1 option A 15.0 + 2 3 Ship item 1 @ location 1 30.0 +Upstream pegging of operationplan with id 4 with quantity 35.0 of 'Purchase component 1 for option B @ location 1 from My supplier': + 0 4 Purchase component 1 for option B @ location 1 from My supplier 35.0 +Downstream pegging of operationplan with id 4 with quantity 35.0 of 'Purchase component 1 for option B @ location 1 from My supplier': + 0 4 Purchase component 1 for option B @ location 1 from My supplier 35.0 + 1 5 make item 1 option B 35.0 + 2 3 Ship item 1 @ location 1 35.0 +Upstream pegging of operationplan with id 6 with quantity 7.0 of 'Purchase component 1 for option C @ location 1 from My supplier': + 0 6 Purchase component 1 for option C @ location 1 from My supplier 7.0 +Downstream pegging of operationplan with id 6 with quantity 7.0 of 'Purchase component 1 for option C @ location 1 from My supplier': + 0 6 Purchase component 1 for option C @ location 1 from My supplier 7.0 + 1 7 make item 1 option C 7.0 + 2 3 Ship item 1 @ location 1 35.0 +Upstream pegging of operationplan with id 8 with quantity 30.0 of 'Purchase component 2 for option A @ location 2 from My supplier': + 0 8 Purchase component 2 for option A @ location 2 from My supplier 30.0 +Downstream pegging of operationplan with id 8 with quantity 30.0 of 'Purchase component 2 for option A @ location 2 from My supplier': + 0 8 Purchase component 2 for option A @ location 2 from My supplier 30.0 + 1 9 make item 2 option A 30.0 +Upstream pegging of operationplan with id 10 with quantity 35.0 of 'Purchase component 2 for option B @ location 2 from My supplier': + 0 10 Purchase component 2 for option B @ location 2 from My supplier 35.0 +Downstream pegging of operationplan with id 10 with quantity 35.0 of 'Purchase component 2 for option B @ location 2 from My supplier': + 0 10 Purchase component 2 for option B @ location 2 from My supplier 35.0 + 1 11 make item 2 option B 35.0 +Upstream pegging of operationplan with id 12 with quantity 35.0 of 'Purchase component 2 for option C @ location 2 from My supplier': + 0 12 Purchase component 2 for option C @ location 2 from My supplier 35.0 +Downstream pegging of operationplan with id 12 with quantity 35.0 of 'Purchase component 2 for option C @ location 2 from My supplier': + 0 12 Purchase component 2 for option C @ location 2 from My supplier 35.0 + 1 13 make item 2 option C 35.0 +Upstream pegging of operationplan with id 14 with quantity 30.0 of 'Purchase component 3 for option A @ location 3 from My supplier': + 0 14 Purchase component 3 for option A @ location 3 from My supplier 30.0 +Downstream pegging of operationplan with id 14 with quantity 30.0 of 'Purchase component 3 for option A @ location 3 from My supplier': + 0 14 Purchase component 3 for option A @ location 3 from My supplier 30.0 + 1 15 make item 3 option A 30.0 +Upstream pegging of operationplan with id 16 with quantity 35.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 16 Purchase component 3 for option B @ location 3 from My supplier 35.0 +Downstream pegging of operationplan with id 16 with quantity 35.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 16 Purchase component 3 for option B @ location 3 from My supplier 35.0 + 1 17 make item 3 option B 35.0 +Upstream pegging of operationplan with id 18 with quantity 50.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 18 Purchase component 3 for option B @ location 3 from My supplier 50.0 +Downstream pegging of operationplan with id 18 with quantity 50.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 18 Purchase component 3 for option B @ location 3 from My supplier 50.0 + 1 19 make item 3 option B 50.0 +Upstream pegging of operationplan with id 20 with quantity 25.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 20 Purchase component 3 for option B @ location 3 from My supplier 25.0 +Downstream pegging of operationplan with id 20 with quantity 25.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 20 Purchase component 3 for option B @ location 3 from My supplier 25.0 + 1 21 make item 3 option B 25.0 +Upstream pegging of operationplan with id 22 with quantity 20.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 22 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Downstream pegging of operationplan with id 22 with quantity 20.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 22 Purchase component 3 for option B @ location 3 from My supplier 20.0 + 1 23 make item 3 option B 20.0 +Upstream pegging of operationplan with id 24 with quantity 20.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 24 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Downstream pegging of operationplan with id 24 with quantity 20.0 of 'Purchase component 3 for option B @ location 3 from My supplier': + 0 24 Purchase component 3 for option B @ location 3 from My supplier 20.0 + 1 25 make item 3 option B 20.0 +Upstream pegging of operationplan with id 26 with quantity 35.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 26 Purchase component 3 for option C @ location 3 from My supplier 35.0 +Downstream pegging of operationplan with id 26 with quantity 35.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 26 Purchase component 3 for option C @ location 3 from My supplier 35.0 + 1 27 make item 3 option C 35.0 +Upstream pegging of operationplan with id 28 with quantity 50.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 28 Purchase component 3 for option C @ location 3 from My supplier 50.0 +Downstream pegging of operationplan with id 28 with quantity 50.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 28 Purchase component 3 for option C @ location 3 from My supplier 50.0 + 1 29 make item 3 option C 50.0 +Upstream pegging of operationplan with id 30 with quantity 75.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 30 Purchase component 3 for option C @ location 3 from My supplier 75.0 +Downstream pegging of operationplan with id 30 with quantity 75.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 30 Purchase component 3 for option C @ location 3 from My supplier 75.0 + 1 31 make item 3 option C 75.0 +Upstream pegging of operationplan with id 32 with quantity 80.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 32 Purchase component 3 for option C @ location 3 from My supplier 80.0 +Downstream pegging of operationplan with id 32 with quantity 80.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 32 Purchase component 3 for option C @ location 3 from My supplier 80.0 + 1 33 make item 3 option C 80.0 +Upstream pegging of operationplan with id 34 with quantity 80.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 34 Purchase component 3 for option C @ location 3 from My supplier 80.0 +Downstream pegging of operationplan with id 34 with quantity 80.0 of 'Purchase component 3 for option C @ location 3 from My supplier': + 0 34 Purchase component 3 for option C @ location 3 from My supplier 80.0 + 1 35 make item 3 option C 80.0 +Upstream pegging of operationplan with id 36 with quantity 20.0 of 'Purchase component 4 for option A @ location 4 from My supplier': + 0 36 Purchase component 4 for option A @ location 4 from My supplier 20.0 +Downstream pegging of operationplan with id 36 with quantity 20.0 of 'Purchase component 4 for option A @ location 4 from My supplier': + 0 36 Purchase component 4 for option A @ location 4 from My supplier 20.0 + 1 37 make item 4 option A 20.0 +Upstream pegging of operationplan with id 38 with quantity 10.0 of 'Purchase component 4 for option B @ location 4 from My supplier': + 0 38 Purchase component 4 for option B @ location 4 from My supplier 10.0 +Downstream pegging of operationplan with id 38 with quantity 10.0 of 'Purchase component 4 for option B @ location 4 from My supplier': + 0 38 Purchase component 4 for option B @ location 4 from My supplier 10.0 + 1 39 make item 4 option B 10.0 +Upstream pegging of operationplan with id 40 with quantity 30.0 of 'Purchase component 4 for option B @ location 4 from My supplier': + 0 40 Purchase component 4 for option B @ location 4 from My supplier 30.0 +Downstream pegging of operationplan with id 40 with quantity 30.0 of 'Purchase component 4 for option B @ location 4 from My supplier': + 0 40 Purchase component 4 for option B @ location 4 from My supplier 30.0 + 1 41 make item 4 option B 30.0 +Upstream pegging of operationplan with id 42 with quantity 10.0 of 'Purchase component 4 for option C @ location 4 from My supplier': + 0 42 Purchase component 4 for option C @ location 4 from My supplier 10.0 +Downstream pegging of operationplan with id 42 with quantity 10.0 of 'Purchase component 4 for option C @ location 4 from My supplier': + 0 42 Purchase component 4 for option C @ location 4 from My supplier 10.0 + 1 43 make item 4 option C 10.0 +Upstream pegging of operationplan with id 44 with quantity 30.0 of 'Purchase component 4 for option C @ location 4 from My supplier': + 0 44 Purchase component 4 for option C @ location 4 from My supplier 30.0 +Downstream pegging of operationplan with id 44 with quantity 30.0 of 'Purchase component 4 for option C @ location 4 from My supplier': + 0 44 Purchase component 4 for option C @ location 4 from My supplier 30.0 + 1 45 make item 4 option C 30.0 +Upstream pegging of operationplan with id 3 with quantity 100.0 of 'Ship item 1 @ location 1': + 0 3 Ship item 1 @ location 1 100.0 + 1 2 make item 1 option A 15.0 + 2 1 Purchase component 1 for option A @ location 1 from My supplier 15.0 + 1 5 make item 1 option B 35.0 + 2 4 Purchase component 1 for option B @ location 1 from My supplier 35.0 + 1 7 make item 1 option C 7.0 + 2 6 Purchase component 1 for option C @ location 1 from My supplier 7.0 +Downstream pegging of operationplan with id 3 with quantity 100.0 of 'Ship item 1 @ location 1': + 0 3 Ship item 1 @ location 1 100.0 +Upstream pegging of operationplan with id 46 with quantity 200.0 of 'Ship item 2 @ location 2': + 0 46 Ship item 2 @ location 2 200.0 + 1 47 make item 2 100.0 + 2 13 make item 2 option C 35.0 + 3 12 Purchase component 2 for option C @ location 2 from My supplier 35.0 + 2 11 make item 2 option B 35.0 + 3 10 Purchase component 2 for option B @ location 2 from My supplier 35.0 + 2 9 make item 2 option A 30.0 + 3 8 Purchase component 2 for option A @ location 2 from My supplier 30.0 +Downstream pegging of operationplan with id 46 with quantity 200.0 of 'Ship item 2 @ location 2': + 0 46 Ship item 2 @ location 2 200.0 +Upstream pegging of operationplan with id 48 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 48 Ship item 3 @ location 3 100.0 + 1 49 make item 3 100.0 + 2 27 make item 3 option C 35.0 + 3 26 Purchase component 3 for option C @ location 3 from My supplier 35.0 + 2 17 make item 3 option B 35.0 + 3 16 Purchase component 3 for option B @ location 3 from My supplier 35.0 + 2 15 make item 3 option A 30.0 + 3 14 Purchase component 3 for option A @ location 3 from My supplier 30.0 +Downstream pegging of operationplan with id 48 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 48 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 50 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 50 Ship item 3 @ location 3 100.0 + 1 51 make item 3 100.0 + 2 29 make item 3 option C 50.0 + 3 28 Purchase component 3 for option C @ location 3 from My supplier 50.0 + 2 19 make item 3 option B 50.0 + 3 18 Purchase component 3 for option B @ location 3 from My supplier 50.0 +Downstream pegging of operationplan with id 50 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 50 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 52 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 52 Ship item 3 @ location 3 100.0 + 1 53 make item 3 100.0 + 2 31 make item 3 option C 75.0 + 3 30 Purchase component 3 for option C @ location 3 from My supplier 75.0 + 2 21 make item 3 option B 25.0 + 3 20 Purchase component 3 for option B @ location 3 from My supplier 25.0 +Downstream pegging of operationplan with id 52 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 52 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 54 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 54 Ship item 3 @ location 3 100.0 + 1 55 make item 3 100.0 + 2 33 make item 3 option C 80.0 + 3 32 Purchase component 3 for option C @ location 3 from My supplier 80.0 + 2 23 make item 3 option B 20.0 + 3 22 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Downstream pegging of operationplan with id 54 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 54 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 56 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 56 Ship item 3 @ location 3 100.0 + 1 57 make item 3 100.0 + 2 35 make item 3 option C 80.0 + 3 34 Purchase component 3 for option C @ location 3 from My supplier 80.0 + 2 25 make item 3 option B 20.0 + 3 24 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Downstream pegging of operationplan with id 56 with quantity 100.0 of 'Ship item 3 @ location 3': + 0 56 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 58 with quantity 40.0 of 'Ship item 4 @ location 4': + 0 58 Ship item 4 @ location 4 40.0 + 1 59 make item 4 20.0 + 2 43 make item 4 option C 10.0 + 3 42 Purchase component 4 for option C @ location 4 from My supplier 10.0 + 2 39 make item 4 option B 10.0 + 3 38 Purchase component 4 for option B @ location 4 from My supplier 10.0 +Downstream pegging of operationplan with id 58 with quantity 40.0 of 'Ship item 4 @ location 4': + 0 58 Ship item 4 @ location 4 40.0 +Upstream pegging of operationplan with id 60 with quantity 160.0 of 'Ship item 4 @ location 4': + 0 60 Ship item 4 @ location 4 160.0 + 1 61 make item 4 80.0 + 2 45 make item 4 option C 30.0 + 3 44 Purchase component 4 for option C @ location 4 from My supplier 30.0 + 2 41 make item 4 option B 30.0 + 3 40 Purchase component 4 for option B @ location 4 from My supplier 30.0 + 2 37 make item 4 option A 20.0 + 3 36 Purchase component 4 for option A @ location 4 from My supplier 20.0 +Downstream pegging of operationplan with id 60 with quantity 160.0 of 'Ship item 4 @ location 4': + 0 60 Ship item 4 @ location 4 160.0 +Upstream pegging of operationplan with id 62 with quantity 100.0 of 'make item 1': + 0 62 make item 1 100.0 + 1 7 make item 1 option C 7.0 + 2 6 Purchase component 1 for option C @ location 1 from My supplier 7.0 + 1 5 make item 1 option B 35.0 + 2 4 Purchase component 1 for option B @ location 1 from My supplier 35.0 + 1 2 make item 1 option A 15.0 + 2 1 Purchase component 1 for option A @ location 1 from My supplier 15.0 +Downstream pegging of operationplan with id 62 with quantity 100.0 of 'make item 1': + 0 62 make item 1 100.0 + 1 7 make item 1 option C 7.0 + 2 3 Ship item 1 @ location 1 35.0 + 1 5 make item 1 option B 35.0 + 2 3 Ship item 1 @ location 1 35.0 + 1 2 make item 1 option A 15.0 + 2 3 Ship item 1 @ location 1 30.0 +Upstream pegging of operationplan with id 2 with quantity 15.0 of 'make item 1 option A': + 0 2 make item 1 option A 15.0 + 1 1 Purchase component 1 for option A @ location 1 from My supplier 15.0 +Downstream pegging of operationplan with id 2 with quantity 15.0 of 'make item 1 option A': + 0 2 make item 1 option A 15.0 + 1 3 Ship item 1 @ location 1 30.0 +Upstream pegging of operationplan with id 5 with quantity 35.0 of 'make item 1 option B': + 0 5 make item 1 option B 35.0 + 1 4 Purchase component 1 for option B @ location 1 from My supplier 35.0 +Downstream pegging of operationplan with id 5 with quantity 35.0 of 'make item 1 option B': + 0 5 make item 1 option B 35.0 + 1 3 Ship item 1 @ location 1 35.0 +Upstream pegging of operationplan with id 7 with quantity 7.0 of 'make item 1 option C': + 0 7 make item 1 option C 7.0 + 1 6 Purchase component 1 for option C @ location 1 from My supplier 7.0 +Downstream pegging of operationplan with id 7 with quantity 7.0 of 'make item 1 option C': + 0 7 make item 1 option C 7.0 + 1 3 Ship item 1 @ location 1 35.0 +Upstream pegging of operationplan with id 47 with quantity 100.0 of 'make item 2': + 0 47 make item 2 100.0 + 1 13 make item 2 option C 35.0 + 2 12 Purchase component 2 for option C @ location 2 from My supplier 35.0 + 1 11 make item 2 option B 35.0 + 2 10 Purchase component 2 for option B @ location 2 from My supplier 35.0 + 1 9 make item 2 option A 30.0 + 2 8 Purchase component 2 for option A @ location 2 from My supplier 30.0 +Downstream pegging of operationplan with id 47 with quantity 100.0 of 'make item 2': + 0 47 make item 2 100.0 + 1 13 make item 2 option C 35.0 + 1 11 make item 2 option B 35.0 + 1 9 make item 2 option A 30.0 + 1 46 Ship item 2 @ location 2 200.0 +Upstream pegging of operationplan with id 9 with quantity 30.0 of 'make item 2 option A': + 0 9 make item 2 option A 30.0 + 1 8 Purchase component 2 for option A @ location 2 from My supplier 30.0 +Downstream pegging of operationplan with id 9 with quantity 30.0 of 'make item 2 option A': + 0 9 make item 2 option A 30.0 +Upstream pegging of operationplan with id 11 with quantity 35.0 of 'make item 2 option B': + 0 11 make item 2 option B 35.0 + 1 10 Purchase component 2 for option B @ location 2 from My supplier 35.0 +Downstream pegging of operationplan with id 11 with quantity 35.0 of 'make item 2 option B': + 0 11 make item 2 option B 35.0 +Upstream pegging of operationplan with id 13 with quantity 35.0 of 'make item 2 option C': + 0 13 make item 2 option C 35.0 + 1 12 Purchase component 2 for option C @ location 2 from My supplier 35.0 +Downstream pegging of operationplan with id 13 with quantity 35.0 of 'make item 2 option C': + 0 13 make item 2 option C 35.0 +Upstream pegging of operationplan with id 49 with quantity 100.0 of 'make item 3': + 0 49 make item 3 100.0 + 1 27 make item 3 option C 35.0 + 2 26 Purchase component 3 for option C @ location 3 from My supplier 35.0 + 1 17 make item 3 option B 35.0 + 2 16 Purchase component 3 for option B @ location 3 from My supplier 35.0 + 1 15 make item 3 option A 30.0 + 2 14 Purchase component 3 for option A @ location 3 from My supplier 30.0 +Downstream pegging of operationplan with id 49 with quantity 100.0 of 'make item 3': + 0 49 make item 3 100.0 + 1 27 make item 3 option C 35.0 + 1 17 make item 3 option B 35.0 + 1 15 make item 3 option A 30.0 + 1 48 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 51 with quantity 100.0 of 'make item 3': + 0 51 make item 3 100.0 + 1 29 make item 3 option C 50.0 + 2 28 Purchase component 3 for option C @ location 3 from My supplier 50.0 + 1 19 make item 3 option B 50.0 + 2 18 Purchase component 3 for option B @ location 3 from My supplier 50.0 +Downstream pegging of operationplan with id 51 with quantity 100.0 of 'make item 3': + 0 51 make item 3 100.0 + 1 29 make item 3 option C 50.0 + 1 19 make item 3 option B 50.0 + 1 50 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 53 with quantity 100.0 of 'make item 3': + 0 53 make item 3 100.0 + 1 31 make item 3 option C 75.0 + 2 30 Purchase component 3 for option C @ location 3 from My supplier 75.0 + 1 21 make item 3 option B 25.0 + 2 20 Purchase component 3 for option B @ location 3 from My supplier 25.0 +Downstream pegging of operationplan with id 53 with quantity 100.0 of 'make item 3': + 0 53 make item 3 100.0 + 1 31 make item 3 option C 75.0 + 1 21 make item 3 option B 25.0 + 1 52 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 55 with quantity 100.0 of 'make item 3': + 0 55 make item 3 100.0 + 1 33 make item 3 option C 80.0 + 2 32 Purchase component 3 for option C @ location 3 from My supplier 80.0 + 1 23 make item 3 option B 20.0 + 2 22 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Downstream pegging of operationplan with id 55 with quantity 100.0 of 'make item 3': + 0 55 make item 3 100.0 + 1 33 make item 3 option C 80.0 + 1 23 make item 3 option B 20.0 + 1 54 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 57 with quantity 100.0 of 'make item 3': + 0 57 make item 3 100.0 + 1 35 make item 3 option C 80.0 + 2 34 Purchase component 3 for option C @ location 3 from My supplier 80.0 + 1 25 make item 3 option B 20.0 + 2 24 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Downstream pegging of operationplan with id 57 with quantity 100.0 of 'make item 3': + 0 57 make item 3 100.0 + 1 35 make item 3 option C 80.0 + 1 25 make item 3 option B 20.0 + 1 56 Ship item 3 @ location 3 100.0 +Upstream pegging of operationplan with id 15 with quantity 30.0 of 'make item 3 option A': + 0 15 make item 3 option A 30.0 + 1 14 Purchase component 3 for option A @ location 3 from My supplier 30.0 +Downstream pegging of operationplan with id 15 with quantity 30.0 of 'make item 3 option A': + 0 15 make item 3 option A 30.0 +Upstream pegging of operationplan with id 17 with quantity 35.0 of 'make item 3 option B': + 0 17 make item 3 option B 35.0 + 1 16 Purchase component 3 for option B @ location 3 from My supplier 35.0 +Downstream pegging of operationplan with id 17 with quantity 35.0 of 'make item 3 option B': + 0 17 make item 3 option B 35.0 +Upstream pegging of operationplan with id 19 with quantity 50.0 of 'make item 3 option B': + 0 19 make item 3 option B 50.0 + 1 18 Purchase component 3 for option B @ location 3 from My supplier 50.0 +Downstream pegging of operationplan with id 19 with quantity 50.0 of 'make item 3 option B': + 0 19 make item 3 option B 50.0 +Upstream pegging of operationplan with id 21 with quantity 25.0 of 'make item 3 option B': + 0 21 make item 3 option B 25.0 + 1 20 Purchase component 3 for option B @ location 3 from My supplier 25.0 +Downstream pegging of operationplan with id 21 with quantity 25.0 of 'make item 3 option B': + 0 21 make item 3 option B 25.0 +Upstream pegging of operationplan with id 23 with quantity 20.0 of 'make item 3 option B': + 0 23 make item 3 option B 20.0 + 1 22 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Downstream pegging of operationplan with id 23 with quantity 20.0 of 'make item 3 option B': + 0 23 make item 3 option B 20.0 +Upstream pegging of operationplan with id 25 with quantity 20.0 of 'make item 3 option B': + 0 25 make item 3 option B 20.0 + 1 24 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Downstream pegging of operationplan with id 25 with quantity 20.0 of 'make item 3 option B': + 0 25 make item 3 option B 20.0 +Upstream pegging of operationplan with id 27 with quantity 35.0 of 'make item 3 option C': + 0 27 make item 3 option C 35.0 + 1 26 Purchase component 3 for option C @ location 3 from My supplier 35.0 +Downstream pegging of operationplan with id 27 with quantity 35.0 of 'make item 3 option C': + 0 27 make item 3 option C 35.0 +Upstream pegging of operationplan with id 29 with quantity 50.0 of 'make item 3 option C': + 0 29 make item 3 option C 50.0 + 1 28 Purchase component 3 for option C @ location 3 from My supplier 50.0 +Downstream pegging of operationplan with id 29 with quantity 50.0 of 'make item 3 option C': + 0 29 make item 3 option C 50.0 +Upstream pegging of operationplan with id 31 with quantity 75.0 of 'make item 3 option C': + 0 31 make item 3 option C 75.0 + 1 30 Purchase component 3 for option C @ location 3 from My supplier 75.0 +Downstream pegging of operationplan with id 31 with quantity 75.0 of 'make item 3 option C': + 0 31 make item 3 option C 75.0 +Upstream pegging of operationplan with id 33 with quantity 80.0 of 'make item 3 option C': + 0 33 make item 3 option C 80.0 + 1 32 Purchase component 3 for option C @ location 3 from My supplier 80.0 +Downstream pegging of operationplan with id 33 with quantity 80.0 of 'make item 3 option C': + 0 33 make item 3 option C 80.0 +Upstream pegging of operationplan with id 35 with quantity 80.0 of 'make item 3 option C': + 0 35 make item 3 option C 80.0 + 1 34 Purchase component 3 for option C @ location 3 from My supplier 80.0 +Downstream pegging of operationplan with id 35 with quantity 80.0 of 'make item 3 option C': + 0 35 make item 3 option C 80.0 +Upstream pegging of operationplan with id 59 with quantity 20.0 of 'make item 4': + 0 59 make item 4 20.0 + 1 43 make item 4 option C 10.0 + 2 42 Purchase component 4 for option C @ location 4 from My supplier 10.0 + 1 39 make item 4 option B 10.0 + 2 38 Purchase component 4 for option B @ location 4 from My supplier 10.0 +Downstream pegging of operationplan with id 59 with quantity 20.0 of 'make item 4': + 0 59 make item 4 20.0 + 1 43 make item 4 option C 10.0 + 1 39 make item 4 option B 10.0 + 1 58 Ship item 4 @ location 4 40.0 +Upstream pegging of operationplan with id 61 with quantity 80.0 of 'make item 4': + 0 61 make item 4 80.0 + 1 45 make item 4 option C 30.0 + 2 44 Purchase component 4 for option C @ location 4 from My supplier 30.0 + 1 41 make item 4 option B 30.0 + 2 40 Purchase component 4 for option B @ location 4 from My supplier 30.0 + 1 37 make item 4 option A 20.0 + 2 36 Purchase component 4 for option A @ location 4 from My supplier 20.0 +Downstream pegging of operationplan with id 61 with quantity 80.0 of 'make item 4': + 0 61 make item 4 80.0 + 1 45 make item 4 option C 30.0 + 1 41 make item 4 option B 30.0 + 1 37 make item 4 option A 20.0 + 1 60 Ship item 4 @ location 4 160.0 +Upstream pegging of operationplan with id 37 with quantity 20.0 of 'make item 4 option A': + 0 37 make item 4 option A 20.0 + 1 36 Purchase component 4 for option A @ location 4 from My supplier 20.0 +Downstream pegging of operationplan with id 37 with quantity 20.0 of 'make item 4 option A': + 0 37 make item 4 option A 20.0 +Upstream pegging of operationplan with id 39 with quantity 10.0 of 'make item 4 option B': + 0 39 make item 4 option B 10.0 + 1 38 Purchase component 4 for option B @ location 4 from My supplier 10.0 +Downstream pegging of operationplan with id 39 with quantity 10.0 of 'make item 4 option B': + 0 39 make item 4 option B 10.0 +Upstream pegging of operationplan with id 41 with quantity 30.0 of 'make item 4 option B': + 0 41 make item 4 option B 30.0 + 1 40 Purchase component 4 for option B @ location 4 from My supplier 30.0 +Downstream pegging of operationplan with id 41 with quantity 30.0 of 'make item 4 option B': + 0 41 make item 4 option B 30.0 +Upstream pegging of operationplan with id 43 with quantity 10.0 of 'make item 4 option C': + 0 43 make item 4 option C 10.0 + 1 42 Purchase component 4 for option C @ location 4 from My supplier 10.0 +Downstream pegging of operationplan with id 43 with quantity 10.0 of 'make item 4 option C': + 0 43 make item 4 option C 10.0 +Upstream pegging of operationplan with id 45 with quantity 30.0 of 'make item 4 option C': + 0 45 make item 4 option C 30.0 + 1 44 Purchase component 4 for option C @ location 4 from My supplier 30.0 +Downstream pegging of operationplan with id 45 with quantity 30.0 of 'make item 4 option C': + 0 45 make item 4 option C 30.0 +Pegging of demand order 1 for item 1 with quantity 100.0: + 0 3 Ship item 1 @ location 1 100.0 + 1 2 make item 1 option A 15.0 + 2 1 Purchase component 1 for option A @ location 1 from My supplier 15.0 + 1 5 make item 1 option B 35.0 + 2 4 Purchase component 1 for option B @ location 1 from My supplier 35.0 + 1 7 make item 1 option C 7.0 + 2 6 Purchase component 1 for option C @ location 1 from My supplier 7.0 +Pegging of demand order 1 for item 2 with quantity 200.0: + 0 46 Ship item 2 @ location 2 200.0 + 1 47 make item 2 100.0 + 2 13 make item 2 option C 35.0 + 3 12 Purchase component 2 for option C @ location 2 from My supplier 35.0 + 2 11 make item 2 option B 35.0 + 3 10 Purchase component 2 for option B @ location 2 from My supplier 35.0 + 2 9 make item 2 option A 30.0 + 3 8 Purchase component 2 for option A @ location 2 from My supplier 30.0 +Pegging of demand order 1 for item 3 with quantity 100.0: + 0 48 Ship item 3 @ location 3 100.0 + 1 49 make item 3 100.0 + 2 27 make item 3 option C 35.0 + 3 26 Purchase component 3 for option C @ location 3 from My supplier 35.0 + 2 17 make item 3 option B 35.0 + 3 16 Purchase component 3 for option B @ location 3 from My supplier 35.0 + 2 15 make item 3 option A 30.0 + 3 14 Purchase component 3 for option A @ location 3 from My supplier 30.0 +Pegging of demand order 1 for item 4 with quantity 200.0: + 0 58 Ship item 4 @ location 4 40.0 + 1 59 make item 4 20.0 + 2 43 make item 4 option C 10.0 + 3 42 Purchase component 4 for option C @ location 4 from My supplier 10.0 + 2 39 make item 4 option B 10.0 + 3 38 Purchase component 4 for option B @ location 4 from My supplier 10.0 + 0 60 Ship item 4 @ location 4 160.0 + 1 61 make item 4 80.0 + 2 45 make item 4 option C 30.0 + 3 44 Purchase component 4 for option C @ location 4 from My supplier 30.0 + 2 41 make item 4 option B 30.0 + 3 40 Purchase component 4 for option B @ location 4 from My supplier 30.0 + 2 37 make item 4 option A 20.0 + 3 36 Purchase component 4 for option A @ location 4 from My supplier 20.0 +Pegging of demand order 2 for item 3 with quantity 100.0: + 0 50 Ship item 3 @ location 3 100.0 + 1 51 make item 3 100.0 + 2 29 make item 3 option C 50.0 + 3 28 Purchase component 3 for option C @ location 3 from My supplier 50.0 + 2 19 make item 3 option B 50.0 + 3 18 Purchase component 3 for option B @ location 3 from My supplier 50.0 +Pegging of demand order 3 for item 3 with quantity 100.0: + 0 52 Ship item 3 @ location 3 100.0 + 1 53 make item 3 100.0 + 2 31 make item 3 option C 75.0 + 3 30 Purchase component 3 for option C @ location 3 from My supplier 75.0 + 2 21 make item 3 option B 25.0 + 3 20 Purchase component 3 for option B @ location 3 from My supplier 25.0 +Pegging of demand order 4 for item 3 with quantity 100.0: + 0 54 Ship item 3 @ location 3 100.0 + 1 55 make item 3 100.0 + 2 33 make item 3 option C 80.0 + 3 32 Purchase component 3 for option C @ location 3 from My supplier 80.0 + 2 23 make item 3 option B 20.0 + 3 22 Purchase component 3 for option B @ location 3 from My supplier 20.0 +Pegging of demand order 5 for item 3 with quantity 100.0: + 0 56 Ship item 3 @ location 3 100.0 + 1 57 make item 3 100.0 + 2 35 make item 3 option C 80.0 + 3 34 Purchase component 3 for option C @ location 3 from My supplier 80.0 + 2 25 make item 3 option B 20.0 + 3 24 Purchase component 3 for option B @ location 3 from My supplier 20.0 diff --git a/test/pegging_3/pegging_3.xml b/test/pegging_3/pegging_3.xml new file mode 100644 index 0000000000..31385fc419 --- /dev/null +++ b/test/pegging_3/pegging_3.xml @@ -0,0 +1,589 @@ + + + Test model for operation_split + + This test verifies how an operation of type operation_split divides + the demand over different operations. + There are a number of test situations being investigated, both in the + constrained and unconstrained plan. + 1) Simple case of split across 3 operations. + Producing flows exist at the child operations. + 2) Simple case of split across 3 operations. + Producing flow exists at the top operation. + 3) Case with date effective split across 3 operations. + In some periods some alternates are not available. + And at different times a different percentage split is applied + across the alternates. + 4) Case with lot size constraints on the child operations. + The desired split is impossible to achieve exactly. + + TODO Criticality computation is flawed for cases 2, 3 and 4. They have the outgoing flow on the parent operation. + + 2014-01-01T00:00:00 + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + 2 + + + + -1 + + + 0 + P3D + + 30 + + + + + + + + 1 + + + + -1 + + + + + + 1 + + + 0 + P5D + + 35 + + + + + + + + 5 + + + + -1 + + + + + + 1 + + + 0 + P10D + + 35 + + + + + + + + + + + P7D + + + + + P2D + + + + + P1D + + + + + + + 100 + 2014-01-01T00:00:00 + 1 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + 1 + + + + + + -1 + + + 0 + P3D + + 30 + + + + + + + + -1 + + + + + + 1 + + + 0 + P5D + + 35 + + + + + + + + -1 + + + + + + 1 + + + 0 + P10D + + 35 + + + + + + + + + + + P7D + + + + + P2D + + + + + P1D + + + + + + + 200 + 2014-01-01T00:00:00 + 1 + + + + + + + + + + + + + + + + + + + + + + 1 + + + + + + + + + + 1 + + + + + + -1 + + + 0 + P3D + + 30 + 2014-08-01T00:00:00 + + + + + + + + -1 + + + + + + 1 + + + 0 + P5D + + 35 + 2014-09-01T00:00:00 + + + + + + + + -1 + + + + + + 1 + + + 0 + P10D + + 35 + 2014-09-01T00:00:00 + + + + 25 + 2014-09-01T00:00:00 + 2014-10-01T00:00:00 + + + + 75 + 2014-09-01T00:00:00 + 2014-10-01T00:00:00 + + + + 20 + 2014-10-01T00:00:00 + + + + 80 + 2014-10-01T00:00:00 + + + + + + + + + + + P7D + + + + + P2D + + + + + P1D + + + + + + + 100 + 2014-07-01T00:00:00 + 1 + + + + + 100 + 2014-08-01T00:00:00 + 1 + + + + + 100 + 2014-09-01T00:00:00 + 1 + + + + + 100 + 2014-10-01T00:00:00 + 1 + + + + + 100 + 2014-11-01T00:00:00 + 1 + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + 1 + + + + + + -1 + + + 1 + 10 + P3D + + 30 + + + + + + + + -1 + + + + + + 1 + + + 1 + 10 + P5D + + 35 + + + + + + + + -1 + + + + + + 1 + + + 1 + 10 + P10D + + 35 + + + + + + + + + + + P7D + + + + + P2D + + + + + P1D + + + + + + + 200 + 2014-01-01T00:00:00 + 1 + + + + + + + diff --git a/test/pegging_4/pegging_4.1.expect b/test/pegging_4/pegging_4.1.expect new file mode 100644 index 0000000000..2f96c90be9 --- /dev/null +++ b/test/pegging_4/pegging_4.1.expect @@ -0,0 +1,774 @@ +Upstream pegging of operationplan with id 1 with quantity 13.0 of '2. make item qty 10-20': + 0 2 Replenish 2. item @ warehouse 13.0 + 1 1 2. make item qty 10-20 13.0 +Downstream pegging of operationplan with id 1 with quantity 13.0 of '2. make item qty 10-20': + 0 2 Replenish 2. item @ warehouse 13.0 + 1 1 2. make item qty 10-20 13.0 + 2 3 Ship 2. item @ warehouse 13.0 +Upstream pegging of operationplan with id 4 with quantity 30.0 of '2. make item qty 20+': + 0 5 Replenish 2. item @ warehouse 30.0 + 1 4 2. make item qty 20+ 30.0 +Downstream pegging of operationplan with id 4 with quantity 30.0 of '2. make item qty 20+': + 0 5 Replenish 2. item @ warehouse 30.0 + 1 4 2. make item qty 20+ 30.0 + 2 6 Ship 2. item @ warehouse 30.0 +Upstream pegging of operationplan with id 7 with quantity 5.0 of '2. make item qty 5-10': + 0 8 Replenish 2. item @ warehouse 5.0 + 1 7 2. make item qty 5-10 5.0 +Downstream pegging of operationplan with id 7 with quantity 5.0 of '2. make item qty 5-10': + 0 8 Replenish 2. item @ warehouse 5.0 + 1 7 2. make item qty 5-10 5.0 + 2 3 Ship 2. item @ warehouse 2.0 + 2 9 Ship 2. item @ warehouse 3.0 +Upstream pegging of operationplan with id 10 with quantity 10.0 of '3. producing': + 0 11 Replenish 3. item @ warehouse 10.0 + 1 10 3. producing 10.0 + 2 3. component @ warehouse Inventory 3. component @ warehouse 5.0 + 2 12 Purchase 3. component @ warehouse from 3. Supplier 5.0 +Downstream pegging of operationplan with id 10 with quantity 10.0 of '3. producing': + 0 11 Replenish 3. item @ warehouse 10.0 + 1 10 3. producing 10.0 + 2 13 Ship 3. item @ warehouse 10.0 +Upstream pegging of operationplan with id 14 with quantity 1.0 of '4. alternate B': + 0 15 Replenish 4. item @ warehouse 1.0 + 1 14 4. alternate B 1.0 +Downstream pegging of operationplan with id 14 with quantity 1.0 of '4. alternate B': + 0 15 Replenish 4. item @ warehouse 1.0 + 1 14 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 17 with quantity 1.0 of '4. alternate B': + 0 18 Replenish 4. item @ warehouse 1.0 + 1 17 4. alternate B 1.0 +Downstream pegging of operationplan with id 17 with quantity 1.0 of '4. alternate B': + 0 18 Replenish 4. item @ warehouse 1.0 + 1 17 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 19 with quantity 1.0 of '4. alternate B': + 0 20 Replenish 4. item @ warehouse 1.0 + 1 19 4. alternate B 1.0 +Downstream pegging of operationplan with id 19 with quantity 1.0 of '4. alternate B': + 0 20 Replenish 4. item @ warehouse 1.0 + 1 19 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 21 with quantity 1.0 of '4. alternate B': + 0 22 Replenish 4. item @ warehouse 1.0 + 1 21 4. alternate B 1.0 +Downstream pegging of operationplan with id 21 with quantity 1.0 of '4. alternate B': + 0 22 Replenish 4. item @ warehouse 1.0 + 1 21 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 23 with quantity 1.0 of '4. alternate B': + 0 24 Replenish 4. item @ warehouse 1.0 + 1 23 4. alternate B 1.0 +Downstream pegging of operationplan with id 23 with quantity 1.0 of '4. alternate B': + 0 24 Replenish 4. item @ warehouse 1.0 + 1 23 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 25 with quantity 1.0 of '4. alternate B': + 0 26 Replenish 4. item @ warehouse 1.0 + 1 25 4. alternate B 1.0 +Downstream pegging of operationplan with id 25 with quantity 1.0 of '4. alternate B': + 0 26 Replenish 4. item @ warehouse 1.0 + 1 25 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 27 with quantity 1.0 of '4. alternate B': + 0 28 Replenish 4. item @ warehouse 1.0 + 1 27 4. alternate B 1.0 +Downstream pegging of operationplan with id 27 with quantity 1.0 of '4. alternate B': + 0 28 Replenish 4. item @ warehouse 1.0 + 1 27 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 29 with quantity 1.0 of '4. alternate B': + 0 30 Replenish 4. item @ warehouse 1.0 + 1 29 4. alternate B 1.0 +Downstream pegging of operationplan with id 29 with quantity 1.0 of '4. alternate B': + 0 30 Replenish 4. item @ warehouse 1.0 + 1 29 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 31 with quantity 1.0 of '4. alternate B': + 0 32 Replenish 4. item @ warehouse 1.0 + 1 31 4. alternate B 1.0 +Downstream pegging of operationplan with id 31 with quantity 1.0 of '4. alternate B': + 0 32 Replenish 4. item @ warehouse 1.0 + 1 31 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 33 with quantity 1.0 of '4. alternate B': + 0 34 Replenish 4. item @ warehouse 1.0 + 1 33 4. alternate B 1.0 +Downstream pegging of operationplan with id 33 with quantity 1.0 of '4. alternate B': + 0 34 Replenish 4. item @ warehouse 1.0 + 1 33 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 35 with quantity 20.0 of '5. make item qty 10-20': + 0 36 Replenish 5. item @ warehouse 20.0 + 1 35 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 35 with quantity 20.0 of '5. make item qty 10-20': + 0 36 Replenish 5. item @ warehouse 20.0 + 1 35 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 38 with quantity 20.0 of '5. make item qty 10-20': + 0 39 Replenish 5. item @ warehouse 20.0 + 1 38 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 38 with quantity 20.0 of '5. make item qty 10-20': + 0 39 Replenish 5. item @ warehouse 20.0 + 1 38 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 40 with quantity 20.0 of '5. make item qty 10-20': + 0 41 Replenish 5. item @ warehouse 20.0 + 1 40 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 40 with quantity 20.0 of '5. make item qty 10-20': + 0 41 Replenish 5. item @ warehouse 20.0 + 1 40 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 42 with quantity 20.0 of '5. make item qty 10-20': + 0 43 Replenish 5. item @ warehouse 20.0 + 1 42 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 42 with quantity 20.0 of '5. make item qty 10-20': + 0 43 Replenish 5. item @ warehouse 20.0 + 1 42 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 44 with quantity 20.0 of '5. make item qty 10-20': + 0 45 Replenish 5. item @ warehouse 20.0 + 1 44 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 44 with quantity 20.0 of '5. make item qty 10-20': + 0 45 Replenish 5. item @ warehouse 20.0 + 1 44 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 46 with quantity 20.0 of '5. make item qty 10-20': + 0 47 Replenish 5. item @ warehouse 20.0 + 1 46 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 46 with quantity 20.0 of '5. make item qty 10-20': + 0 47 Replenish 5. item @ warehouse 20.0 + 1 46 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 48 with quantity 20.0 of '5. make item qty 10-20': + 0 49 Replenish 5. item @ warehouse 20.0 + 1 48 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 48 with quantity 20.0 of '5. make item qty 10-20': + 0 49 Replenish 5. item @ warehouse 20.0 + 1 48 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 50 with quantity 20.0 of '5. make item qty 10-20': + 0 51 Replenish 5. item @ warehouse 20.0 + 1 50 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 50 with quantity 20.0 of '5. make item qty 10-20': + 0 51 Replenish 5. item @ warehouse 20.0 + 1 50 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 52 with quantity 20.0 of '5. make item qty 10-20': + 0 53 Replenish 5. item @ warehouse 20.0 + 1 52 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 52 with quantity 20.0 of '5. make item qty 10-20': + 0 53 Replenish 5. item @ warehouse 20.0 + 1 52 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 54 with quantity 20.0 of '5. make item qty 10-20': + 0 55 Replenish 5. item @ warehouse 20.0 + 1 54 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 54 with quantity 20.0 of '5. make item qty 10-20': + 0 55 Replenish 5. item @ warehouse 20.0 + 1 54 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 56 with quantity 10.0 of '5. make item qty 5-10': + 0 57 Replenish 5. item @ warehouse 10.0 + 1 56 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 56 with quantity 10.0 of '5. make item qty 5-10': + 0 57 Replenish 5. item @ warehouse 10.0 + 1 56 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 58 with quantity 10.0 of '5. make item qty 5-10': + 0 59 Replenish 5. item @ warehouse 10.0 + 1 58 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 58 with quantity 10.0 of '5. make item qty 5-10': + 0 59 Replenish 5. item @ warehouse 10.0 + 1 58 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 60 with quantity 10.0 of '5. make item qty 5-10': + 0 61 Replenish 5. item @ warehouse 10.0 + 1 60 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 60 with quantity 10.0 of '5. make item qty 5-10': + 0 61 Replenish 5. item @ warehouse 10.0 + 1 60 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 62 with quantity 10.0 of '5. make item qty 5-10': + 0 63 Replenish 5. item @ warehouse 10.0 + 1 62 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 62 with quantity 10.0 of '5. make item qty 5-10': + 0 63 Replenish 5. item @ warehouse 10.0 + 1 62 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 64 with quantity 10.0 of '5. make item qty 5-10': + 0 65 Replenish 5. item @ warehouse 10.0 + 1 64 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 64 with quantity 10.0 of '5. make item qty 5-10': + 0 65 Replenish 5. item @ warehouse 10.0 + 1 64 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 66 with quantity 10.0 of '5. make item qty 5-10': + 0 67 Replenish 5. item @ warehouse 10.0 + 1 66 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 66 with quantity 10.0 of '5. make item qty 5-10': + 0 67 Replenish 5. item @ warehouse 10.0 + 1 66 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 68 with quantity 10.0 of '5. make item qty 5-10': + 0 69 Replenish 5. item @ warehouse 10.0 + 1 68 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 68 with quantity 10.0 of '5. make item qty 5-10': + 0 69 Replenish 5. item @ warehouse 10.0 + 1 68 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 70 with quantity 10.0 of '5. make item qty 5-10': + 0 71 Replenish 5. item @ warehouse 10.0 + 1 70 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 70 with quantity 10.0 of '5. make item qty 5-10': + 0 71 Replenish 5. item @ warehouse 10.0 + 1 70 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 72 with quantity 10.0 of '5. make item qty 5-10': + 0 73 Replenish 5. item @ warehouse 10.0 + 1 72 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 72 with quantity 10.0 of '5. make item qty 5-10': + 0 73 Replenish 5. item @ warehouse 10.0 + 1 72 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 74 with quantity 10.0 of '5. make item qty 5-10': + 0 75 Replenish 5. item @ warehouse 10.0 + 1 74 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 74 with quantity 10.0 of '5. make item qty 5-10': + 0 75 Replenish 5. item @ warehouse 10.0 + 1 74 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 3. component @ warehouse with quantity 5.0 of 'Inventory 3. component @ warehouse': + 0 3. component @ warehouse Inventory 3. component @ warehouse 5.0 +Downstream pegging of operationplan with id 3. component @ warehouse with quantity 5.0 of 'Inventory 3. component @ warehouse': + 0 3. component @ warehouse Inventory 3. component @ warehouse 5.0 + 1 11 Replenish 3. item @ warehouse 5.0 + 2 10 3. producing 5.0 + 3 13 Ship 3. item @ warehouse 5.0 +Upstream pegging of operationplan with id 12 with quantity 5.0 of 'Purchase 3. component @ warehouse from 3. Supplier': + 0 12 Purchase 3. component @ warehouse from 3. Supplier 5.0 +Downstream pegging of operationplan with id 12 with quantity 5.0 of 'Purchase 3. component @ warehouse from 3. Supplier': + 0 12 Purchase 3. component @ warehouse from 3. Supplier 5.0 + 1 11 Replenish 3. item @ warehouse 5.0 + 2 10 3. producing 5.0 + 3 13 Ship 3. item @ warehouse 5.0 +Upstream pegging of operationplan with id 76 with quantity 10.0 of 'Purchase item 2 @ warehouse from MySupplier': + 0 77 Replenish item 2 @ warehouse 10.0 + 1 76 Purchase item 2 @ warehouse from MySupplier 10.0 +Downstream pegging of operationplan with id 76 with quantity 10.0 of 'Purchase item 2 @ warehouse from MySupplier': + 0 77 Replenish item 2 @ warehouse 10.0 + 1 76 Purchase item 2 @ warehouse from MySupplier 10.0 + 2 78 Ship item 2 @ warehouse 10.0 +Upstream pegging of operationplan with id 79 with quantity 10.0 of 'Purchase item 4 @ warehouse from MySupplier valid from 2009-02-10T00:00:00': + 0 80 Replenish item 4 @ warehouse 10.0 + 1 79 Purchase item 4 @ warehouse from MySupplier valid from 2009-02-10T00:00:00 10.0 +Downstream pegging of operationplan with id 79 with quantity 10.0 of 'Purchase item 4 @ warehouse from MySupplier valid from 2009-02-10T00:00:00': + 0 80 Replenish item 4 @ warehouse 10.0 + 1 79 Purchase item 4 @ warehouse from MySupplier valid from 2009-02-10T00:00:00 10.0 + 2 81 Ship item 4 @ warehouse 10.0 +Upstream pegging of operationplan with id 8 with quantity 5.0 of 'Replenish 2. item @ warehouse': + 0 8 Replenish 2. item @ warehouse 5.0 + 1 7 2. make item qty 5-10 5.0 +Downstream pegging of operationplan with id 8 with quantity 5.0 of 'Replenish 2. item @ warehouse': + 0 8 Replenish 2. item @ warehouse 5.0 + 1 7 2. make item qty 5-10 5.0 + 2 3 Ship 2. item @ warehouse 2.0 + 2 9 Ship 2. item @ warehouse 3.0 +Upstream pegging of operationplan with id 2 with quantity 13.0 of 'Replenish 2. item @ warehouse': + 0 2 Replenish 2. item @ warehouse 13.0 + 1 1 2. make item qty 10-20 13.0 +Downstream pegging of operationplan with id 2 with quantity 13.0 of 'Replenish 2. item @ warehouse': + 0 2 Replenish 2. item @ warehouse 13.0 + 1 1 2. make item qty 10-20 13.0 + 2 3 Ship 2. item @ warehouse 13.0 +Upstream pegging of operationplan with id 5 with quantity 30.0 of 'Replenish 2. item @ warehouse': + 0 5 Replenish 2. item @ warehouse 30.0 + 1 4 2. make item qty 20+ 30.0 +Downstream pegging of operationplan with id 5 with quantity 30.0 of 'Replenish 2. item @ warehouse': + 0 5 Replenish 2. item @ warehouse 30.0 + 1 4 2. make item qty 20+ 30.0 + 2 6 Ship 2. item @ warehouse 30.0 +Upstream pegging of operationplan with id 11 with quantity 10.0 of 'Replenish 3. item @ warehouse': + 0 11 Replenish 3. item @ warehouse 10.0 + 1 10 3. producing 10.0 + 2 3. component @ warehouse Inventory 3. component @ warehouse 5.0 + 2 12 Purchase 3. component @ warehouse from 3. Supplier 5.0 +Downstream pegging of operationplan with id 11 with quantity 10.0 of 'Replenish 3. item @ warehouse': + 0 11 Replenish 3. item @ warehouse 10.0 + 1 10 3. producing 10.0 + 2 13 Ship 3. item @ warehouse 10.0 +Upstream pegging of operationplan with id 15 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 15 Replenish 4. item @ warehouse 1.0 + 1 14 4. alternate B 1.0 +Downstream pegging of operationplan with id 15 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 15 Replenish 4. item @ warehouse 1.0 + 1 14 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 18 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 18 Replenish 4. item @ warehouse 1.0 + 1 17 4. alternate B 1.0 +Downstream pegging of operationplan with id 18 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 18 Replenish 4. item @ warehouse 1.0 + 1 17 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 20 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 20 Replenish 4. item @ warehouse 1.0 + 1 19 4. alternate B 1.0 +Downstream pegging of operationplan with id 20 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 20 Replenish 4. item @ warehouse 1.0 + 1 19 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 22 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 22 Replenish 4. item @ warehouse 1.0 + 1 21 4. alternate B 1.0 +Downstream pegging of operationplan with id 22 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 22 Replenish 4. item @ warehouse 1.0 + 1 21 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 24 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 24 Replenish 4. item @ warehouse 1.0 + 1 23 4. alternate B 1.0 +Downstream pegging of operationplan with id 24 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 24 Replenish 4. item @ warehouse 1.0 + 1 23 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 26 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 26 Replenish 4. item @ warehouse 1.0 + 1 25 4. alternate B 1.0 +Downstream pegging of operationplan with id 26 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 26 Replenish 4. item @ warehouse 1.0 + 1 25 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 28 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 28 Replenish 4. item @ warehouse 1.0 + 1 27 4. alternate B 1.0 +Downstream pegging of operationplan with id 28 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 28 Replenish 4. item @ warehouse 1.0 + 1 27 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 30 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 30 Replenish 4. item @ warehouse 1.0 + 1 29 4. alternate B 1.0 +Downstream pegging of operationplan with id 30 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 30 Replenish 4. item @ warehouse 1.0 + 1 29 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 32 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 32 Replenish 4. item @ warehouse 1.0 + 1 31 4. alternate B 1.0 +Downstream pegging of operationplan with id 32 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 32 Replenish 4. item @ warehouse 1.0 + 1 31 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 34 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 34 Replenish 4. item @ warehouse 1.0 + 1 33 4. alternate B 1.0 +Downstream pegging of operationplan with id 34 with quantity 1.0 of 'Replenish 4. item @ warehouse': + 0 34 Replenish 4. item @ warehouse 1.0 + 1 33 4. alternate B 1.0 + 2 16 Ship 4. item @ warehouse 1.0 +Upstream pegging of operationplan with id 36 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 36 Replenish 5. item @ warehouse 20.0 + 1 35 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 36 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 36 Replenish 5. item @ warehouse 20.0 + 1 35 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 39 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 39 Replenish 5. item @ warehouse 20.0 + 1 38 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 39 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 39 Replenish 5. item @ warehouse 20.0 + 1 38 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 41 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 41 Replenish 5. item @ warehouse 20.0 + 1 40 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 41 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 41 Replenish 5. item @ warehouse 20.0 + 1 40 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 43 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 43 Replenish 5. item @ warehouse 20.0 + 1 42 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 43 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 43 Replenish 5. item @ warehouse 20.0 + 1 42 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 45 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 45 Replenish 5. item @ warehouse 20.0 + 1 44 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 45 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 45 Replenish 5. item @ warehouse 20.0 + 1 44 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 47 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 47 Replenish 5. item @ warehouse 20.0 + 1 46 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 47 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 47 Replenish 5. item @ warehouse 20.0 + 1 46 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 49 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 49 Replenish 5. item @ warehouse 20.0 + 1 48 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 49 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 49 Replenish 5. item @ warehouse 20.0 + 1 48 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 51 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 51 Replenish 5. item @ warehouse 20.0 + 1 50 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 51 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 51 Replenish 5. item @ warehouse 20.0 + 1 50 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 53 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 53 Replenish 5. item @ warehouse 20.0 + 1 52 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 53 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 53 Replenish 5. item @ warehouse 20.0 + 1 52 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 55 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 55 Replenish 5. item @ warehouse 20.0 + 1 54 5. make item qty 10-20 20.0 +Downstream pegging of operationplan with id 55 with quantity 20.0 of 'Replenish 5. item @ warehouse': + 0 55 Replenish 5. item @ warehouse 20.0 + 1 54 5. make item qty 10-20 20.0 + 2 37 Ship 5. item @ warehouse 20.0 +Upstream pegging of operationplan with id 57 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 57 Replenish 5. item @ warehouse 10.0 + 1 56 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 57 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 57 Replenish 5. item @ warehouse 10.0 + 1 56 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 59 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 59 Replenish 5. item @ warehouse 10.0 + 1 58 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 59 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 59 Replenish 5. item @ warehouse 10.0 + 1 58 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 61 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 61 Replenish 5. item @ warehouse 10.0 + 1 60 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 61 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 61 Replenish 5. item @ warehouse 10.0 + 1 60 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 63 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 63 Replenish 5. item @ warehouse 10.0 + 1 62 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 63 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 63 Replenish 5. item @ warehouse 10.0 + 1 62 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 65 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 65 Replenish 5. item @ warehouse 10.0 + 1 64 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 65 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 65 Replenish 5. item @ warehouse 10.0 + 1 64 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 67 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 67 Replenish 5. item @ warehouse 10.0 + 1 66 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 67 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 67 Replenish 5. item @ warehouse 10.0 + 1 66 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 69 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 69 Replenish 5. item @ warehouse 10.0 + 1 68 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 69 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 69 Replenish 5. item @ warehouse 10.0 + 1 68 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 71 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 71 Replenish 5. item @ warehouse 10.0 + 1 70 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 71 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 71 Replenish 5. item @ warehouse 10.0 + 1 70 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 73 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 73 Replenish 5. item @ warehouse 10.0 + 1 72 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 73 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 73 Replenish 5. item @ warehouse 10.0 + 1 72 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 75 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 75 Replenish 5. item @ warehouse 10.0 + 1 74 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 75 with quantity 10.0 of 'Replenish 5. item @ warehouse': + 0 75 Replenish 5. item @ warehouse 10.0 + 1 74 5. make item qty 5-10 10.0 + 2 37 Ship 5. item @ warehouse 10.0 +Upstream pegging of operationplan with id 77 with quantity 10.0 of 'Replenish item 2 @ warehouse': + 0 77 Replenish item 2 @ warehouse 10.0 + 1 76 Purchase item 2 @ warehouse from MySupplier 10.0 +Downstream pegging of operationplan with id 77 with quantity 10.0 of 'Replenish item 2 @ warehouse': + 0 77 Replenish item 2 @ warehouse 10.0 + 1 76 Purchase item 2 @ warehouse from MySupplier 10.0 + 2 78 Ship item 2 @ warehouse 10.0 +Upstream pegging of operationplan with id 80 with quantity 10.0 of 'Replenish item 4 @ warehouse': + 0 80 Replenish item 4 @ warehouse 10.0 + 1 79 Purchase item 4 @ warehouse from MySupplier valid from 2009-02-10T00:00:00 10.0 +Downstream pegging of operationplan with id 80 with quantity 10.0 of 'Replenish item 4 @ warehouse': + 0 80 Replenish item 4 @ warehouse 10.0 + 1 79 Purchase item 4 @ warehouse from MySupplier valid from 2009-02-10T00:00:00 10.0 + 2 81 Ship item 4 @ warehouse 10.0 +Upstream pegging of operationplan with id 9 with quantity 3.0 of 'Ship 2. item @ warehouse': + 0 9 Ship 2. item @ warehouse 3.0 + 1 8 Replenish 2. item @ warehouse 3.0 + 2 7 2. make item qty 5-10 3.0 +Downstream pegging of operationplan with id 9 with quantity 3.0 of 'Ship 2. item @ warehouse': + 0 9 Ship 2. item @ warehouse 3.0 +Upstream pegging of operationplan with id 3 with quantity 15.0 of 'Ship 2. item @ warehouse': + 0 3 Ship 2. item @ warehouse 15.0 + 1 8 Replenish 2. item @ warehouse 2.0 + 2 7 2. make item qty 5-10 2.0 + 1 2 Replenish 2. item @ warehouse 13.0 + 2 1 2. make item qty 10-20 13.0 +Downstream pegging of operationplan with id 3 with quantity 15.0 of 'Ship 2. item @ warehouse': + 0 3 Ship 2. item @ warehouse 15.0 +Upstream pegging of operationplan with id 6 with quantity 30.0 of 'Ship 2. item @ warehouse': + 0 6 Ship 2. item @ warehouse 30.0 + 1 5 Replenish 2. item @ warehouse 30.0 + 2 4 2. make item qty 20+ 30.0 +Downstream pegging of operationplan with id 6 with quantity 30.0 of 'Ship 2. item @ warehouse': + 0 6 Ship 2. item @ warehouse 30.0 +Upstream pegging of operationplan with id 13 with quantity 10.0 of 'Ship 3. item @ warehouse': + 0 13 Ship 3. item @ warehouse 10.0 + 1 11 Replenish 3. item @ warehouse 10.0 + 2 10 3. producing 10.0 + 3 3. component @ warehouse Inventory 3. component @ warehouse 5.0 + 3 12 Purchase 3. component @ warehouse from 3. Supplier 5.0 +Downstream pegging of operationplan with id 13 with quantity 10.0 of 'Ship 3. item @ warehouse': + 0 13 Ship 3. item @ warehouse 10.0 +Upstream pegging of operationplan with id 16 with quantity 10.0 of 'Ship 4. item @ warehouse': + 0 16 Ship 4. item @ warehouse 10.0 + 1 15 Replenish 4. item @ warehouse 1.0 + 2 14 4. alternate B 1.0 + 1 18 Replenish 4. item @ warehouse 1.0 + 2 17 4. alternate B 1.0 + 1 20 Replenish 4. item @ warehouse 1.0 + 2 19 4. alternate B 1.0 + 1 22 Replenish 4. item @ warehouse 1.0 + 2 21 4. alternate B 1.0 + 1 24 Replenish 4. item @ warehouse 1.0 + 2 23 4. alternate B 1.0 + 1 26 Replenish 4. item @ warehouse 1.0 + 2 25 4. alternate B 1.0 + 1 28 Replenish 4. item @ warehouse 1.0 + 2 27 4. alternate B 1.0 + 1 30 Replenish 4. item @ warehouse 1.0 + 2 29 4. alternate B 1.0 + 1 32 Replenish 4. item @ warehouse 1.0 + 2 31 4. alternate B 1.0 + 1 34 Replenish 4. item @ warehouse 1.0 + 2 33 4. alternate B 1.0 +Downstream pegging of operationplan with id 16 with quantity 10.0 of 'Ship 4. item @ warehouse': + 0 16 Ship 4. item @ warehouse 10.0 +Upstream pegging of operationplan with id 37 with quantity 300.0 of 'Ship 5. item @ warehouse': + 0 37 Ship 5. item @ warehouse 300.0 + 1 36 Replenish 5. item @ warehouse 20.0 + 2 35 5. make item qty 10-20 20.0 + 1 39 Replenish 5. item @ warehouse 20.0 + 2 38 5. make item qty 10-20 20.0 + 1 41 Replenish 5. item @ warehouse 20.0 + 2 40 5. make item qty 10-20 20.0 + 1 43 Replenish 5. item @ warehouse 20.0 + 2 42 5. make item qty 10-20 20.0 + 1 45 Replenish 5. item @ warehouse 20.0 + 2 44 5. make item qty 10-20 20.0 + 1 47 Replenish 5. item @ warehouse 20.0 + 2 46 5. make item qty 10-20 20.0 + 1 49 Replenish 5. item @ warehouse 20.0 + 2 48 5. make item qty 10-20 20.0 + 1 51 Replenish 5. item @ warehouse 20.0 + 2 50 5. make item qty 10-20 20.0 + 1 53 Replenish 5. item @ warehouse 20.0 + 2 52 5. make item qty 10-20 20.0 + 1 55 Replenish 5. item @ warehouse 20.0 + 2 54 5. make item qty 10-20 20.0 + 1 57 Replenish 5. item @ warehouse 10.0 + 2 56 5. make item qty 5-10 10.0 + 1 59 Replenish 5. item @ warehouse 10.0 + 2 58 5. make item qty 5-10 10.0 + 1 61 Replenish 5. item @ warehouse 10.0 + 2 60 5. make item qty 5-10 10.0 + 1 63 Replenish 5. item @ warehouse 10.0 + 2 62 5. make item qty 5-10 10.0 + 1 65 Replenish 5. item @ warehouse 10.0 + 2 64 5. make item qty 5-10 10.0 + 1 67 Replenish 5. item @ warehouse 10.0 + 2 66 5. make item qty 5-10 10.0 + 1 69 Replenish 5. item @ warehouse 10.0 + 2 68 5. make item qty 5-10 10.0 + 1 71 Replenish 5. item @ warehouse 10.0 + 2 70 5. make item qty 5-10 10.0 + 1 73 Replenish 5. item @ warehouse 10.0 + 2 72 5. make item qty 5-10 10.0 + 1 75 Replenish 5. item @ warehouse 10.0 + 2 74 5. make item qty 5-10 10.0 +Downstream pegging of operationplan with id 37 with quantity 300.0 of 'Ship 5. item @ warehouse': + 0 37 Ship 5. item @ warehouse 300.0 +Upstream pegging of operationplan with id 78 with quantity 10.0 of 'Ship item 2 @ warehouse': + 0 78 Ship item 2 @ warehouse 10.0 + 1 77 Replenish item 2 @ warehouse 10.0 + 2 76 Purchase item 2 @ warehouse from MySupplier 10.0 +Downstream pegging of operationplan with id 78 with quantity 10.0 of 'Ship item 2 @ warehouse': + 0 78 Ship item 2 @ warehouse 10.0 +Upstream pegging of operationplan with id 81 with quantity 10.0 of 'Ship item 4 @ warehouse': + 0 81 Ship item 4 @ warehouse 10.0 + 1 80 Replenish item 4 @ warehouse 10.0 + 2 79 Purchase item 4 @ warehouse from MySupplier valid from 2009-02-10T00:00:00 10.0 +Downstream pegging of operationplan with id 81 with quantity 10.0 of 'Ship item 4 @ warehouse': + 0 81 Ship item 4 @ warehouse 10.0 +Upstream pegging of operationplan with id 82 with quantity 10.0 of 'alternatives for making item 1': + 0 82 alternatives for making item 1 10.0 + 1 83 buy from supplier D 10.0 +Downstream pegging of operationplan with id 82 with quantity 10.0 of 'alternatives for making item 1': + 0 82 alternatives for making item 1 10.0 + 1 83 buy from supplier D 10.0 + 2 84 delivery 1 10.0 +Upstream pegging of operationplan with id 85 with quantity 20.0 of 'alternatives for making item 1': + 0 85 alternatives for making item 1 20.0 + 1 86 buy from supplier C 20.0 +Downstream pegging of operationplan with id 85 with quantity 20.0 of 'alternatives for making item 1': + 0 85 alternatives for making item 1 20.0 + 1 86 buy from supplier C 20.0 + 2 87 delivery 1 20.0 +Upstream pegging of operationplan with id 86 with quantity 20.0 of 'buy from supplier C': + 0 85 alternatives for making item 1 20.0 + 1 86 buy from supplier C 20.0 +Downstream pegging of operationplan with id 86 with quantity 20.0 of 'buy from supplier C': + 0 85 alternatives for making item 1 20.0 + 1 86 buy from supplier C 20.0 + 2 87 delivery 1 20.0 +Upstream pegging of operationplan with id 83 with quantity 10.0 of 'buy from supplier D': + 0 82 alternatives for making item 1 10.0 + 1 83 buy from supplier D 10.0 +Downstream pegging of operationplan with id 83 with quantity 10.0 of 'buy from supplier D': + 0 82 alternatives for making item 1 10.0 + 1 83 buy from supplier D 10.0 + 2 84 delivery 1 10.0 +Upstream pegging of operationplan with id 84 with quantity 10.0 of 'delivery 1': + 0 84 delivery 1 10.0 + 1 82 alternatives for making item 1 10.0 + 2 83 buy from supplier D 10.0 +Downstream pegging of operationplan with id 84 with quantity 10.0 of 'delivery 1': + 0 84 delivery 1 10.0 +Upstream pegging of operationplan with id 87 with quantity 20.0 of 'delivery 1': + 0 87 delivery 1 20.0 + 1 85 alternatives for making item 1 20.0 + 2 86 buy from supplier C 20.0 +Downstream pegging of operationplan with id 87 with quantity 20.0 of 'delivery 1': + 0 87 delivery 1 20.0 +Pegging of demand 2. item SO1 with quantity 3.0: + 0 9 Ship 2. item @ warehouse 3.0 + 1 8 Replenish 2. item @ warehouse 3.0 + 2 7 2. make item qty 5-10 3.0 +Pegging of demand 2. item SO2 with quantity 15.0: + 0 3 Ship 2. item @ warehouse 15.0 + 1 8 Replenish 2. item @ warehouse 2.0 + 2 7 2. make item qty 5-10 2.0 + 1 2 Replenish 2. item @ warehouse 13.0 + 2 1 2. make item qty 10-20 13.0 +Pegging of demand 2. item SO3 with quantity 30.0: + 0 6 Ship 2. item @ warehouse 30.0 + 1 5 Replenish 2. item @ warehouse 30.0 + 2 4 2. make item qty 20+ 30.0 +Pegging of demand 3. SO1 with quantity 10.0: + 0 13 Ship 3. item @ warehouse 10.0 + 1 11 Replenish 3. item @ warehouse 10.0 + 2 10 3. producing 10.0 + 3 3. component @ warehouse Inventory 3. component @ warehouse 5.0 + 3 12 Purchase 3. component @ warehouse from 3. Supplier 5.0 +Pegging of demand 4. SO1 with quantity 10.0: + 0 16 Ship 4. item @ warehouse 10.0 + 1 15 Replenish 4. item @ warehouse 1.0 + 2 14 4. alternate B 1.0 + 1 18 Replenish 4. item @ warehouse 1.0 + 2 17 4. alternate B 1.0 + 1 20 Replenish 4. item @ warehouse 1.0 + 2 19 4. alternate B 1.0 + 1 22 Replenish 4. item @ warehouse 1.0 + 2 21 4. alternate B 1.0 + 1 24 Replenish 4. item @ warehouse 1.0 + 2 23 4. alternate B 1.0 + 1 26 Replenish 4. item @ warehouse 1.0 + 2 25 4. alternate B 1.0 + 1 28 Replenish 4. item @ warehouse 1.0 + 2 27 4. alternate B 1.0 + 1 30 Replenish 4. item @ warehouse 1.0 + 2 29 4. alternate B 1.0 + 1 32 Replenish 4. item @ warehouse 1.0 + 2 31 4. alternate B 1.0 + 1 34 Replenish 4. item @ warehouse 1.0 + 2 33 4. alternate B 1.0 +Pegging of demand 5. item SO1 with quantity 300.0: + 0 37 Ship 5. item @ warehouse 300.0 + 1 36 Replenish 5. item @ warehouse 20.0 + 2 35 5. make item qty 10-20 20.0 + 1 39 Replenish 5. item @ warehouse 20.0 + 2 38 5. make item qty 10-20 20.0 + 1 41 Replenish 5. item @ warehouse 20.0 + 2 40 5. make item qty 10-20 20.0 + 1 43 Replenish 5. item @ warehouse 20.0 + 2 42 5. make item qty 10-20 20.0 + 1 45 Replenish 5. item @ warehouse 20.0 + 2 44 5. make item qty 10-20 20.0 + 1 47 Replenish 5. item @ warehouse 20.0 + 2 46 5. make item qty 10-20 20.0 + 1 49 Replenish 5. item @ warehouse 20.0 + 2 48 5. make item qty 10-20 20.0 + 1 51 Replenish 5. item @ warehouse 20.0 + 2 50 5. make item qty 10-20 20.0 + 1 53 Replenish 5. item @ warehouse 20.0 + 2 52 5. make item qty 10-20 20.0 + 1 55 Replenish 5. item @ warehouse 20.0 + 2 54 5. make item qty 10-20 20.0 + 1 57 Replenish 5. item @ warehouse 10.0 + 2 56 5. make item qty 5-10 10.0 + 1 59 Replenish 5. item @ warehouse 10.0 + 2 58 5. make item qty 5-10 10.0 + 1 61 Replenish 5. item @ warehouse 10.0 + 2 60 5. make item qty 5-10 10.0 + 1 63 Replenish 5. item @ warehouse 10.0 + 2 62 5. make item qty 5-10 10.0 + 1 65 Replenish 5. item @ warehouse 10.0 + 2 64 5. make item qty 5-10 10.0 + 1 67 Replenish 5. item @ warehouse 10.0 + 2 66 5. make item qty 5-10 10.0 + 1 69 Replenish 5. item @ warehouse 10.0 + 2 68 5. make item qty 5-10 10.0 + 1 71 Replenish 5. item @ warehouse 10.0 + 2 70 5. make item qty 5-10 10.0 + 1 73 Replenish 5. item @ warehouse 10.0 + 2 72 5. make item qty 5-10 10.0 + 1 75 Replenish 5. item @ warehouse 10.0 + 2 74 5. make item qty 5-10 10.0 +Pegging of demand item 2 SO1 with quantity 10.0: + 0 78 Ship item 2 @ warehouse 10.0 + 1 77 Replenish item 2 @ warehouse 10.0 + 2 76 Purchase item 2 @ warehouse from MySupplier 10.0 +Pegging of demand item 3 SO1 with quantity 10.0: +Pegging of demand item 4 SO1 with quantity 10.0: + 0 81 Ship item 4 @ warehouse 10.0 + 1 80 Replenish item 4 @ warehouse 10.0 + 2 79 Purchase item 4 @ warehouse from MySupplier valid from 2009-02-10T00:00:00 10.0 +Pegging of demand item 5 SO1 with quantity 10.0: +Pegging of demand order prio 1 for item 1 with quantity 20.0: + 0 87 delivery 1 20.0 + 1 85 alternatives for making item 1 20.0 + 2 86 buy from supplier C 20.0 +Pegging of demand order prio 2 for item 1 with quantity 10.0: + 0 84 delivery 1 10.0 + 1 82 alternatives for making item 1 10.0 + 2 83 buy from supplier D 10.0 diff --git a/test/pegging_4/pegging_4.xml b/test/pegging_4/pegging_4.xml new file mode 100644 index 0000000000..20f4fcc15d --- /dev/null +++ b/test/pegging_4/pegging_4.xml @@ -0,0 +1,483 @@ + + + + Test model for alternate selection + + This test verifies that alternates are searched and selected correctly. + Depending on the selection criterion this can be the alternate with the + lowest priority number, the alternate with the lowest cost or the + alternate with the lowest penalty value. + + 2009-01-01T00:00:00 + + + + 1 + + + + 1 + + + + + + 0.25 + + + + 1 + + + + + + + + 13 + + + + 2 + P3D + + + + + 2 + + + + + + P5D + P1D + + + + + + 11 + + + + + + P1D + P2D + + + 30 + P7D + + + fast suplier, but expensive... + 50 + P3D + + + + + + + + + 1 + + + + 1 + + + + + + + 1 + + + + 2 + + + + + + + 1 + + + + 3 + + + + + + + 1 + + + + 4 + + + + + + + + + + + + + 20 + 2009-01-11T00:00:00 + 1 + + + + + + -1 + + + + + + 10 + 10 + 2009-01-02T00:00:00 + 2 + + + + + + + + + + + + P2D + 1 + 2009-01-10T00:00:00 + + + + + + + 10 + 10 + 2009-01-31T00:00:00 + 2 + + + + + + + + + + + P14D + 1 + 2009-01-10T00:00:00 + + + + + + + 10 + 10 + 2009-01-31T00:00:00 + 2 + + + + + + + + + + + P14D + 1 + 2009-02-10T00:00:00 + + + + + + + 10 + 10 + 2009-01-31T00:00:00 + 2 + + + + + + + + + + + + P14D + 0 + + + + + + + 10 + 10 + 2009-01-31T00:00:00 + 2 + + + + + + + + + + + + + 1 + 5 + 10 + PT1H + PT60M + + + + + 2 + 10 + 20 + PT1H + PT50M + + + + + 20 + 3 + PT1H + PT40M + + + + + 3 + 3 + 2009-01-20T00:00:00 + + + + + 15 + 15 + 2009-02-20T00:00:00 + + + + + 30 + 30 + 2009-03-20T00:00:00 + + + + + + + + + + 1 + + + + -1 + + + PT1H + + + + + 2 + + + + -1 + + + P1D + P1D + + + + + + + + + + + + + + P70D + + + + + + + 10 + 10 + 2009-01-21T00:00:00 + + + + + + + + + + 1 + 30 + 1 + 1 + MINCOST + + + + 1 + + + PT1H + + + + + 2 + 5 + 1 + 1 + MINCOST + + + + 1 + + + PT1H + + + + + 3 + 20 + 1 + 1 + MINCOST + + + + 1 + + + PT1H + + + + + 10 + 10 + 2009-01-21T00:00:00 + + + + + + + + + + + 1 + 5 + 10 + PT1H + PT60M + + + + + 1 + 10 + 20 + PT1H + PT50M + + + + + 300 + 300 + 2009-01-20T00:00:00 + + + + + + + diff --git a/test/pegging_5/pegging_5.1.expect b/test/pegging_5/pegging_5.1.expect new file mode 100644 index 0000000000..a8ff3f16ea --- /dev/null +++ b/test/pegging_5/pegging_5.1.expect @@ -0,0 +1,1276 @@ +Upstream pegging of operationplan with id component A @ factory with quantity 20.0 of 'Inventory component A @ factory': + 0 component A @ factory Inventory component A @ factory 20.0 +Downstream pegging of operationplan with id component A @ factory with quantity 20.0 of 'Inventory component A @ factory': + 0 component A @ factory Inventory component A @ factory 20.0 + 1 1 assemble product 3.0 + 2 2 assemble product step C 3.0 + 2 3 assemble product step B 3.0 + 2 4 assemble product step A 3.0 + 2 5 Ship product @ factory 3.0 + 1 6 assemble product 1.0 + 2 7 assemble product step C 1.0 + 2 8 assemble product step B 1.0 + 2 9 assemble product step A 1.0 + 2 10 Ship product @ factory 1.0 + 1 11 assemble product 1.0 + 2 12 assemble product step C 1.0 + 2 13 assemble product step B 1.0 + 2 14 assemble product step A 1.0 + 2 15 Ship product @ factory 1.0 + 1 16 assemble product 1.0 + 2 17 assemble product step C 1.0 + 2 18 assemble product step B 1.0 + 2 19 assemble product step A 1.0 + 2 20 Ship product @ factory 1.0 + 1 21 assemble product 1.0 + 2 22 assemble product step C 1.0 + 2 23 assemble product step B 1.0 + 2 24 assemble product step A 1.0 + 2 25 Ship product @ factory 1.0 + 1 26 assemble product 1.0 + 2 27 assemble product step C 1.0 + 2 28 assemble product step B 1.0 + 2 29 assemble product step A 1.0 + 2 30 Ship product @ factory 1.0 + 1 31 assemble product 1.0 + 2 32 assemble product step C 1.0 + 2 33 assemble product step B 1.0 + 2 34 assemble product step A 1.0 + 2 35 Ship product @ factory 1.0 + 1 36 assemble product 1.0 + 2 37 assemble product step C 1.0 + 2 38 assemble product step B 1.0 + 2 39 assemble product step A 1.0 + 2 40 Ship product @ factory 1.0 + 1 41 assemble product 5.0 + 2 42 assemble product step C 5.0 + 2 43 assemble product step B 5.0 + 2 44 assemble product step A 5.0 + 2 45 Ship product @ factory 5.0 + 1 46 assemble product 5.0 + 2 47 assemble product step C 5.0 + 2 48 assemble product step B 5.0 + 2 49 assemble product step A 5.0 + 2 50 Ship product @ factory 5.0 +Upstream pegging of operationplan with id component D @ factory with quantity 30.0 of 'Inventory component D @ factory': + 0 component D @ factory Inventory component D @ factory 30.0 +Downstream pegging of operationplan with id component D @ factory with quantity 30.0 of 'Inventory component D @ factory': + 0 component D @ factory Inventory component D @ factory 30.0 + 1 51 assemble product 3.0 + 2 52 assemble product step C 3.0 + 2 53 assemble product step B 3.0 + 2 54 assemble product step A 3.0 + 2 55 Ship product @ factory 3.0 + 1 56 assemble product 1.0 + 2 57 assemble product step C 1.0 + 2 58 assemble product step B 1.0 + 2 59 assemble product step A 1.0 + 2 60 Ship product @ factory 1.0 + 1 61 assemble product 6.0 + 2 62 assemble product step C 6.0 + 2 63 assemble product step B 6.0 + 2 64 assemble product step A 6.0 + 2 65 Ship product @ factory 6.0 + 1 1 assemble product 3.0 + 2 2 assemble product step C 3.0 + 2 3 assemble product step B 3.0 + 2 4 assemble product step A 3.0 + 2 5 Ship product @ factory 3.0 + 1 6 assemble product 1.0 + 2 7 assemble product step C 1.0 + 2 8 assemble product step B 1.0 + 2 9 assemble product step A 1.0 + 2 10 Ship product @ factory 1.0 + 1 11 assemble product 1.0 + 2 12 assemble product step C 1.0 + 2 13 assemble product step B 1.0 + 2 14 assemble product step A 1.0 + 2 15 Ship product @ factory 1.0 + 1 16 assemble product 1.0 + 2 17 assemble product step C 1.0 + 2 18 assemble product step B 1.0 + 2 19 assemble product step A 1.0 + 2 20 Ship product @ factory 1.0 + 1 21 assemble product 1.0 + 2 22 assemble product step C 1.0 + 2 23 assemble product step B 1.0 + 2 24 assemble product step A 1.0 + 2 25 Ship product @ factory 1.0 + 1 26 assemble product 1.0 + 2 27 assemble product step C 1.0 + 2 28 assemble product step B 1.0 + 2 29 assemble product step A 1.0 + 2 30 Ship product @ factory 1.0 + 1 31 assemble product 1.0 + 2 32 assemble product step C 1.0 + 2 33 assemble product step B 1.0 + 2 34 assemble product step A 1.0 + 2 35 Ship product @ factory 1.0 + 1 36 assemble product 1.0 + 2 37 assemble product step C 1.0 + 2 38 assemble product step B 1.0 + 2 39 assemble product step A 1.0 + 2 40 Ship product @ factory 1.0 + 1 41 assemble product 5.0 + 2 42 assemble product step C 5.0 + 2 43 assemble product step B 5.0 + 2 44 assemble product step A 5.0 + 2 45 Ship product @ factory 5.0 + 1 46 assemble product 5.0 + 2 47 assemble product step C 5.0 + 2 48 assemble product step B 5.0 + 2 49 assemble product step A 5.0 + 2 50 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 66 with quantity 6.0 of 'Purchase component A @ factory from Component supplier': + 0 66 Purchase component A @ factory from Component supplier 6.0 +Downstream pegging of operationplan with id 66 with quantity 6.0 of 'Purchase component A @ factory from Component supplier': + 0 66 Purchase component A @ factory from Component supplier 6.0 + 1 61 assemble product 6.0 + 2 62 assemble product step C 6.0 + 2 63 assemble product step B 6.0 + 2 64 assemble product step A 6.0 + 2 65 Ship product @ factory 6.0 +Upstream pegging of operationplan with id 67 with quantity 1.0 of 'Purchase component A @ factory from Component supplier': + 0 67 Purchase component A @ factory from Component supplier 1.0 +Downstream pegging of operationplan with id 67 with quantity 1.0 of 'Purchase component A @ factory from Component supplier': + 0 67 Purchase component A @ factory from Component supplier 1.0 + 1 56 assemble product 1.0 + 2 57 assemble product step C 1.0 + 2 58 assemble product step B 1.0 + 2 59 assemble product step A 1.0 + 2 60 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 68 with quantity 3.0 of 'Purchase component A @ factory from Component supplier': + 0 68 Purchase component A @ factory from Component supplier 3.0 +Downstream pegging of operationplan with id 68 with quantity 3.0 of 'Purchase component A @ factory from Component supplier': + 0 68 Purchase component A @ factory from Component supplier 3.0 + 1 51 assemble product 3.0 + 2 52 assemble product step C 3.0 + 2 53 assemble product step B 3.0 + 2 54 assemble product step A 3.0 + 2 55 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 50 with quantity 5.0 of 'Ship product @ factory': + 0 50 Ship product @ factory 5.0 + 1 46 assemble product 5.0 + 2 47 assemble product step C 5.0 + 2 48 assemble product step B 5.0 + 2 49 assemble product step A 5.0 + 3 component A @ factory Inventory component A @ factory 5.0 + 2 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 50 with quantity 5.0 of 'Ship product @ factory': + 0 50 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 45 with quantity 5.0 of 'Ship product @ factory': + 0 45 Ship product @ factory 5.0 + 1 41 assemble product 5.0 + 2 42 assemble product step C 5.0 + 2 43 assemble product step B 5.0 + 2 44 assemble product step A 5.0 + 3 component A @ factory Inventory component A @ factory 5.0 + 2 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 45 with quantity 5.0 of 'Ship product @ factory': + 0 45 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 40 with quantity 1.0 of 'Ship product @ factory': + 0 40 Ship product @ factory 1.0 + 1 36 assemble product 1.0 + 2 37 assemble product step C 1.0 + 2 38 assemble product step B 1.0 + 2 39 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 40 with quantity 1.0 of 'Ship product @ factory': + 0 40 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 35 with quantity 1.0 of 'Ship product @ factory': + 0 35 Ship product @ factory 1.0 + 1 31 assemble product 1.0 + 2 32 assemble product step C 1.0 + 2 33 assemble product step B 1.0 + 2 34 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 35 with quantity 1.0 of 'Ship product @ factory': + 0 35 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 30 with quantity 1.0 of 'Ship product @ factory': + 0 30 Ship product @ factory 1.0 + 1 26 assemble product 1.0 + 2 27 assemble product step C 1.0 + 2 28 assemble product step B 1.0 + 2 29 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 30 with quantity 1.0 of 'Ship product @ factory': + 0 30 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 25 with quantity 1.0 of 'Ship product @ factory': + 0 25 Ship product @ factory 1.0 + 1 21 assemble product 1.0 + 2 22 assemble product step C 1.0 + 2 23 assemble product step B 1.0 + 2 24 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 25 with quantity 1.0 of 'Ship product @ factory': + 0 25 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 20 with quantity 1.0 of 'Ship product @ factory': + 0 20 Ship product @ factory 1.0 + 1 16 assemble product 1.0 + 2 17 assemble product step C 1.0 + 2 18 assemble product step B 1.0 + 2 19 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 20 with quantity 1.0 of 'Ship product @ factory': + 0 20 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 15 with quantity 1.0 of 'Ship product @ factory': + 0 15 Ship product @ factory 1.0 + 1 11 assemble product 1.0 + 2 12 assemble product step C 1.0 + 2 13 assemble product step B 1.0 + 2 14 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 15 with quantity 1.0 of 'Ship product @ factory': + 0 15 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 10 with quantity 1.0 of 'Ship product @ factory': + 0 10 Ship product @ factory 1.0 + 1 6 assemble product 1.0 + 2 7 assemble product step C 1.0 + 2 8 assemble product step B 1.0 + 2 9 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 10 with quantity 1.0 of 'Ship product @ factory': + 0 10 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 5 with quantity 3.0 of 'Ship product @ factory': + 0 5 Ship product @ factory 3.0 + 1 1 assemble product 3.0 + 2 2 assemble product step C 3.0 + 2 3 assemble product step B 3.0 + 2 4 assemble product step A 3.0 + 3 component A @ factory Inventory component A @ factory 3.0 + 2 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 5 with quantity 3.0 of 'Ship product @ factory': + 0 5 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 65 with quantity 6.0 of 'Ship product @ factory': + 0 65 Ship product @ factory 6.0 + 1 61 assemble product 6.0 + 2 62 assemble product step C 6.0 + 2 63 assemble product step B 6.0 + 2 64 assemble product step A 6.0 + 3 66 Purchase component A @ factory from Component supplier 6.0 + 2 component D @ factory Inventory component D @ factory 6.0 +Downstream pegging of operationplan with id 65 with quantity 6.0 of 'Ship product @ factory': + 0 65 Ship product @ factory 6.0 +Upstream pegging of operationplan with id 60 with quantity 1.0 of 'Ship product @ factory': + 0 60 Ship product @ factory 1.0 + 1 56 assemble product 1.0 + 2 57 assemble product step C 1.0 + 2 58 assemble product step B 1.0 + 2 59 assemble product step A 1.0 + 3 67 Purchase component A @ factory from Component supplier 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 60 with quantity 1.0 of 'Ship product @ factory': + 0 60 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 55 with quantity 3.0 of 'Ship product @ factory': + 0 55 Ship product @ factory 3.0 + 1 51 assemble product 3.0 + 2 52 assemble product step C 3.0 + 2 53 assemble product step B 3.0 + 2 54 assemble product step A 3.0 + 3 68 Purchase component A @ factory from Component supplier 3.0 + 2 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 55 with quantity 3.0 of 'Ship product @ factory': + 0 55 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 69 with quantity 100.0 of 'WIP product step A': + 0 MO WIP routing WIP routing 100.0 + 1 MO WIP product step C WIP product step C 100.0 + 1 70 WIP product step B 100.0 + 1 69 WIP product step A 100.0 +Downstream pegging of operationplan with id 69 with quantity 100.0 of 'WIP product step A': + 0 MO WIP routing WIP routing 100.0 + 1 MO WIP product step C WIP product step C 100.0 + 1 70 WIP product step B 100.0 + 1 69 WIP product step A 100.0 +Upstream pegging of operationplan with id 70 with quantity 100.0 of 'WIP product step B': + 0 MO WIP routing WIP routing 100.0 + 1 MO WIP product step C WIP product step C 100.0 + 1 70 WIP product step B 100.0 + 1 69 WIP product step A 100.0 +Downstream pegging of operationplan with id 70 with quantity 100.0 of 'WIP product step B': + 0 MO WIP routing WIP routing 100.0 + 1 MO WIP product step C WIP product step C 100.0 + 1 70 WIP product step B 100.0 + 1 69 WIP product step A 100.0 +Upstream pegging of operationplan with id MO WIP product step C with quantity 100.0 of 'WIP product step C': + 0 MO WIP routing WIP routing 100.0 + 1 MO WIP product step C WIP product step C 100.0 + 1 70 WIP product step B 100.0 + 1 69 WIP product step A 100.0 +Downstream pegging of operationplan with id MO WIP product step C with quantity 100.0 of 'WIP product step C': + 0 MO WIP routing WIP routing 100.0 + 1 MO WIP product step C WIP product step C 100.0 + 1 70 WIP product step B 100.0 + 1 69 WIP product step A 100.0 +Upstream pegging of operationplan with id MO WIP routing with quantity 100.0 of 'WIP routing': + 0 MO WIP routing WIP routing 100.0 + 1 MO WIP product step C WIP product step C 100.0 + 1 70 WIP product step B 100.0 + 1 69 WIP product step A 100.0 +Downstream pegging of operationplan with id MO WIP routing with quantity 100.0 of 'WIP routing': + 0 MO WIP routing WIP routing 100.0 + 1 MO WIP product step C WIP product step C 100.0 + 1 70 WIP product step B 100.0 + 1 69 WIP product step A 100.0 +Upstream pegging of operationplan with id 46 with quantity 5.0 of 'assemble product': + 0 46 assemble product 5.0 + 1 47 assemble product step C 5.0 + 1 48 assemble product step B 5.0 + 1 49 assemble product step A 5.0 + 2 component A @ factory Inventory component A @ factory 5.0 + 1 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 46 with quantity 5.0 of 'assemble product': + 0 46 assemble product 5.0 + 1 47 assemble product step C 5.0 + 1 48 assemble product step B 5.0 + 1 49 assemble product step A 5.0 + 1 50 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 41 with quantity 5.0 of 'assemble product': + 0 41 assemble product 5.0 + 1 42 assemble product step C 5.0 + 1 43 assemble product step B 5.0 + 1 44 assemble product step A 5.0 + 2 component A @ factory Inventory component A @ factory 5.0 + 1 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 41 with quantity 5.0 of 'assemble product': + 0 41 assemble product 5.0 + 1 42 assemble product step C 5.0 + 1 43 assemble product step B 5.0 + 1 44 assemble product step A 5.0 + 1 45 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 36 with quantity 1.0 of 'assemble product': + 0 36 assemble product 1.0 + 1 37 assemble product step C 1.0 + 1 38 assemble product step B 1.0 + 1 39 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 36 with quantity 1.0 of 'assemble product': + 0 36 assemble product 1.0 + 1 37 assemble product step C 1.0 + 1 38 assemble product step B 1.0 + 1 39 assemble product step A 1.0 + 1 40 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 31 with quantity 1.0 of 'assemble product': + 0 31 assemble product 1.0 + 1 32 assemble product step C 1.0 + 1 33 assemble product step B 1.0 + 1 34 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 31 with quantity 1.0 of 'assemble product': + 0 31 assemble product 1.0 + 1 32 assemble product step C 1.0 + 1 33 assemble product step B 1.0 + 1 34 assemble product step A 1.0 + 1 35 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 26 with quantity 1.0 of 'assemble product': + 0 26 assemble product 1.0 + 1 27 assemble product step C 1.0 + 1 28 assemble product step B 1.0 + 1 29 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 26 with quantity 1.0 of 'assemble product': + 0 26 assemble product 1.0 + 1 27 assemble product step C 1.0 + 1 28 assemble product step B 1.0 + 1 29 assemble product step A 1.0 + 1 30 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 21 with quantity 1.0 of 'assemble product': + 0 21 assemble product 1.0 + 1 22 assemble product step C 1.0 + 1 23 assemble product step B 1.0 + 1 24 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 21 with quantity 1.0 of 'assemble product': + 0 21 assemble product 1.0 + 1 22 assemble product step C 1.0 + 1 23 assemble product step B 1.0 + 1 24 assemble product step A 1.0 + 1 25 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 16 with quantity 1.0 of 'assemble product': + 0 16 assemble product 1.0 + 1 17 assemble product step C 1.0 + 1 18 assemble product step B 1.0 + 1 19 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 16 with quantity 1.0 of 'assemble product': + 0 16 assemble product 1.0 + 1 17 assemble product step C 1.0 + 1 18 assemble product step B 1.0 + 1 19 assemble product step A 1.0 + 1 20 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 11 with quantity 1.0 of 'assemble product': + 0 11 assemble product 1.0 + 1 12 assemble product step C 1.0 + 1 13 assemble product step B 1.0 + 1 14 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 11 with quantity 1.0 of 'assemble product': + 0 11 assemble product 1.0 + 1 12 assemble product step C 1.0 + 1 13 assemble product step B 1.0 + 1 14 assemble product step A 1.0 + 1 15 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 6 with quantity 1.0 of 'assemble product': + 0 6 assemble product 1.0 + 1 7 assemble product step C 1.0 + 1 8 assemble product step B 1.0 + 1 9 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 6 with quantity 1.0 of 'assemble product': + 0 6 assemble product 1.0 + 1 7 assemble product step C 1.0 + 1 8 assemble product step B 1.0 + 1 9 assemble product step A 1.0 + 1 10 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 1 with quantity 3.0 of 'assemble product': + 0 1 assemble product 3.0 + 1 2 assemble product step C 3.0 + 1 3 assemble product step B 3.0 + 1 4 assemble product step A 3.0 + 2 component A @ factory Inventory component A @ factory 3.0 + 1 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 1 with quantity 3.0 of 'assemble product': + 0 1 assemble product 3.0 + 1 2 assemble product step C 3.0 + 1 3 assemble product step B 3.0 + 1 4 assemble product step A 3.0 + 1 5 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 61 with quantity 6.0 of 'assemble product': + 0 61 assemble product 6.0 + 1 62 assemble product step C 6.0 + 1 63 assemble product step B 6.0 + 1 64 assemble product step A 6.0 + 2 66 Purchase component A @ factory from Component supplier 6.0 + 1 component D @ factory Inventory component D @ factory 6.0 +Downstream pegging of operationplan with id 61 with quantity 6.0 of 'assemble product': + 0 61 assemble product 6.0 + 1 62 assemble product step C 6.0 + 1 63 assemble product step B 6.0 + 1 64 assemble product step A 6.0 + 1 65 Ship product @ factory 6.0 +Upstream pegging of operationplan with id 56 with quantity 1.0 of 'assemble product': + 0 56 assemble product 1.0 + 1 57 assemble product step C 1.0 + 1 58 assemble product step B 1.0 + 1 59 assemble product step A 1.0 + 2 67 Purchase component A @ factory from Component supplier 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 56 with quantity 1.0 of 'assemble product': + 0 56 assemble product 1.0 + 1 57 assemble product step C 1.0 + 1 58 assemble product step B 1.0 + 1 59 assemble product step A 1.0 + 1 60 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 51 with quantity 3.0 of 'assemble product': + 0 51 assemble product 3.0 + 1 52 assemble product step C 3.0 + 1 53 assemble product step B 3.0 + 1 54 assemble product step A 3.0 + 2 68 Purchase component A @ factory from Component supplier 3.0 + 1 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 51 with quantity 3.0 of 'assemble product': + 0 51 assemble product 3.0 + 1 52 assemble product step C 3.0 + 1 53 assemble product step B 3.0 + 1 54 assemble product step A 3.0 + 1 55 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 49 with quantity 5.0 of 'assemble product step A': + 0 46 assemble product 5.0 + 1 47 assemble product step C 5.0 + 1 48 assemble product step B 5.0 + 1 49 assemble product step A 5.0 + 2 component A @ factory Inventory component A @ factory 5.0 + 1 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 49 with quantity 5.0 of 'assemble product step A': + 0 46 assemble product 5.0 + 1 47 assemble product step C 5.0 + 1 48 assemble product step B 5.0 + 1 49 assemble product step A 5.0 + 1 50 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 44 with quantity 5.0 of 'assemble product step A': + 0 41 assemble product 5.0 + 1 42 assemble product step C 5.0 + 1 43 assemble product step B 5.0 + 1 44 assemble product step A 5.0 + 2 component A @ factory Inventory component A @ factory 5.0 + 1 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 44 with quantity 5.0 of 'assemble product step A': + 0 41 assemble product 5.0 + 1 42 assemble product step C 5.0 + 1 43 assemble product step B 5.0 + 1 44 assemble product step A 5.0 + 1 45 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 39 with quantity 1.0 of 'assemble product step A': + 0 36 assemble product 1.0 + 1 37 assemble product step C 1.0 + 1 38 assemble product step B 1.0 + 1 39 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 39 with quantity 1.0 of 'assemble product step A': + 0 36 assemble product 1.0 + 1 37 assemble product step C 1.0 + 1 38 assemble product step B 1.0 + 1 39 assemble product step A 1.0 + 1 40 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 34 with quantity 1.0 of 'assemble product step A': + 0 31 assemble product 1.0 + 1 32 assemble product step C 1.0 + 1 33 assemble product step B 1.0 + 1 34 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 34 with quantity 1.0 of 'assemble product step A': + 0 31 assemble product 1.0 + 1 32 assemble product step C 1.0 + 1 33 assemble product step B 1.0 + 1 34 assemble product step A 1.0 + 1 35 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 29 with quantity 1.0 of 'assemble product step A': + 0 26 assemble product 1.0 + 1 27 assemble product step C 1.0 + 1 28 assemble product step B 1.0 + 1 29 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 29 with quantity 1.0 of 'assemble product step A': + 0 26 assemble product 1.0 + 1 27 assemble product step C 1.0 + 1 28 assemble product step B 1.0 + 1 29 assemble product step A 1.0 + 1 30 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 24 with quantity 1.0 of 'assemble product step A': + 0 21 assemble product 1.0 + 1 22 assemble product step C 1.0 + 1 23 assemble product step B 1.0 + 1 24 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 24 with quantity 1.0 of 'assemble product step A': + 0 21 assemble product 1.0 + 1 22 assemble product step C 1.0 + 1 23 assemble product step B 1.0 + 1 24 assemble product step A 1.0 + 1 25 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 19 with quantity 1.0 of 'assemble product step A': + 0 16 assemble product 1.0 + 1 17 assemble product step C 1.0 + 1 18 assemble product step B 1.0 + 1 19 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 19 with quantity 1.0 of 'assemble product step A': + 0 16 assemble product 1.0 + 1 17 assemble product step C 1.0 + 1 18 assemble product step B 1.0 + 1 19 assemble product step A 1.0 + 1 20 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 14 with quantity 1.0 of 'assemble product step A': + 0 11 assemble product 1.0 + 1 12 assemble product step C 1.0 + 1 13 assemble product step B 1.0 + 1 14 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 14 with quantity 1.0 of 'assemble product step A': + 0 11 assemble product 1.0 + 1 12 assemble product step C 1.0 + 1 13 assemble product step B 1.0 + 1 14 assemble product step A 1.0 + 1 15 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 9 with quantity 1.0 of 'assemble product step A': + 0 6 assemble product 1.0 + 1 7 assemble product step C 1.0 + 1 8 assemble product step B 1.0 + 1 9 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 9 with quantity 1.0 of 'assemble product step A': + 0 6 assemble product 1.0 + 1 7 assemble product step C 1.0 + 1 8 assemble product step B 1.0 + 1 9 assemble product step A 1.0 + 1 10 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 4 with quantity 3.0 of 'assemble product step A': + 0 1 assemble product 3.0 + 1 2 assemble product step C 3.0 + 1 3 assemble product step B 3.0 + 1 4 assemble product step A 3.0 + 2 component A @ factory Inventory component A @ factory 3.0 + 1 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 4 with quantity 3.0 of 'assemble product step A': + 0 1 assemble product 3.0 + 1 2 assemble product step C 3.0 + 1 3 assemble product step B 3.0 + 1 4 assemble product step A 3.0 + 1 5 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 64 with quantity 6.0 of 'assemble product step A': + 0 61 assemble product 6.0 + 1 62 assemble product step C 6.0 + 1 63 assemble product step B 6.0 + 1 64 assemble product step A 6.0 + 2 66 Purchase component A @ factory from Component supplier 6.0 + 1 component D @ factory Inventory component D @ factory 6.0 +Downstream pegging of operationplan with id 64 with quantity 6.0 of 'assemble product step A': + 0 61 assemble product 6.0 + 1 62 assemble product step C 6.0 + 1 63 assemble product step B 6.0 + 1 64 assemble product step A 6.0 + 1 65 Ship product @ factory 6.0 +Upstream pegging of operationplan with id 59 with quantity 1.0 of 'assemble product step A': + 0 56 assemble product 1.0 + 1 57 assemble product step C 1.0 + 1 58 assemble product step B 1.0 + 1 59 assemble product step A 1.0 + 2 67 Purchase component A @ factory from Component supplier 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 59 with quantity 1.0 of 'assemble product step A': + 0 56 assemble product 1.0 + 1 57 assemble product step C 1.0 + 1 58 assemble product step B 1.0 + 1 59 assemble product step A 1.0 + 1 60 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 54 with quantity 3.0 of 'assemble product step A': + 0 51 assemble product 3.0 + 1 52 assemble product step C 3.0 + 1 53 assemble product step B 3.0 + 1 54 assemble product step A 3.0 + 2 68 Purchase component A @ factory from Component supplier 3.0 + 1 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 54 with quantity 3.0 of 'assemble product step A': + 0 51 assemble product 3.0 + 1 52 assemble product step C 3.0 + 1 53 assemble product step B 3.0 + 1 54 assemble product step A 3.0 + 1 55 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 48 with quantity 5.0 of 'assemble product step B': + 0 46 assemble product 5.0 + 1 47 assemble product step C 5.0 + 1 48 assemble product step B 5.0 + 1 49 assemble product step A 5.0 + 2 component A @ factory Inventory component A @ factory 5.0 + 1 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 48 with quantity 5.0 of 'assemble product step B': + 0 46 assemble product 5.0 + 1 47 assemble product step C 5.0 + 1 48 assemble product step B 5.0 + 1 49 assemble product step A 5.0 + 1 50 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 43 with quantity 5.0 of 'assemble product step B': + 0 41 assemble product 5.0 + 1 42 assemble product step C 5.0 + 1 43 assemble product step B 5.0 + 1 44 assemble product step A 5.0 + 2 component A @ factory Inventory component A @ factory 5.0 + 1 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 43 with quantity 5.0 of 'assemble product step B': + 0 41 assemble product 5.0 + 1 42 assemble product step C 5.0 + 1 43 assemble product step B 5.0 + 1 44 assemble product step A 5.0 + 1 45 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 38 with quantity 1.0 of 'assemble product step B': + 0 36 assemble product 1.0 + 1 37 assemble product step C 1.0 + 1 38 assemble product step B 1.0 + 1 39 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 38 with quantity 1.0 of 'assemble product step B': + 0 36 assemble product 1.0 + 1 37 assemble product step C 1.0 + 1 38 assemble product step B 1.0 + 1 39 assemble product step A 1.0 + 1 40 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 33 with quantity 1.0 of 'assemble product step B': + 0 31 assemble product 1.0 + 1 32 assemble product step C 1.0 + 1 33 assemble product step B 1.0 + 1 34 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 33 with quantity 1.0 of 'assemble product step B': + 0 31 assemble product 1.0 + 1 32 assemble product step C 1.0 + 1 33 assemble product step B 1.0 + 1 34 assemble product step A 1.0 + 1 35 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 28 with quantity 1.0 of 'assemble product step B': + 0 26 assemble product 1.0 + 1 27 assemble product step C 1.0 + 1 28 assemble product step B 1.0 + 1 29 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 28 with quantity 1.0 of 'assemble product step B': + 0 26 assemble product 1.0 + 1 27 assemble product step C 1.0 + 1 28 assemble product step B 1.0 + 1 29 assemble product step A 1.0 + 1 30 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 23 with quantity 1.0 of 'assemble product step B': + 0 21 assemble product 1.0 + 1 22 assemble product step C 1.0 + 1 23 assemble product step B 1.0 + 1 24 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 23 with quantity 1.0 of 'assemble product step B': + 0 21 assemble product 1.0 + 1 22 assemble product step C 1.0 + 1 23 assemble product step B 1.0 + 1 24 assemble product step A 1.0 + 1 25 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 18 with quantity 1.0 of 'assemble product step B': + 0 16 assemble product 1.0 + 1 17 assemble product step C 1.0 + 1 18 assemble product step B 1.0 + 1 19 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 18 with quantity 1.0 of 'assemble product step B': + 0 16 assemble product 1.0 + 1 17 assemble product step C 1.0 + 1 18 assemble product step B 1.0 + 1 19 assemble product step A 1.0 + 1 20 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 13 with quantity 1.0 of 'assemble product step B': + 0 11 assemble product 1.0 + 1 12 assemble product step C 1.0 + 1 13 assemble product step B 1.0 + 1 14 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 13 with quantity 1.0 of 'assemble product step B': + 0 11 assemble product 1.0 + 1 12 assemble product step C 1.0 + 1 13 assemble product step B 1.0 + 1 14 assemble product step A 1.0 + 1 15 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 8 with quantity 1.0 of 'assemble product step B': + 0 6 assemble product 1.0 + 1 7 assemble product step C 1.0 + 1 8 assemble product step B 1.0 + 1 9 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 8 with quantity 1.0 of 'assemble product step B': + 0 6 assemble product 1.0 + 1 7 assemble product step C 1.0 + 1 8 assemble product step B 1.0 + 1 9 assemble product step A 1.0 + 1 10 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 3 with quantity 3.0 of 'assemble product step B': + 0 1 assemble product 3.0 + 1 2 assemble product step C 3.0 + 1 3 assemble product step B 3.0 + 1 4 assemble product step A 3.0 + 2 component A @ factory Inventory component A @ factory 3.0 + 1 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 3 with quantity 3.0 of 'assemble product step B': + 0 1 assemble product 3.0 + 1 2 assemble product step C 3.0 + 1 3 assemble product step B 3.0 + 1 4 assemble product step A 3.0 + 1 5 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 63 with quantity 6.0 of 'assemble product step B': + 0 61 assemble product 6.0 + 1 62 assemble product step C 6.0 + 1 63 assemble product step B 6.0 + 1 64 assemble product step A 6.0 + 2 66 Purchase component A @ factory from Component supplier 6.0 + 1 component D @ factory Inventory component D @ factory 6.0 +Downstream pegging of operationplan with id 63 with quantity 6.0 of 'assemble product step B': + 0 61 assemble product 6.0 + 1 62 assemble product step C 6.0 + 1 63 assemble product step B 6.0 + 1 64 assemble product step A 6.0 + 1 65 Ship product @ factory 6.0 +Upstream pegging of operationplan with id 58 with quantity 1.0 of 'assemble product step B': + 0 56 assemble product 1.0 + 1 57 assemble product step C 1.0 + 1 58 assemble product step B 1.0 + 1 59 assemble product step A 1.0 + 2 67 Purchase component A @ factory from Component supplier 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 58 with quantity 1.0 of 'assemble product step B': + 0 56 assemble product 1.0 + 1 57 assemble product step C 1.0 + 1 58 assemble product step B 1.0 + 1 59 assemble product step A 1.0 + 1 60 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 53 with quantity 3.0 of 'assemble product step B': + 0 51 assemble product 3.0 + 1 52 assemble product step C 3.0 + 1 53 assemble product step B 3.0 + 1 54 assemble product step A 3.0 + 2 68 Purchase component A @ factory from Component supplier 3.0 + 1 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 53 with quantity 3.0 of 'assemble product step B': + 0 51 assemble product 3.0 + 1 52 assemble product step C 3.0 + 1 53 assemble product step B 3.0 + 1 54 assemble product step A 3.0 + 1 55 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 47 with quantity 5.0 of 'assemble product step C': + 0 46 assemble product 5.0 + 1 47 assemble product step C 5.0 + 1 48 assemble product step B 5.0 + 1 49 assemble product step A 5.0 + 2 component A @ factory Inventory component A @ factory 5.0 + 1 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 47 with quantity 5.0 of 'assemble product step C': + 0 46 assemble product 5.0 + 1 47 assemble product step C 5.0 + 1 48 assemble product step B 5.0 + 1 49 assemble product step A 5.0 + 1 50 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 42 with quantity 5.0 of 'assemble product step C': + 0 41 assemble product 5.0 + 1 42 assemble product step C 5.0 + 1 43 assemble product step B 5.0 + 1 44 assemble product step A 5.0 + 2 component A @ factory Inventory component A @ factory 5.0 + 1 component D @ factory Inventory component D @ factory 5.0 +Downstream pegging of operationplan with id 42 with quantity 5.0 of 'assemble product step C': + 0 41 assemble product 5.0 + 1 42 assemble product step C 5.0 + 1 43 assemble product step B 5.0 + 1 44 assemble product step A 5.0 + 1 45 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 37 with quantity 1.0 of 'assemble product step C': + 0 36 assemble product 1.0 + 1 37 assemble product step C 1.0 + 1 38 assemble product step B 1.0 + 1 39 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 37 with quantity 1.0 of 'assemble product step C': + 0 36 assemble product 1.0 + 1 37 assemble product step C 1.0 + 1 38 assemble product step B 1.0 + 1 39 assemble product step A 1.0 + 1 40 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 32 with quantity 1.0 of 'assemble product step C': + 0 31 assemble product 1.0 + 1 32 assemble product step C 1.0 + 1 33 assemble product step B 1.0 + 1 34 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 32 with quantity 1.0 of 'assemble product step C': + 0 31 assemble product 1.0 + 1 32 assemble product step C 1.0 + 1 33 assemble product step B 1.0 + 1 34 assemble product step A 1.0 + 1 35 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 22 with quantity 1.0 of 'assemble product step C': + 0 21 assemble product 1.0 + 1 22 assemble product step C 1.0 + 1 23 assemble product step B 1.0 + 1 24 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 22 with quantity 1.0 of 'assemble product step C': + 0 21 assemble product 1.0 + 1 22 assemble product step C 1.0 + 1 23 assemble product step B 1.0 + 1 24 assemble product step A 1.0 + 1 25 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 27 with quantity 1.0 of 'assemble product step C': + 0 26 assemble product 1.0 + 1 27 assemble product step C 1.0 + 1 28 assemble product step B 1.0 + 1 29 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 27 with quantity 1.0 of 'assemble product step C': + 0 26 assemble product 1.0 + 1 27 assemble product step C 1.0 + 1 28 assemble product step B 1.0 + 1 29 assemble product step A 1.0 + 1 30 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 17 with quantity 1.0 of 'assemble product step C': + 0 16 assemble product 1.0 + 1 17 assemble product step C 1.0 + 1 18 assemble product step B 1.0 + 1 19 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 17 with quantity 1.0 of 'assemble product step C': + 0 16 assemble product 1.0 + 1 17 assemble product step C 1.0 + 1 18 assemble product step B 1.0 + 1 19 assemble product step A 1.0 + 1 20 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 7 with quantity 1.0 of 'assemble product step C': + 0 6 assemble product 1.0 + 1 7 assemble product step C 1.0 + 1 8 assemble product step B 1.0 + 1 9 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 7 with quantity 1.0 of 'assemble product step C': + 0 6 assemble product 1.0 + 1 7 assemble product step C 1.0 + 1 8 assemble product step B 1.0 + 1 9 assemble product step A 1.0 + 1 10 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 12 with quantity 1.0 of 'assemble product step C': + 0 11 assemble product 1.0 + 1 12 assemble product step C 1.0 + 1 13 assemble product step B 1.0 + 1 14 assemble product step A 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 12 with quantity 1.0 of 'assemble product step C': + 0 11 assemble product 1.0 + 1 12 assemble product step C 1.0 + 1 13 assemble product step B 1.0 + 1 14 assemble product step A 1.0 + 1 15 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 2 with quantity 3.0 of 'assemble product step C': + 0 1 assemble product 3.0 + 1 2 assemble product step C 3.0 + 1 3 assemble product step B 3.0 + 1 4 assemble product step A 3.0 + 2 component A @ factory Inventory component A @ factory 3.0 + 1 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 2 with quantity 3.0 of 'assemble product step C': + 0 1 assemble product 3.0 + 1 2 assemble product step C 3.0 + 1 3 assemble product step B 3.0 + 1 4 assemble product step A 3.0 + 1 5 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 62 with quantity 6.0 of 'assemble product step C': + 0 61 assemble product 6.0 + 1 62 assemble product step C 6.0 + 1 63 assemble product step B 6.0 + 1 64 assemble product step A 6.0 + 2 66 Purchase component A @ factory from Component supplier 6.0 + 1 component D @ factory Inventory component D @ factory 6.0 +Downstream pegging of operationplan with id 62 with quantity 6.0 of 'assemble product step C': + 0 61 assemble product 6.0 + 1 62 assemble product step C 6.0 + 1 63 assemble product step B 6.0 + 1 64 assemble product step A 6.0 + 1 65 Ship product @ factory 6.0 +Upstream pegging of operationplan with id 57 with quantity 1.0 of 'assemble product step C': + 0 56 assemble product 1.0 + 1 57 assemble product step C 1.0 + 1 58 assemble product step B 1.0 + 1 59 assemble product step A 1.0 + 2 67 Purchase component A @ factory from Component supplier 1.0 + 1 component D @ factory Inventory component D @ factory 1.0 +Downstream pegging of operationplan with id 57 with quantity 1.0 of 'assemble product step C': + 0 56 assemble product 1.0 + 1 57 assemble product step C 1.0 + 1 58 assemble product step B 1.0 + 1 59 assemble product step A 1.0 + 1 60 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 52 with quantity 3.0 of 'assemble product step C': + 0 51 assemble product 3.0 + 1 52 assemble product step C 3.0 + 1 53 assemble product step B 3.0 + 1 54 assemble product step A 3.0 + 2 68 Purchase component A @ factory from Component supplier 3.0 + 1 component D @ factory Inventory component D @ factory 3.0 +Downstream pegging of operationplan with id 52 with quantity 3.0 of 'assemble product step C': + 0 51 assemble product 3.0 + 1 52 assemble product step C 3.0 + 1 53 assemble product step B 3.0 + 1 54 assemble product step A 3.0 + 1 55 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 71 with quantity 10.0 of 'routing 8': + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Downstream pegging of operationplan with id 71 with quantity 10.0 of 'routing 8': + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Upstream pegging of operationplan with id 75 with quantity 10.0 of 'routing 8': + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Downstream pegging of operationplan with id 75 with quantity 10.0 of 'routing 8': + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Upstream pegging of operationplan with id 74 with quantity 10.0 of 'routing 8 step 1': + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Downstream pegging of operationplan with id 74 with quantity 10.0 of 'routing 8 step 1': + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Upstream pegging of operationplan with id 78 with quantity 10.0 of 'routing 8 step 1': + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Downstream pegging of operationplan with id 78 with quantity 10.0 of 'routing 8 step 1': + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Upstream pegging of operationplan with id 73 with quantity 10.0 of 'routing 8 step 2': + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Downstream pegging of operationplan with id 73 with quantity 10.0 of 'routing 8 step 2': + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Upstream pegging of operationplan with id 77 with quantity 10.0 of 'routing 8 step 2': + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Downstream pegging of operationplan with id 77 with quantity 10.0 of 'routing 8 step 2': + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Upstream pegging of operationplan with id 72 with quantity 10.0 of 'routing 8 step 3': + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Downstream pegging of operationplan with id 72 with quantity 10.0 of 'routing 8 step 3': + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Upstream pegging of operationplan with id 76 with quantity 10.0 of 'routing 8 step 3': + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Downstream pegging of operationplan with id 76 with quantity 10.0 of 'routing 8 step 3': + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Upstream pegging of operationplan with id 79 with quantity 10.0 of 'routing 9': + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Downstream pegging of operationplan with id 79 with quantity 10.0 of 'routing 9': + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Upstream pegging of operationplan with id 83 with quantity 10.0 of 'routing 9': + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 +Downstream pegging of operationplan with id 83 with quantity 10.0 of 'routing 9': + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 +Upstream pegging of operationplan with id 82 with quantity 10.0 of 'routing 9 step 1': + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Downstream pegging of operationplan with id 82 with quantity 10.0 of 'routing 9 step 1': + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Upstream pegging of operationplan with id 86 with quantity 10.0 of 'routing 9 step 1': + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 +Downstream pegging of operationplan with id 86 with quantity 10.0 of 'routing 9 step 1': + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 +Upstream pegging of operationplan with id 81 with quantity 10.0 of 'routing 9 step 2': + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Downstream pegging of operationplan with id 81 with quantity 10.0 of 'routing 9 step 2': + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Upstream pegging of operationplan with id 85 with quantity 10.0 of 'routing 9 step 2': + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 +Downstream pegging of operationplan with id 85 with quantity 10.0 of 'routing 9 step 2': + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 +Upstream pegging of operationplan with id 80 with quantity 10.0 of 'routing 9 step 3': + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Downstream pegging of operationplan with id 80 with quantity 10.0 of 'routing 9 step 3': + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Upstream pegging of operationplan with id 84 with quantity 10.0 of 'routing 9 step 3': + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 +Downstream pegging of operationplan with id 84 with quantity 10.0 of 'routing 9 step 3': + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 +Pegging of demand order 1 with quantity 5.0: + 0 45 Ship product @ factory 5.0 + 1 41 assemble product 5.0 + 2 42 assemble product step C 5.0 + 2 43 assemble product step B 5.0 + 2 44 assemble product step A 5.0 + 3 component A @ factory Inventory component A @ factory 5.0 + 2 component D @ factory Inventory component D @ factory 5.0 +Pegging of demand order 2 with quantity 5.0: + 0 50 Ship product @ factory 5.0 + 1 46 assemble product 5.0 + 2 47 assemble product step C 5.0 + 2 48 assemble product step B 5.0 + 2 49 assemble product step A 5.0 + 3 component A @ factory Inventory component A @ factory 5.0 + 2 component D @ factory Inventory component D @ factory 5.0 +Pegging of demand order 3a with quantity 1.0: + 0 40 Ship product @ factory 1.0 + 1 36 assemble product 1.0 + 2 37 assemble product step C 1.0 + 2 38 assemble product step B 1.0 + 2 39 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Pegging of demand order 3b with quantity 1.0: + 0 35 Ship product @ factory 1.0 + 1 31 assemble product 1.0 + 2 32 assemble product step C 1.0 + 2 33 assemble product step B 1.0 + 2 34 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Pegging of demand order 4a with quantity 1.0: + 0 30 Ship product @ factory 1.0 + 1 26 assemble product 1.0 + 2 27 assemble product step C 1.0 + 2 28 assemble product step B 1.0 + 2 29 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Pegging of demand order 4b with quantity 1.0: + 0 25 Ship product @ factory 1.0 + 1 21 assemble product 1.0 + 2 22 assemble product step C 1.0 + 2 23 assemble product step B 1.0 + 2 24 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Pegging of demand order 5a with quantity 1.0: + 0 20 Ship product @ factory 1.0 + 1 16 assemble product 1.0 + 2 17 assemble product step C 1.0 + 2 18 assemble product step B 1.0 + 2 19 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Pegging of demand order 5b with quantity 1.0: + 0 15 Ship product @ factory 1.0 + 1 11 assemble product 1.0 + 2 12 assemble product step C 1.0 + 2 13 assemble product step B 1.0 + 2 14 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Pegging of demand order 5c with quantity 1.0: + 0 10 Ship product @ factory 1.0 + 1 6 assemble product 1.0 + 2 7 assemble product step C 1.0 + 2 8 assemble product step B 1.0 + 2 9 assemble product step A 1.0 + 3 component A @ factory Inventory component A @ factory 1.0 + 2 component D @ factory Inventory component D @ factory 1.0 +Pegging of demand order 6 with quantity 10.0: + 0 5 Ship product @ factory 3.0 + 1 1 assemble product 3.0 + 2 2 assemble product step C 3.0 + 2 3 assemble product step B 3.0 + 2 4 assemble product step A 3.0 + 3 component A @ factory Inventory component A @ factory 3.0 + 2 component D @ factory Inventory component D @ factory 10.0 + 0 65 Ship product @ factory 6.0 + 1 61 assemble product 6.0 + 2 62 assemble product step C 6.0 + 2 63 assemble product step B 6.0 + 2 64 assemble product step A 6.0 + 3 66 Purchase component A @ factory from Component supplier 6.0 + 0 60 Ship product @ factory 1.0 + 1 56 assemble product 1.0 + 2 57 assemble product step C 1.0 + 2 58 assemble product step B 1.0 + 2 59 assemble product step A 1.0 + 3 67 Purchase component A @ factory from Component supplier 1.0 +Pegging of demand order 7 with quantity 10.0: + 0 55 Ship product @ factory 3.0 + 1 51 assemble product 3.0 + 2 52 assemble product step C 3.0 + 2 53 assemble product step B 3.0 + 2 54 assemble product step A 3.0 + 3 68 Purchase component A @ factory from Component supplier 3.0 + 2 component D @ factory Inventory component D @ factory 3.0 +Pegging of demand order 8A with quantity 10.0: + 0 71 routing 8 10.0 + 1 72 routing 8 step 3 10.0 + 1 73 routing 8 step 2 10.0 + 1 74 routing 8 step 1 10.0 +Pegging of demand order 8B with quantity 10.0: + 0 75 routing 8 10.0 + 1 76 routing 8 step 3 10.0 + 1 77 routing 8 step 2 10.0 + 1 78 routing 8 step 1 10.0 +Pegging of demand order 9A with quantity 10.0: + 0 79 routing 9 10.0 + 1 80 routing 9 step 3 10.0 + 1 81 routing 9 step 2 10.0 + 1 82 routing 9 step 1 10.0 +Pegging of demand order 9B with quantity 10.0: + 0 83 routing 9 10.0 + 1 84 routing 9 step 3 10.0 + 1 85 routing 9 step 2 10.0 + 1 86 routing 9 step 1 10.0 diff --git a/test/pegging_5/pegging_5.xml b/test/pegging_5/pegging_5.xml new file mode 100644 index 0000000000..60715c5b76 --- /dev/null +++ b/test/pegging_5/pegging_5.xml @@ -0,0 +1,461 @@ + + + + Test model for routing operations + + This test verifies the behavior of routing operations. + + 2009-01-01T00:00:00 + + + + + + + + + + + + P150D + + + + 20 + + + + + + + + + + + + + + + + + 30 + + + + + + + + + + + + + + 1 + + + + -1 + + + + -1 + + + + + + + + + + -1 + + + + 1 + + + + + + + + -1 + + + + -1 + + + + 2 + + + + + + + + -1 + + + + 3 + + + + + + + + + + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 2 + + + + + + + + + + + + + + 5 + 5 + 2009-01-25T00:00:00 + 1 + + + + + + + 5 + 5 + 2009-01-05T00:00:00 + 2 + + + + + + + 1 + 1 + 2009-02-10T00:00:00 + 3 + + + + + 1 + 2009-02-10T00:00:00 + 4 + + + + + + + 1 + 1 + 2009-02-20T00:00:00 + 5 + + + + + 1 + 1 + 2009-02-20T00:00:00 + 6 + + + + + + + 1 + 1 + 2009-03-20T00:00:00 + 7 + + + + + 1 + 1 + 2009-03-20T00:00:00 + 8 + + + + + 1 + 2009-03-20T00:00:00 + 9 + + + + + + + 10 + 1 + 2009-04-20T00:00:00 + 10 + + + + + + + 10 + 1 + 2009-07-20T00:00:00 + 11 + + + + + + + 10 + 1 + 2009-01-06T00:00:00 + 12 + + + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 10 + 1 + 2009-09-20T00:00:00 + 13 + + + + + + + + 10 + 1 + 2009-01-06T00:00:00 + 14 + + + + + + + + + 1 + + + + + + 2 + + + + + + 3 + + + + + + 10 + 1 + 2009-09-20T00:00:00 + 15 + + + + + + + + + + + + + + + + + + + -1 + + + + 1 + + + + + + + + -1 + + + + -1 + + + + 2 + + + + + + + + -1 + + + + 1 + + + + 3 + + + + + + + MO WIP routing + + 2009-02-09T00:00:00 + 2009-02-10T00:00:00 + 100 + confirmed + + + MO WIP product step C + + 2009-02-01T00:00:00 + 2009-02-03T00:00:00 + 100 + confirmed + + + MO WIP product step C + + + + + + diff --git a/test/pegging_6/pegging_6.1.expect b/test/pegging_6/pegging_6.1.expect new file mode 100644 index 0000000000..9e0407d8ba --- /dev/null +++ b/test/pegging_6/pegging_6.1.expect @@ -0,0 +1,218 @@ +Upstream pegging of operationplan with id product A @ Central WH with quantity 10.0 of 'Inventory product A @ Central WH': + 0 product A @ Central WH Inventory product A @ Central WH 10.0 +Downstream pegging of operationplan with id product A @ Central WH with quantity 10.0 of 'Inventory product A @ Central WH': + 0 product A @ Central WH Inventory product A @ Central WH 10.0 + 1 5 Ship product A from Central WH to Regional DC 1 10.0 + 2 6 Ship product A from Regional DC 1 to Local DC 1a 10.0 + 3 7 Ship product A @ Local DC 1a 10.0 +Upstream pegging of operationplan with id 8 with quantity 90.0 of 'Purchase product A @ Central WH from Supplier 1': + 0 8 Purchase product A @ Central WH from Supplier 1 90.0 +Downstream pegging of operationplan with id 8 with quantity 90.0 of 'Purchase product A @ Central WH from Supplier 1': + 0 8 Purchase product A @ Central WH from Supplier 1 90.0 + 1 9 Ship product A from Central WH to Regional DC 1 90.0 + 2 10 Ship product A from Regional DC 1 to Local DC 1a 90.0 + 3 11 Ship product A @ Local DC 1a 50.0 + 3 12 Ship product A @ Local DC 1a 40.0 +Upstream pegging of operationplan with id 13 with quantity 100.0 of 'Purchase product A @ Central WH from Supplier 1': + 0 13 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 13 with quantity 100.0 of 'Purchase product A @ Central WH from Supplier 1': + 0 13 Purchase product A @ Central WH from Supplier 1 100.0 + 1 14 Replenish product A @ Regional DC 2 100.0 + 2 15 Ship product A from Central WH to Regional DC 2 100.0 + 3 16 Ship product A from Regional DC 2 to Local DC 2b 100.0 + 4 17 Ship product A @ Local DC 2b 100.0 +Upstream pegging of operationplan with id 18 with quantity 100.0 of 'Purchase product A @ Central WH from Supplier 1': + 0 18 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 18 with quantity 100.0 of 'Purchase product A @ Central WH from Supplier 1': + 0 18 Purchase product A @ Central WH from Supplier 1 100.0 + 1 19 Ship product A from Central WH to Regional DC 1 100.0 + 2 20 Ship product A from Regional DC 1 to Local DC 1b 100.0 + 3 21 Ship product A @ Local DC 1b 100.0 +Upstream pegging of operationplan with id 22 with quantity 100.0 of 'Purchase product A @ Central WH from Supplier 1': + 0 22 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 22 with quantity 100.0 of 'Purchase product A @ Central WH from Supplier 1': + 0 22 Purchase product A @ Central WH from Supplier 1 100.0 + 1 1 Ship product A from Central WH to Local DC 2b 100.0 +Upstream pegging of operationplan with id 23 with quantity 100.0 of 'Purchase product A @ Regional DC 2 from Supplier 1': + 0 24 Replenish product A @ Regional DC 2 100.0 + 1 23 Purchase product A @ Regional DC 2 from Supplier 1 100.0 +Downstream pegging of operationplan with id 23 with quantity 100.0 of 'Purchase product A @ Regional DC 2 from Supplier 1': + 0 24 Replenish product A @ Regional DC 2 100.0 + 1 23 Purchase product A @ Regional DC 2 from Supplier 1 100.0 + 2 25 Ship product A from Regional DC 2 to Local DC 2a 100.0 + 3 26 Ship product A @ Local DC 2a 100.0 +Upstream pegging of operationplan with id 24 with quantity 100.0 of 'Replenish product A @ Regional DC 2': + 0 24 Replenish product A @ Regional DC 2 100.0 + 1 23 Purchase product A @ Regional DC 2 from Supplier 1 100.0 +Downstream pegging of operationplan with id 24 with quantity 100.0 of 'Replenish product A @ Regional DC 2': + 0 24 Replenish product A @ Regional DC 2 100.0 + 1 23 Purchase product A @ Regional DC 2 from Supplier 1 100.0 + 2 25 Ship product A from Regional DC 2 to Local DC 2a 100.0 + 3 26 Ship product A @ Local DC 2a 100.0 +Upstream pegging of operationplan with id 14 with quantity 100.0 of 'Replenish product A @ Regional DC 2': + 0 14 Replenish product A @ Regional DC 2 100.0 + 1 15 Ship product A from Central WH to Regional DC 2 100.0 + 2 13 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 14 with quantity 100.0 of 'Replenish product A @ Regional DC 2': + 0 14 Replenish product A @ Regional DC 2 100.0 + 1 15 Ship product A from Central WH to Regional DC 2 100.0 + 2 16 Ship product A from Regional DC 2 to Local DC 2b 100.0 + 3 17 Ship product A @ Local DC 2b 100.0 +Upstream pegging of operationplan with id 4 with quantity 100.0 of 'Ship In transit product from Source': + 0 4 Ship In transit product from Source 100.0 +Downstream pegging of operationplan with id 4 with quantity 100.0 of 'Ship In transit product from Source': + 0 4 Ship In transit product from Source 100.0 +Upstream pegging of operationplan with id 2 with quantity 100.0 of 'Ship In transit product from Source to Destination': + 0 2 Ship In transit product from Source to Destination 100.0 +Downstream pegging of operationplan with id 2 with quantity 100.0 of 'Ship In transit product from Source to Destination': + 0 2 Ship In transit product from Source to Destination 100.0 +Upstream pegging of operationplan with id 3 with quantity 100.0 of 'Ship In transit product to Destination': + 0 3 Ship In transit product to Destination 100.0 +Downstream pegging of operationplan with id 3 with quantity 100.0 of 'Ship In transit product to Destination': + 0 3 Ship In transit product to Destination 100.0 +Upstream pegging of operationplan with id 7 with quantity 10.0 of 'Ship product A @ Local DC 1a': + 0 7 Ship product A @ Local DC 1a 10.0 + 1 6 Ship product A from Regional DC 1 to Local DC 1a 10.0 + 2 5 Ship product A from Central WH to Regional DC 1 10.0 + 3 product A @ Central WH Inventory product A @ Central WH 10.0 +Downstream pegging of operationplan with id 7 with quantity 10.0 of 'Ship product A @ Local DC 1a': + 0 7 Ship product A @ Local DC 1a 10.0 +Upstream pegging of operationplan with id 11 with quantity 50.0 of 'Ship product A @ Local DC 1a': + 0 11 Ship product A @ Local DC 1a 50.0 + 1 10 Ship product A from Regional DC 1 to Local DC 1a 50.0 + 2 9 Ship product A from Central WH to Regional DC 1 50.0 + 3 8 Purchase product A @ Central WH from Supplier 1 50.0 +Downstream pegging of operationplan with id 11 with quantity 50.0 of 'Ship product A @ Local DC 1a': + 0 11 Ship product A @ Local DC 1a 50.0 +Upstream pegging of operationplan with id 12 with quantity 40.0 of 'Ship product A @ Local DC 1a': + 0 12 Ship product A @ Local DC 1a 40.0 + 1 10 Ship product A from Regional DC 1 to Local DC 1a 40.0 + 2 9 Ship product A from Central WH to Regional DC 1 40.0 + 3 8 Purchase product A @ Central WH from Supplier 1 40.0 +Downstream pegging of operationplan with id 12 with quantity 40.0 of 'Ship product A @ Local DC 1a': + 0 12 Ship product A @ Local DC 1a 40.0 +Upstream pegging of operationplan with id 21 with quantity 100.0 of 'Ship product A @ Local DC 1b': + 0 21 Ship product A @ Local DC 1b 100.0 + 1 20 Ship product A from Regional DC 1 to Local DC 1b 100.0 + 2 19 Ship product A from Central WH to Regional DC 1 100.0 + 3 18 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 21 with quantity 100.0 of 'Ship product A @ Local DC 1b': + 0 21 Ship product A @ Local DC 1b 100.0 +Upstream pegging of operationplan with id 26 with quantity 100.0 of 'Ship product A @ Local DC 2a': + 0 26 Ship product A @ Local DC 2a 100.0 + 1 25 Ship product A from Regional DC 2 to Local DC 2a 100.0 + 2 24 Replenish product A @ Regional DC 2 100.0 + 3 23 Purchase product A @ Regional DC 2 from Supplier 1 100.0 +Downstream pegging of operationplan with id 26 with quantity 100.0 of 'Ship product A @ Local DC 2a': + 0 26 Ship product A @ Local DC 2a 100.0 +Upstream pegging of operationplan with id 17 with quantity 100.0 of 'Ship product A @ Local DC 2b': + 0 17 Ship product A @ Local DC 2b 100.0 + 1 16 Ship product A from Regional DC 2 to Local DC 2b 100.0 + 2 14 Replenish product A @ Regional DC 2 100.0 + 3 15 Ship product A from Central WH to Regional DC 2 100.0 + 4 13 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 17 with quantity 100.0 of 'Ship product A @ Local DC 2b': + 0 17 Ship product A @ Local DC 2b 100.0 +Upstream pegging of operationplan with id 1 with quantity 100.0 of 'Ship product A from Central WH to Local DC 2b': + 0 1 Ship product A from Central WH to Local DC 2b 100.0 + 1 22 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 1 with quantity 100.0 of 'Ship product A from Central WH to Local DC 2b': + 0 1 Ship product A from Central WH to Local DC 2b 100.0 +Upstream pegging of operationplan with id 5 with quantity 10.0 of 'Ship product A from Central WH to Regional DC 1': + 0 5 Ship product A from Central WH to Regional DC 1 10.0 + 1 product A @ Central WH Inventory product A @ Central WH 10.0 +Downstream pegging of operationplan with id 5 with quantity 10.0 of 'Ship product A from Central WH to Regional DC 1': + 0 5 Ship product A from Central WH to Regional DC 1 10.0 + 1 6 Ship product A from Regional DC 1 to Local DC 1a 10.0 + 2 7 Ship product A @ Local DC 1a 10.0 +Upstream pegging of operationplan with id 9 with quantity 90.0 of 'Ship product A from Central WH to Regional DC 1': + 0 9 Ship product A from Central WH to Regional DC 1 90.0 + 1 8 Purchase product A @ Central WH from Supplier 1 90.0 +Downstream pegging of operationplan with id 9 with quantity 90.0 of 'Ship product A from Central WH to Regional DC 1': + 0 9 Ship product A from Central WH to Regional DC 1 90.0 + 1 10 Ship product A from Regional DC 1 to Local DC 1a 90.0 + 2 11 Ship product A @ Local DC 1a 50.0 + 2 12 Ship product A @ Local DC 1a 40.0 +Upstream pegging of operationplan with id 19 with quantity 100.0 of 'Ship product A from Central WH to Regional DC 1': + 0 19 Ship product A from Central WH to Regional DC 1 100.0 + 1 18 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 19 with quantity 100.0 of 'Ship product A from Central WH to Regional DC 1': + 0 19 Ship product A from Central WH to Regional DC 1 100.0 + 1 20 Ship product A from Regional DC 1 to Local DC 1b 100.0 + 2 21 Ship product A @ Local DC 1b 100.0 +Upstream pegging of operationplan with id 15 with quantity 100.0 of 'Ship product A from Central WH to Regional DC 2': + 0 14 Replenish product A @ Regional DC 2 100.0 + 1 15 Ship product A from Central WH to Regional DC 2 100.0 + 2 13 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 15 with quantity 100.0 of 'Ship product A from Central WH to Regional DC 2': + 0 14 Replenish product A @ Regional DC 2 100.0 + 1 15 Ship product A from Central WH to Regional DC 2 100.0 + 2 16 Ship product A from Regional DC 2 to Local DC 2b 100.0 + 3 17 Ship product A @ Local DC 2b 100.0 +Upstream pegging of operationplan with id 6 with quantity 10.0 of 'Ship product A from Regional DC 1 to Local DC 1a': + 0 6 Ship product A from Regional DC 1 to Local DC 1a 10.0 + 1 5 Ship product A from Central WH to Regional DC 1 10.0 + 2 product A @ Central WH Inventory product A @ Central WH 10.0 +Downstream pegging of operationplan with id 6 with quantity 10.0 of 'Ship product A from Regional DC 1 to Local DC 1a': + 0 6 Ship product A from Regional DC 1 to Local DC 1a 10.0 + 1 7 Ship product A @ Local DC 1a 10.0 +Upstream pegging of operationplan with id 10 with quantity 90.0 of 'Ship product A from Regional DC 1 to Local DC 1a': + 0 10 Ship product A from Regional DC 1 to Local DC 1a 90.0 + 1 9 Ship product A from Central WH to Regional DC 1 90.0 + 2 8 Purchase product A @ Central WH from Supplier 1 90.0 +Downstream pegging of operationplan with id 10 with quantity 90.0 of 'Ship product A from Regional DC 1 to Local DC 1a': + 0 10 Ship product A from Regional DC 1 to Local DC 1a 90.0 + 1 11 Ship product A @ Local DC 1a 50.0 + 1 12 Ship product A @ Local DC 1a 40.0 +Upstream pegging of operationplan with id 20 with quantity 100.0 of 'Ship product A from Regional DC 1 to Local DC 1b': + 0 20 Ship product A from Regional DC 1 to Local DC 1b 100.0 + 1 19 Ship product A from Central WH to Regional DC 1 100.0 + 2 18 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 20 with quantity 100.0 of 'Ship product A from Regional DC 1 to Local DC 1b': + 0 20 Ship product A from Regional DC 1 to Local DC 1b 100.0 + 1 21 Ship product A @ Local DC 1b 100.0 +Upstream pegging of operationplan with id 25 with quantity 100.0 of 'Ship product A from Regional DC 2 to Local DC 2a': + 0 25 Ship product A from Regional DC 2 to Local DC 2a 100.0 + 1 24 Replenish product A @ Regional DC 2 100.0 + 2 23 Purchase product A @ Regional DC 2 from Supplier 1 100.0 +Downstream pegging of operationplan with id 25 with quantity 100.0 of 'Ship product A from Regional DC 2 to Local DC 2a': + 0 25 Ship product A from Regional DC 2 to Local DC 2a 100.0 + 1 26 Ship product A @ Local DC 2a 100.0 +Upstream pegging of operationplan with id 16 with quantity 100.0 of 'Ship product A from Regional DC 2 to Local DC 2b': + 0 16 Ship product A from Regional DC 2 to Local DC 2b 100.0 + 1 14 Replenish product A @ Regional DC 2 100.0 + 2 15 Ship product A from Central WH to Regional DC 2 100.0 + 3 13 Purchase product A @ Central WH from Supplier 1 100.0 +Downstream pegging of operationplan with id 16 with quantity 100.0 of 'Ship product A from Regional DC 2 to Local DC 2b': + 0 16 Ship product A from Regional DC 2 to Local DC 2b 100.0 + 1 17 Ship product A @ Local DC 2b 100.0 +Pegging of demand order 1a for product A with quantity 50.0: + 0 7 Ship product A @ Local DC 1a 10.0 + 1 6 Ship product A from Regional DC 1 to Local DC 1a 10.0 + 2 5 Ship product A from Central WH to Regional DC 1 10.0 + 3 product A @ Central WH Inventory product A @ Central WH 10.0 + 0 12 Ship product A @ Local DC 1a 40.0 + 1 10 Ship product A from Regional DC 1 to Local DC 1a 40.0 + 2 9 Ship product A from Central WH to Regional DC 1 40.0 + 3 8 Purchase product A @ Central WH from Supplier 1 40.0 +Pegging of demand order 1b for product A with quantity 50.0: + 0 11 Ship product A @ Local DC 1a 50.0 + 1 10 Ship product A from Regional DC 1 to Local DC 1a 50.0 + 2 9 Ship product A from Central WH to Regional DC 1 50.0 + 3 8 Purchase product A @ Central WH from Supplier 1 50.0 +Pegging of demand order 2 for product A with quantity 100.0: + 0 21 Ship product A @ Local DC 1b 100.0 + 1 20 Ship product A from Regional DC 1 to Local DC 1b 100.0 + 2 19 Ship product A from Central WH to Regional DC 1 100.0 + 3 18 Purchase product A @ Central WH from Supplier 1 100.0 +Pegging of demand order 3 for product A with quantity 100.0: + 0 26 Ship product A @ Local DC 2a 100.0 + 1 25 Ship product A from Regional DC 2 to Local DC 2a 100.0 + 2 24 Replenish product A @ Regional DC 2 100.0 + 3 23 Purchase product A @ Regional DC 2 from Supplier 1 100.0 +Pegging of demand order 4 for product A with quantity 100.0: + 0 17 Ship product A @ Local DC 2b 100.0 + 1 16 Ship product A from Regional DC 2 to Local DC 2b 100.0 + 2 14 Replenish product A @ Regional DC 2 100.0 + 3 15 Ship product A from Central WH to Regional DC 2 100.0 + 4 13 Purchase product A @ Central WH from Supplier 1 100.0 diff --git a/test/pegging_6/pegging_6.xml b/test/pegging_6/pegging_6.xml new file mode 100644 index 0000000000..933b90bf66 --- /dev/null +++ b/test/pegging_6/pegging_6.xml @@ -0,0 +1,195 @@ + + + Test model for suppliers + + This test model demonstrates the distribution network modeling features. + + 2015-01-01T00:00:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + P7D + + 1 + + + + P7D + + 2 + + + + + + P10D + + 1 + + + + P12D + + 1 + + + + + P3D + 1 + + + + + P3D + 1 + + + + + P3D + 1 + + + + + P3D + 1 + + + + + + + 2 + + + + + + + + + + + 50 + 2015-01-01T00:00:00 + 1 + + + + + 50 + 2015-01-01T00:00:00 + 1 + + + + + + 100 + 2015-03-01T00:00:00 + 1 + + + + + + 100 + 2015-01-01T00:00:00 + 1 + + + + + + 100 + 2015-03-01T00:00:00 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/pegging_7/pegging_7.1.expect b/test/pegging_7/pegging_7.1.expect new file mode 100644 index 0000000000..221af8a38d --- /dev/null +++ b/test/pegging_7/pegging_7.1.expect @@ -0,0 +1,246 @@ +Upstream pegging of operationplan with id component A @ factory with quantity 1.0 of 'Inventory component A @ factory': + 0 component A @ factory Inventory component A @ factory 1.0 +Downstream pegging of operationplan with id component A @ factory with quantity 1.0 of 'Inventory component A @ factory': + 0 component A @ factory Inventory component A @ factory 1.0 + 1 1 assembly 1.0 + 2 2 Ship product @ factory 1.0 +Upstream pegging of operationplan with id component B @ factory with quantity 4.0 of 'Inventory component B @ factory': + 0 component B @ factory Inventory component B @ factory 4.0 +Downstream pegging of operationplan with id component B @ factory with quantity 4.0 of 'Inventory component B @ factory': + 0 component B @ factory Inventory component B @ factory 4.0 + 1 3 assembly 1.0 + 2 2 Ship product @ factory 1.0 + 1 4 assembly 1.0 + 2 2 Ship product @ factory 1.0 +Upstream pegging of operationplan with id component C @ factory with quantity 5.0 of 'Inventory component C @ factory': + 0 component C @ factory Inventory component C @ factory 5.0 +Downstream pegging of operationplan with id component C @ factory with quantity 5.0 of 'Inventory component C @ factory': + 0 component C @ factory Inventory component C @ factory 5.0 + 1 5 assembly 1.0 + 2 6 Ship product @ factory 1.0 + 1 7 assembly 1.0 + 2 6 Ship product @ factory 1.0 + 1 1 assembly 1.0 + 2 2 Ship product @ factory 1.0 + 1 3 assembly 1.0 + 2 2 Ship product @ factory 1.0 + 1 4 assembly 1.0 + 2 2 Ship product @ factory 1.0 +Upstream pegging of operationplan with id component F @ factory with quantity 5.0 of 'Inventory component F @ factory': + 0 component F @ factory Inventory component F @ factory 5.0 +Downstream pegging of operationplan with id component F @ factory with quantity 5.0 of 'Inventory component F @ factory': + 0 component F @ factory Inventory component F @ factory 5.0 + 1 5 assembly 1.0 + 2 6 Ship product @ factory 1.0 + 1 7 assembly 1.0 + 2 6 Ship product @ factory 1.0 + 1 1 assembly 1.0 + 2 2 Ship product @ factory 1.0 + 1 3 assembly 1.0 + 2 2 Ship product @ factory 1.0 + 1 4 assembly 1.0 + 2 2 Ship product @ factory 1.0 +Upstream pegging of operationplan with id component G @ factory with quantity 5.0 of 'Inventory component G @ factory': + 0 component G @ factory Inventory component G @ factory 5.0 +Downstream pegging of operationplan with id component G @ factory with quantity 5.0 of 'Inventory component G @ factory': + 0 component G @ factory Inventory component G @ factory 5.0 + 1 8 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 10 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 11 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 12 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 13 assembly 1.0 + 2 9 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 14 with quantity 5.0 of 'Purchase component A @ factory from Component supplier': + 0 14 Purchase component A @ factory from Component supplier 5.0 +Downstream pegging of operationplan with id 14 with quantity 5.0 of 'Purchase component A @ factory from Component supplier': + 0 14 Purchase component A @ factory from Component supplier 5.0 + 1 8 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 10 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 11 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 12 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 13 assembly 1.0 + 2 9 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 15 with quantity 4.0 of 'Purchase component B @ factory from Component supplier': + 0 15 Purchase component B @ factory from Component supplier 4.0 +Downstream pegging of operationplan with id 15 with quantity 4.0 of 'Purchase component B @ factory from Component supplier': + 0 15 Purchase component B @ factory from Component supplier 4.0 + 1 5 assembly 1.0 + 2 6 Ship product @ factory 1.0 + 1 7 assembly 1.0 + 2 6 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 16 with quantity 5.0 of 'Purchase component C @ factory from Component supplier': + 0 16 Purchase component C @ factory from Component supplier 5.0 +Downstream pegging of operationplan with id 16 with quantity 5.0 of 'Purchase component C @ factory from Component supplier': + 0 16 Purchase component C @ factory from Component supplier 5.0 + 1 8 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 10 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 11 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 12 assembly 1.0 + 2 9 Ship product @ factory 1.0 + 1 13 assembly 1.0 + 2 9 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 2 with quantity 3.0 of 'Ship product @ factory': + 0 2 Ship product @ factory 3.0 + 1 4 assembly 1.0 + 2 component F @ factory Inventory component F @ factory 1.0 + 2 component C @ factory Inventory component C @ factory 1.0 + 2 component B @ factory Inventory component B @ factory 2.0 + 1 3 assembly 1.0 + 2 component F @ factory Inventory component F @ factory 1.0 + 2 component C @ factory Inventory component C @ factory 1.0 + 2 component B @ factory Inventory component B @ factory 2.0 + 1 1 assembly 1.0 + 2 component F @ factory Inventory component F @ factory 1.0 + 2 component C @ factory Inventory component C @ factory 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 +Downstream pegging of operationplan with id 2 with quantity 3.0 of 'Ship product @ factory': + 0 2 Ship product @ factory 3.0 +Upstream pegging of operationplan with id 6 with quantity 2.0 of 'Ship product @ factory': + 0 6 Ship product @ factory 2.0 + 1 7 assembly 1.0 + 2 component F @ factory Inventory component F @ factory 1.0 + 2 component C @ factory Inventory component C @ factory 1.0 + 2 15 Purchase component B @ factory from Component supplier 2.0 + 1 5 assembly 1.0 + 2 component F @ factory Inventory component F @ factory 1.0 + 2 component C @ factory Inventory component C @ factory 1.0 + 2 15 Purchase component B @ factory from Component supplier 2.0 +Downstream pegging of operationplan with id 6 with quantity 2.0 of 'Ship product @ factory': + 0 6 Ship product @ factory 2.0 +Upstream pegging of operationplan with id 9 with quantity 5.0 of 'Ship product @ factory': + 0 9 Ship product @ factory 5.0 + 1 13 assembly 1.0 + 2 component G @ factory Inventory component G @ factory 1.0 + 2 16 Purchase component C @ factory from Component supplier 1.0 + 2 14 Purchase component A @ factory from Component supplier 1.0 + 1 12 assembly 1.0 + 2 component G @ factory Inventory component G @ factory 1.0 + 2 16 Purchase component C @ factory from Component supplier 1.0 + 2 14 Purchase component A @ factory from Component supplier 1.0 + 1 11 assembly 1.0 + 2 component G @ factory Inventory component G @ factory 1.0 + 2 16 Purchase component C @ factory from Component supplier 1.0 + 2 14 Purchase component A @ factory from Component supplier 1.0 + 1 10 assembly 1.0 + 2 component G @ factory Inventory component G @ factory 1.0 + 2 16 Purchase component C @ factory from Component supplier 1.0 + 2 14 Purchase component A @ factory from Component supplier 1.0 + 1 8 assembly 1.0 + 2 component G @ factory Inventory component G @ factory 1.0 + 2 16 Purchase component C @ factory from Component supplier 1.0 + 2 14 Purchase component A @ factory from Component supplier 1.0 +Downstream pegging of operationplan with id 9 with quantity 5.0 of 'Ship product @ factory': + 0 9 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 4 with quantity 1.0 of 'assembly': + 0 4 assembly 1.0 + 1 component F @ factory Inventory component F @ factory 1.0 + 1 component C @ factory Inventory component C @ factory 1.0 + 1 component B @ factory Inventory component B @ factory 2.0 +Downstream pegging of operationplan with id 4 with quantity 1.0 of 'assembly': + 0 4 assembly 1.0 + 1 2 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 3 with quantity 1.0 of 'assembly': + 0 3 assembly 1.0 + 1 component F @ factory Inventory component F @ factory 1.0 + 1 component C @ factory Inventory component C @ factory 1.0 + 1 component B @ factory Inventory component B @ factory 2.0 +Downstream pegging of operationplan with id 3 with quantity 1.0 of 'assembly': + 0 3 assembly 1.0 + 1 2 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 1 with quantity 1.0 of 'assembly': + 0 1 assembly 1.0 + 1 component F @ factory Inventory component F @ factory 1.0 + 1 component C @ factory Inventory component C @ factory 1.0 + 1 component A @ factory Inventory component A @ factory 1.0 +Downstream pegging of operationplan with id 1 with quantity 1.0 of 'assembly': + 0 1 assembly 1.0 + 1 2 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 7 with quantity 1.0 of 'assembly': + 0 7 assembly 1.0 + 1 component F @ factory Inventory component F @ factory 1.0 + 1 component C @ factory Inventory component C @ factory 1.0 + 1 15 Purchase component B @ factory from Component supplier 2.0 +Downstream pegging of operationplan with id 7 with quantity 1.0 of 'assembly': + 0 7 assembly 1.0 + 1 6 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 5 with quantity 1.0 of 'assembly': + 0 5 assembly 1.0 + 1 component F @ factory Inventory component F @ factory 1.0 + 1 component C @ factory Inventory component C @ factory 1.0 + 1 15 Purchase component B @ factory from Component supplier 2.0 +Downstream pegging of operationplan with id 5 with quantity 1.0 of 'assembly': + 0 5 assembly 1.0 + 1 6 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 13 with quantity 1.0 of 'assembly': + 0 13 assembly 1.0 + 1 component G @ factory Inventory component G @ factory 1.0 + 1 16 Purchase component C @ factory from Component supplier 1.0 + 1 14 Purchase component A @ factory from Component supplier 1.0 +Downstream pegging of operationplan with id 13 with quantity 1.0 of 'assembly': + 0 13 assembly 1.0 + 1 9 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 12 with quantity 1.0 of 'assembly': + 0 12 assembly 1.0 + 1 component G @ factory Inventory component G @ factory 1.0 + 1 16 Purchase component C @ factory from Component supplier 1.0 + 1 14 Purchase component A @ factory from Component supplier 1.0 +Downstream pegging of operationplan with id 12 with quantity 1.0 of 'assembly': + 0 12 assembly 1.0 + 1 9 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 11 with quantity 1.0 of 'assembly': + 0 11 assembly 1.0 + 1 component G @ factory Inventory component G @ factory 1.0 + 1 16 Purchase component C @ factory from Component supplier 1.0 + 1 14 Purchase component A @ factory from Component supplier 1.0 +Downstream pegging of operationplan with id 11 with quantity 1.0 of 'assembly': + 0 11 assembly 1.0 + 1 9 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 10 with quantity 1.0 of 'assembly': + 0 10 assembly 1.0 + 1 component G @ factory Inventory component G @ factory 1.0 + 1 16 Purchase component C @ factory from Component supplier 1.0 + 1 14 Purchase component A @ factory from Component supplier 1.0 +Downstream pegging of operationplan with id 10 with quantity 1.0 of 'assembly': + 0 10 assembly 1.0 + 1 9 Ship product @ factory 1.0 +Upstream pegging of operationplan with id 8 with quantity 1.0 of 'assembly': + 0 8 assembly 1.0 + 1 component G @ factory Inventory component G @ factory 1.0 + 1 16 Purchase component C @ factory from Component supplier 1.0 + 1 14 Purchase component A @ factory from Component supplier 1.0 +Downstream pegging of operationplan with id 8 with quantity 1.0 of 'assembly': + 0 8 assembly 1.0 + 1 9 Ship product @ factory 1.0 +Pegging of demand order 1 with quantity 10.0: + 0 2 Ship product @ factory 3.0 + 1 4 assembly 1.0 + 2 component F @ factory Inventory component F @ factory 5.0 + 2 component C @ factory Inventory component C @ factory 5.0 + 2 component B @ factory Inventory component B @ factory 4.0 + 1 3 assembly 1.0 + 1 1 assembly 1.0 + 2 component A @ factory Inventory component A @ factory 1.0 + 0 6 Ship product @ factory 2.0 + 1 7 assembly 1.0 + 2 15 Purchase component B @ factory from Component supplier 4.0 + 1 5 assembly 1.0 + 0 9 Ship product @ factory 5.0 + 1 13 assembly 1.0 + 2 component G @ factory Inventory component G @ factory 5.0 + 2 16 Purchase component C @ factory from Component supplier 5.0 + 2 14 Purchase component A @ factory from Component supplier 5.0 + 1 12 assembly 1.0 + 1 11 assembly 1.0 + 1 10 assembly 1.0 + 1 8 assembly 1.0 diff --git a/test/pegging_7/pegging_7.xml b/test/pegging_7/pegging_7.xml new file mode 100644 index 0000000000..32a91c0855 --- /dev/null +++ b/test/pegging_7/pegging_7.xml @@ -0,0 +1,214 @@ + + + + Test model for alternate flows + + This test verifies the behavior of alternate flows. + An assembly operation is modeled which has alternate components: + * Component A or component B can be used in the assembly. + Component A is the preferred one, and Component B is only used + when constraints are found on A. + * Component C can be replaced by component D till a certain date, or by + component E valid from a date. + The validity periods of the components D and E can overlap. + * Component F is used till a certain date, after which the component G is + used. The validity periods of the components can overlap. + + 2009-01-01T00:00:00 + + + + + 1 + + + + + 4 + + + + + 5 + + + + + 0 + + + + + 0 + + + + + 5 + + + + + 5 + + + + + + + + P7D + + + + + + + + P5D + + + + + + + + P7D + + + + + + + + P7D + + + + + + + + P7D + + + + + + + + P7D + + + + + + + + P5D + + + + + + + + + + + + 1 + + + + -1 + 1 + group1 + + + + -2 + 2 + group1 + + + + -1 + 1 + group2 + + + + -1 + + 2 + group2 + + + + -1 + + 3 + group2 + + + + -1 + 1 + + group3 + + + + + -1 + 2 + group3 + + + + + + + 10 + 2009-01-04T00:00:00 + 11 + + + + + + + + diff --git a/test/pegging_8/pegging_8.1.expect b/test/pegging_8/pegging_8.1.expect new file mode 100644 index 0000000000..ffc158a4fc --- /dev/null +++ b/test/pegging_8/pegging_8.1.expect @@ -0,0 +1,250 @@ +Upstream pegging of operationplan with id buffer 1 with quantity 10.0 of 'Inventory buffer 1': + 0 buffer 1 Inventory buffer 1 10.0 +Downstream pegging of operationplan with id buffer 1 with quantity 10.0 of 'Inventory buffer 1': + 0 buffer 1 Inventory buffer 1 10.0 + 1 4002 delivery 1 5.0 +Upstream pegging of operationplan with id buffer 2 with quantity 10.0 of 'Inventory buffer 2': + 0 buffer 2 Inventory buffer 2 10.0 +Downstream pegging of operationplan with id buffer 2 with quantity 10.0 of 'Inventory buffer 2': + 0 buffer 2 Inventory buffer 2 10.0 + 1 4003 delivery 2 5.0 +Upstream pegging of operationplan with id buffer 3 with quantity 10.0 of 'Inventory buffer 3': + 0 buffer 3 Inventory buffer 3 10.0 +Downstream pegging of operationplan with id buffer 3 with quantity 10.0 of 'Inventory buffer 3': + 0 buffer 3 Inventory buffer 3 10.0 + 1 4004 delivery 3 5.0 +Upstream pegging of operationplan with id buffer 4 with quantity 10.0 of 'Inventory buffer 4': + 0 buffer 4 Inventory buffer 4 10.0 +Downstream pegging of operationplan with id buffer 4 with quantity 10.0 of 'Inventory buffer 4': + 0 buffer 4 Inventory buffer 4 10.0 + 1 4005 delivery 4 5.0 +Upstream pegging of operationplan with id 4002 with quantity 5.0 of 'delivery 1': + 0 4002 delivery 1 5.0 + 1 buffer 1 Inventory buffer 1 10.0 +Downstream pegging of operationplan with id 4002 with quantity 5.0 of 'delivery 1': + 0 4002 delivery 1 5.0 +Upstream pegging of operationplan with id 4006 with quantity 10.0 of 'delivery 1': + 0 4006 delivery 1 10.0 + 1 4007 make 1 10.0 + 2 1001 supply 1 20.0 +Downstream pegging of operationplan with id 4006 with quantity 10.0 of 'delivery 1': + 0 4006 delivery 1 10.0 +Upstream pegging of operationplan with id 4008 with quantity 10.0 of 'delivery 1': + 0 4008 delivery 1 10.0 + 1 4007 make 1 10.0 + 2 1001 supply 1 20.0 +Downstream pegging of operationplan with id 4008 with quantity 10.0 of 'delivery 1': + 0 4008 delivery 1 10.0 +Upstream pegging of operationplan with id 4009 with quantity 10.0 of 'delivery 1': + 0 4009 delivery 1 10.0 + 1 4007 make 1 10.0 + 2 1001 supply 1 20.0 +Downstream pegging of operationplan with id 4009 with quantity 10.0 of 'delivery 1': + 0 4009 delivery 1 10.0 +Upstream pegging of operationplan with id 4010 with quantity 10.0 of 'delivery 1': + 0 4010 delivery 1 10.0 + 1 4007 make 1 10.0 + 2 1001 supply 1 20.0 +Downstream pegging of operationplan with id 4010 with quantity 10.0 of 'delivery 1': + 0 4010 delivery 1 10.0 +Upstream pegging of operationplan with id 4011 with quantity 10.0 of 'delivery 1': + 0 4011 delivery 1 10.0 + 1 4007 make 1 10.0 + 2 1001 supply 1 20.0 +Downstream pegging of operationplan with id 4011 with quantity 10.0 of 'delivery 1': + 0 4011 delivery 1 10.0 +Upstream pegging of operationplan with id 4012 with quantity 10.0 of 'delivery 1': + 0 4012 delivery 1 10.0 + 1 4013 make 1 10.0 + 2 1002 supply 1 20.0 +Downstream pegging of operationplan with id 4012 with quantity 10.0 of 'delivery 1': + 0 4012 delivery 1 10.0 +Upstream pegging of operationplan with id 4014 with quantity 10.0 of 'delivery 1': + 0 4014 delivery 1 10.0 + 1 4013 make 1 10.0 + 2 1002 supply 1 20.0 +Downstream pegging of operationplan with id 4014 with quantity 10.0 of 'delivery 1': + 0 4014 delivery 1 10.0 +Upstream pegging of operationplan with id 4015 with quantity 10.0 of 'delivery 1': + 0 4015 delivery 1 10.0 + 1 4013 make 1 10.0 + 2 1002 supply 1 20.0 +Downstream pegging of operationplan with id 4015 with quantity 10.0 of 'delivery 1': + 0 4015 delivery 1 10.0 +Upstream pegging of operationplan with id 4016 with quantity 10.0 of 'delivery 1': + 0 4016 delivery 1 10.0 + 1 4013 make 1 10.0 + 2 1002 supply 1 20.0 +Downstream pegging of operationplan with id 4016 with quantity 10.0 of 'delivery 1': + 0 4016 delivery 1 10.0 +Upstream pegging of operationplan with id 4017 with quantity 5.0 of 'delivery 1': + 0 4017 delivery 1 5.0 + 1 4013 make 1 5.0 + 2 1002 supply 1 10.0 +Downstream pegging of operationplan with id 4017 with quantity 5.0 of 'delivery 1': + 0 4017 delivery 1 5.0 +Upstream pegging of operationplan with id 4003 with quantity 5.0 of 'delivery 2': + 0 4003 delivery 2 5.0 + 1 buffer 2 Inventory buffer 2 10.0 +Downstream pegging of operationplan with id 4003 with quantity 5.0 of 'delivery 2': + 0 4003 delivery 2 5.0 +Upstream pegging of operationplan with id 4018 with quantity 50.0 of 'delivery 2': + 0 4018 delivery 2 50.0 + 1 4019 make 2 50.0 + 2 2001 supply 2 100.0 +Downstream pegging of operationplan with id 4018 with quantity 50.0 of 'delivery 2': + 0 4018 delivery 2 50.0 +Upstream pegging of operationplan with id 4020 with quantity 25.0 of 'delivery 2': + 0 4020 delivery 2 25.0 + 1 4021 make 2 25.0 + 2 2002 supply 2 50.0 +Downstream pegging of operationplan with id 4020 with quantity 25.0 of 'delivery 2': + 0 4020 delivery 2 25.0 +Upstream pegging of operationplan with id 4004 with quantity 5.0 of 'delivery 3': + 0 4004 delivery 3 5.0 + 1 buffer 3 Inventory buffer 3 10.0 +Downstream pegging of operationplan with id 4004 with quantity 5.0 of 'delivery 3': + 0 4004 delivery 3 5.0 +Upstream pegging of operationplan with id 4022 with quantity 95.0 of 'delivery 3': + 0 4022 delivery 3 95.0 + 1 4023 make 3 95.0 + 2 3001 supply 3 190.0 +Downstream pegging of operationplan with id 4022 with quantity 95.0 of 'delivery 3': + 0 4022 delivery 3 95.0 +Upstream pegging of operationplan with id 4005 with quantity 5.0 of 'delivery 4': + 0 4005 delivery 4 5.0 + 1 buffer 4 Inventory buffer 4 10.0 +Downstream pegging of operationplan with id 4005 with quantity 5.0 of 'delivery 4': + 0 4005 delivery 4 5.0 +Upstream pegging of operationplan with id 4024 with quantity 95.0 of 'delivery 4': + 0 4024 delivery 4 95.0 + 1 4025 make 4 95.0 + 2 4001 supply 4 190.0 +Downstream pegging of operationplan with id 4024 with quantity 95.0 of 'delivery 4': + 0 4024 delivery 4 95.0 +Upstream pegging of operationplan with id 4007 with quantity 50.0 of 'make 1': + 0 4007 make 1 50.0 + 1 1001 supply 1 100.0 +Downstream pegging of operationplan with id 4007 with quantity 50.0 of 'make 1': + 0 4007 make 1 50.0 + 1 4011 delivery 1 10.0 + 1 4010 delivery 1 10.0 + 1 4009 delivery 1 10.0 + 1 4008 delivery 1 10.0 + 1 4006 delivery 1 10.0 +Upstream pegging of operationplan with id 4013 with quantity 47.0 of 'make 1': + 0 4013 make 1 47.0 + 1 1002 supply 1 94.0 +Downstream pegging of operationplan with id 4013 with quantity 47.0 of 'make 1': + 0 4013 make 1 47.0 + 1 4016 delivery 1 10.0 + 1 4015 delivery 1 10.0 + 1 4014 delivery 1 10.0 + 1 4012 delivery 1 10.0 + 1 4017 delivery 1 5.0 +Upstream pegging of operationplan with id 4019 with quantity 50.0 of 'make 2': + 0 4019 make 2 50.0 + 1 2001 supply 2 100.0 +Downstream pegging of operationplan with id 4019 with quantity 50.0 of 'make 2': + 0 4019 make 2 50.0 + 1 4018 delivery 2 50.0 +Upstream pegging of operationplan with id 4021 with quantity 25.0 of 'make 2': + 0 4021 make 2 25.0 + 1 2002 supply 2 50.0 +Downstream pegging of operationplan with id 4021 with quantity 25.0 of 'make 2': + 0 4021 make 2 25.0 + 1 4020 delivery 2 25.0 +Upstream pegging of operationplan with id 4023 with quantity 96.0 of 'make 3': + 0 4023 make 3 96.0 + 1 3001 supply 3 192.0 +Downstream pegging of operationplan with id 4023 with quantity 96.0 of 'make 3': + 0 4023 make 3 96.0 + 1 4022 delivery 3 95.0 +Upstream pegging of operationplan with id 4025 with quantity 98.0 of 'make 4': + 0 4025 make 4 98.0 + 1 4001 supply 4 196.0 +Downstream pegging of operationplan with id 4025 with quantity 98.0 of 'make 4': + 0 4025 make 4 98.0 + 1 4024 delivery 4 95.0 +Upstream pegging of operationplan with id 1001 with quantity 100.0 of 'supply 1': + 0 1001 supply 1 100.0 +Downstream pegging of operationplan with id 1001 with quantity 100.0 of 'supply 1': + 0 1001 supply 1 100.0 + 1 4007 make 1 50.0 + 2 4011 delivery 1 10.0 + 2 4010 delivery 1 10.0 + 2 4009 delivery 1 10.0 + 2 4008 delivery 1 10.0 + 2 4006 delivery 1 10.0 +Upstream pegging of operationplan with id 1002 with quantity 200.0 of 'supply 1': + 0 1002 supply 1 200.0 +Downstream pegging of operationplan with id 1002 with quantity 200.0 of 'supply 1': + 0 1002 supply 1 200.0 + 1 4013 make 1 47.0 + 2 4016 delivery 1 10.0 + 2 4015 delivery 1 10.0 + 2 4014 delivery 1 10.0 + 2 4012 delivery 1 10.0 + 2 4017 delivery 1 5.0 +Upstream pegging of operationplan with id 2001 with quantity 100.0 of 'supply 2': + 0 2001 supply 2 100.0 +Downstream pegging of operationplan with id 2001 with quantity 100.0 of 'supply 2': + 0 2001 supply 2 100.0 + 1 4019 make 2 50.0 + 2 4018 delivery 2 50.0 +Upstream pegging of operationplan with id 2002 with quantity 50.0 of 'supply 2': + 0 2002 supply 2 50.0 +Downstream pegging of operationplan with id 2002 with quantity 50.0 of 'supply 2': + 0 2002 supply 2 50.0 + 1 4021 make 2 25.0 + 2 4020 delivery 2 25.0 +Upstream pegging of operationplan with id 3001 with quantity 1000.0 of 'supply 3': + 0 3001 supply 3 1000.0 +Downstream pegging of operationplan with id 3001 with quantity 1000.0 of 'supply 3': + 0 3001 supply 3 1000.0 + 1 4023 make 3 96.0 + 2 4022 delivery 3 95.0 +Upstream pegging of operationplan with id 4001 with quantity 1000.0 of 'supply 4': + 0 4001 supply 4 1000.0 +Downstream pegging of operationplan with id 4001 with quantity 1000.0 of 'supply 4': + 0 4001 supply 4 1000.0 + 1 4025 make 4 98.0 + 2 4024 delivery 4 95.0 +Pegging of demand order for item 1 with quantity 100.0: + 0 4002 delivery 1 5.0 + 1 buffer 1 Inventory buffer 1 10.0 + 0 4011 delivery 1 10.0 + 1 4007 make 1 50.0 + 2 1001 supply 1 100.0 + 0 4010 delivery 1 10.0 + 0 4009 delivery 1 10.0 + 0 4008 delivery 1 10.0 + 0 4006 delivery 1 10.0 + 0 4017 delivery 1 5.0 + 1 4013 make 1 45.0 + 2 1002 supply 1 90.0 + 0 4016 delivery 1 10.0 + 0 4015 delivery 1 10.0 + 0 4014 delivery 1 10.0 + 0 4012 delivery 1 10.0 +Pegging of demand order for item 2 with quantity 100.0: + 0 4003 delivery 2 5.0 + 1 buffer 2 Inventory buffer 2 10.0 + 0 4018 delivery 2 50.0 + 1 4019 make 2 50.0 + 2 2001 supply 2 100.0 + 0 4020 delivery 2 25.0 + 1 4021 make 2 25.0 + 2 2002 supply 2 50.0 +Pegging of demand order for item 3 with quantity 100.0: + 0 4004 delivery 3 5.0 + 1 buffer 3 Inventory buffer 3 10.0 + 0 4022 delivery 3 95.0 + 1 4023 make 3 95.0 + 2 3001 supply 3 190.0 +Pegging of demand order for item 4 with quantity 100.0: + 0 4005 delivery 4 5.0 + 1 buffer 4 Inventory buffer 4 10.0 + 0 4024 delivery 4 95.0 + 1 4025 make 4 95.0 + 2 4001 supply 4 190.0 diff --git a/test/pegging_8/pegging_8.xml b/test/pegging_8/pegging_8.xml new file mode 100644 index 0000000000..cffeb139a2 --- /dev/null +++ b/test/pegging_8/pegging_8.xml @@ -0,0 +1,267 @@ + + + Material constraint test model + + This model tests the buffer solver code in situations where the minimum onhand + limit is varying. + - 1: Scenario of a constant, non-zero limit. + - 2: Same as 1, but now the supply is limited. The inventory target can't be + reached and all supply is used to satisfy demand. + - 3: Same as 1, but with minimum target varying DEcreasing over time. + - 4: Same as 3, but with minimum target varying INcreasing over time. + + 2009-01-01T00:00:00 + + + + + 4 + 10 + + + + + + + + 4 + 10 + + + + + + + + + + + + + + 10 + + + + + + + + + + + + + + 10 + + + + + + + + + 0 + + + 1 + + + + + 2 + + + + + -2 + + + + + -2 + + + + 0 + + + 1 + + + + + 2 + + + + + -2 + + + + + -2 + + + + 0 + + + 1 + + + + + 2 + + + + + -2 + + + + + -2 + + + + 0 + + + 1 + + + + + 2 + + + + + -2 + + + + + -2 + + + + + + 100 + 1 + 2009-01-10T00:00:00 + + + 10 + P1D + + + + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 100 + confirmed + + + + 2009-01-17T00:00:00 + 2009-01-17T00:00:00 + 200 + confirmed + + + + + + 100 + 1 + 2009-01-10T00:00:00 + + + P1D + + + + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 100 + confirmed + + + + 2009-01-17T00:00:00 + 2009-01-17T00:00:00 + 50 + confirmed + + + + + + 100 + 1 + 2009-01-10T00:00:00 + + + P1D + + + + + + + 2009-01-15T00:00:00 + 2009-01-15T00:00:00 + 1000 + confirmed + + + + + + 100 + 1 + 2009-01-10T00:00:00 + + + P1D + + + + + + + 2009-01-15T00:00:00 + 2009-01-15T00:00:00 + 1000 + confirmed + + + + + diff --git a/test/pegging_9/pegging_9.1.expect b/test/pegging_9/pegging_9.1.expect new file mode 100644 index 0000000000..6858343a69 --- /dev/null +++ b/test/pegging_9/pegging_9.1.expect @@ -0,0 +1,72 @@ +Upstream pegging of operationplan with id A3 @ factory with quantity 10.0 of 'Inventory A3 @ factory': + 0 A3 @ factory Inventory A3 @ factory 10.0 +Downstream pegging of operationplan with id A3 @ factory with quantity 10.0 of 'Inventory A3 @ factory': + 0 A3 @ factory Inventory A3 @ factory 10.0 + 1 1 assembly 5.0 + 2 2 Ship product @ factory 5.0 + 1 3 assembly 5.0 + 2 2 Ship product @ factory 5.0 +Upstream pegging of operationplan with id B3 @ factory with quantity 5.0 of 'Inventory B3 @ factory': + 0 B3 @ factory Inventory B3 @ factory 5.0 +Downstream pegging of operationplan with id B3 @ factory with quantity 5.0 of 'Inventory B3 @ factory': + 0 B3 @ factory Inventory B3 @ factory 5.0 + 1 3 assembly 5.0 + 2 2 Ship product @ factory 5.0 +Upstream pegging of operationplan with id B4 @ factory with quantity 5.0 of 'Inventory B4 @ factory': + 0 B4 @ factory Inventory B4 @ factory 5.0 +Downstream pegging of operationplan with id B4 @ factory with quantity 5.0 of 'Inventory B4 @ factory': + 0 B4 @ factory Inventory B4 @ factory 5.0 + 1 1 assembly 5.0 + 2 2 Ship product @ factory 5.0 +Upstream pegging of operationplan with id C2 @ factory with quantity 10.0 of 'Inventory C2 @ factory': + 0 C2 @ factory Inventory C2 @ factory 10.0 +Downstream pegging of operationplan with id C2 @ factory with quantity 10.0 of 'Inventory C2 @ factory': + 0 C2 @ factory Inventory C2 @ factory 10.0 + 1 1 assembly 5.0 + 2 2 Ship product @ factory 5.0 + 1 3 assembly 5.0 + 2 2 Ship product @ factory 5.0 +Upstream pegging of operationplan with id C3 @ factory with quantity 5.0 of 'Inventory C3 @ factory': + 0 C3 @ factory Inventory C3 @ factory 5.0 +Downstream pegging of operationplan with id C3 @ factory with quantity 5.0 of 'Inventory C3 @ factory': + 0 C3 @ factory Inventory C3 @ factory 5.0 +Upstream pegging of operationplan with id C4 @ factory with quantity 5.0 of 'Inventory C4 @ factory': + 0 C4 @ factory Inventory C4 @ factory 5.0 +Downstream pegging of operationplan with id C4 @ factory with quantity 5.0 of 'Inventory C4 @ factory': + 0 C4 @ factory Inventory C4 @ factory 5.0 +Upstream pegging of operationplan with id 2 with quantity 10.0 of 'Ship product @ factory': + 0 2 Ship product @ factory 10.0 + 1 3 assembly 5.0 + 2 C2 @ factory Inventory C2 @ factory 5.0 + 2 B3 @ factory Inventory B3 @ factory 5.0 + 2 A3 @ factory Inventory A3 @ factory 5.0 + 1 1 assembly 5.0 + 2 C2 @ factory Inventory C2 @ factory 5.0 + 2 B4 @ factory Inventory B4 @ factory 5.0 + 2 A3 @ factory Inventory A3 @ factory 5.0 +Downstream pegging of operationplan with id 2 with quantity 10.0 of 'Ship product @ factory': + 0 2 Ship product @ factory 10.0 +Upstream pegging of operationplan with id 3 with quantity 5.0 of 'assembly': + 0 3 assembly 5.0 + 1 C2 @ factory Inventory C2 @ factory 5.0 + 1 B3 @ factory Inventory B3 @ factory 5.0 + 1 A3 @ factory Inventory A3 @ factory 5.0 +Downstream pegging of operationplan with id 3 with quantity 5.0 of 'assembly': + 0 3 assembly 5.0 + 1 2 Ship product @ factory 5.0 +Upstream pegging of operationplan with id 1 with quantity 5.0 of 'assembly': + 0 1 assembly 5.0 + 1 C2 @ factory Inventory C2 @ factory 5.0 + 1 B4 @ factory Inventory B4 @ factory 5.0 + 1 A3 @ factory Inventory A3 @ factory 5.0 +Downstream pegging of operationplan with id 1 with quantity 5.0 of 'assembly': + 0 1 assembly 5.0 + 1 2 Ship product @ factory 5.0 +Pegging of demand order 1 with quantity 10.0: + 0 2 Ship product @ factory 10.0 + 1 3 assembly 5.0 + 2 C2 @ factory Inventory C2 @ factory 10.0 + 2 B3 @ factory Inventory B3 @ factory 5.0 + 2 A3 @ factory Inventory A3 @ factory 10.0 + 1 1 assembly 5.0 + 2 B4 @ factory Inventory B4 @ factory 5.0 diff --git a/test/pegging_9/pegging_9.xml b/test/pegging_9/pegging_9.xml new file mode 100644 index 0000000000..4d3860bbca --- /dev/null +++ b/test/pegging_9/pegging_9.xml @@ -0,0 +1,291 @@ + + + Test model for alternate flows + + This test verifies the behavior of the user exit that controls alternate flows. + The user exit gives the user control over the allowed combinations of alternate flows. + + In this example, a product uses 3 components, each having some alternates. + - component A1, with alternates A2 and A3 + - component B1, with alternates B2, B3 and B4 + - component C1, with alternates C2, C3 and C4 + + This gives a total of 3*4*4 = 48 possible combinations of the components. + Using the user exit we restrict the allowed combinations to the following: + - A1, B2, C1 + - A1, B2, C2 + - A2, B1, C2 + - A2, B1, C3 + - A3, B3, C4 + - A3, B4, C4 + These restrictions can represent technical constraints in the bill of material + (as provided by the engineers), different versions in the bill of material, + configuration rules imposed by the customer, etc... + + 2009-01-01T00:00:00 + + + + + + P7D + + + + + + + + P5D + + + + + + + + P7D + + + + + + + + P7D + + + + + + + + P7D + + + + + + + + P7D + + + + + + + + P5D + + + + + + + + P7D + + + + + + + + P7D + + + + + + + + P7D + + + + + + + + P10D + + + + + + + + + 0 + + + + + 0 + + + + + 10 + + + + + 0 + + + + + 0 + + + + + 5 + + + + + 5 + + + + + 0 + + + + + 10 + + + + + 5 + + + + + 5 + + + + + + + + + + 1 + + + + -1 + 1 + groupA + + + + -1 + 2 + groupA + + + + -1 + 3 + groupA + + + + -1 + 1 + groupB + + + + -1 + 2 + groupB + + + + -1 + 3 + groupB + + + + -1 + 4 + groupB + + + + -1 + 1 + groupC + + + + -1 + 2 + groupC + + + + -1 + 3 + groupC + + + + -1 + 4 + groupC + + + + + + + 10 + 2009-01-04T00:00:00 + 11 + + P0D + + + + + + + diff --git a/test/rust_parity/forecast_vectors.json b/test/rust_parity/forecast_vectors.json new file mode 100644 index 0000000000..15309ab0fc --- /dev/null +++ b/test/rust_parity/forecast_vectors.json @@ -0,0 +1,39 @@ +[ + { "name": "constant", "history": [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] }, + { "name": "linear_trend", "history": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15] }, + { "name": "outlier_spike", "history": [10, 11, 9, 10, 12, 8, 10, 11, 9, 500, 10, 11, 9, 10, 12] }, + { "name": "two_outliers", "history": [20, 21, 19, 22, 18, 300, 20, 21, 19, 22, 18, -200, 20, 21, 19, 22] }, + { "name": "intermittent", "history": [0, 0, 5, 0, 0, 0, 8, 0, 0, 3, 0, 0, 0, 0, 6, 0, 0, 2] }, + { "name": "noisy", "history": [23, 19, 27, 31, 18, 22, 29, 17, 25, 30, 21, 16, 28, 24, 20, 26, 33, 15, 22, 27] }, + { "name": "fractional", "history": [1.5, 2.25, 3.125, 2.75, 4.0, 3.5, 2.875, 4.125, 3.75, 5.0, 4.25, 3.625] }, + { "name": "short_below_skip", "history": [4, 5, 6] }, + { "name": "long_over_maxbuckets", "generate": { "n": 800, "base": 100, "mod": 7 } }, + { "name": "long_with_trend", "generate": { "n": 650, "base": 50, "mod": 13 } }, + + { "name": "se_constant", "method": "single_exp", "history": [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] }, + { "name": "se_linear_trend", "method": "single_exp", "history": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20] }, + { "name": "se_outlier", "method": "single_exp", "history": [10, 11, 9, 10, 12, 8, 10, 11, 9, 10, 500, 11, 9, 10, 12, 8, 10, 11, 9, 10] }, + { "name": "se_noisy", "method": "single_exp", "history": [23, 19, 27, 31, 18, 22, 29, 17, 25, 30, 21, 16, 28, 24, 20, 26, 33, 15, 22, 27] }, + { "name": "se_too_short", "method": "single_exp", "history": [4, 5, 6, 7, 8, 9, 10] }, + { "name": "se_long_over_maxbuckets", "method": "single_exp", "generate": { "n": 800, "base": 100, "mod": 7 } }, + + { "name": "de_constant", "method": "double_exp", "history": [10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10, 10] }, + { "name": "de_linear_trend", "method": "double_exp", "history": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25] }, + { "name": "de_outlier", "method": "double_exp", "history": [10, 13, 16, 19, 22, 25, 28, 31, 34, 37, 500, 43, 46, 49, 52, 55, 58, 61, 64, 67] }, + { "name": "de_noisy_trend", "method": "double_exp", "history": [5, 9, 8, 14, 13, 19, 17, 24, 22, 29, 26, 34, 31, 39, 36, 44, 41, 49, 46, 54] }, + { "name": "de_too_short", "method": "double_exp", "history": [4, 5, 6, 7, 8, 9, 10] }, + { "name": "de_long_over_maxbuckets", "method": "double_exp", "generate": { "n": 800, "base": 100, "mod": 9 } }, + + { "name": "cr_all_zero", "method": "croston", "history": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] }, + { "name": "cr_intermittent", "method": "croston", "history": [5, 0, 0, 8, 0, 0, 0, 6, 0, 3, 0, 0, 7, 0, 0, 4, 0, 9, 0, 2] }, + { "name": "cr_sparse", "method": "croston", "history": [10, 0, 0, 0, 0, 12, 0, 0, 0, 0, 0, 8, 0, 0, 0, 0, 15, 0, 0, 0, 0, 11] }, + { "name": "cr_dense", "method": "croston", "history": [3, 4, 0, 5, 6, 0, 4, 5, 0, 6, 7, 0, 5, 6, 0, 7, 8, 0, 6, 7] }, + { "name": "cr_outlier", "method": "croston", "history": [5, 0, 6, 0, 5, 0, 200, 0, 6, 0, 5, 0, 6, 0, 5, 0, 7, 0, 6, 0] }, + { "name": "cr_long_over_maxbuckets", "method": "croston", "generate": { "n": 800, "base": 0, "mod": 4 } }, + + { "name": "seas_no_cycle_trend", "method": "seasonal", "history": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30] }, + { "name": "seas_period7", "method": "seasonal", "generate_cycle": { "cycle": [10, 25, 40, 55, 40, 25, 10], "reps": 10 } }, + { "name": "seas_period4", "method": "seasonal", "generate_cycle": { "cycle": [5, 20, 35, 20], "reps": 14 } }, + { "name": "seas_period7_noisy", "method": "seasonal", "generate_cycle": { "cycle": [12, 24, 41, 53, 38, 27, 9], "reps": 9 } }, + { "name": "seas_long_over_maxbuckets", "method": "seasonal", "generate_cycle": { "cycle": [10, 30, 50, 70, 50, 30, 10], "reps": 120 } } +] diff --git a/test/rust_parity/test_forecast_parity.py b/test/rust_parity/test_forecast_parity.py new file mode 100644 index 0000000000..37eb9497fc --- /dev/null +++ b/test/rust_parity/test_forecast_parity.py @@ -0,0 +1,130 @@ +"""Rust/PyO3 forecast-port parity (Engine track E4). + +Diffs each Rust forecast method against a standalone C++ reference that +replicates the corresponding `timeseries.cpp` numeric core verbatim. smape / +standarddeviation / forecast must match within a tight relative epsilon (same +f64 op order); outlier index sets must match exactly. Vectors include +>MAXBUCKETS series — the smapeWeight OOB site. +""" + +import json +import os +import subprocess +import tempfile +from pathlib import Path + +import pytest + +frepple_forecast = pytest.importorskip("frepple_forecast") + +HERE = Path(__file__).resolve().parent +REPO = HERE.parent.parent +VECTORS = json.loads((HERE / "forecast_vectors.json").read_text()) +CXX_SRC = REPO / "tools" / "rust-pilot" / "forecast_reference.cpp" + +# Engine defaults (timeseries.cpp:32-36, 416-418). +MA_PARAMS = (5, 4.0, 0.95, 5) # order, max_deviation, smape_alfa, skip +SE_PARAMS = (0.2, 0.03, 1.0, 4.0, 0.95, 5, 15) # init/min/max alfa, maxdev, smape_alfa, skip, iters +# init/min/max alfa, init/min/max gamma, maxdev, smape_alfa, skip, iters +DE_PARAMS = (0.2, 0.02, 1.0, 0.2, 0.05, 1.0, 4.0, 0.95, 5, 15) +# min/max alfa, decay_rate, maxdev, smape_alfa, skip, iters +CR_PARAMS = (0.03, 0.8, 0.1, 4.0, 0.95, 5, 15) +# init/min/max alfa, init/min/max beta, gamma, min/max period, min/max autocorr, smape_alfa, skip, iters +SEAS_PARAMS = (0.2, 0.02, 1.0, 0.2, 0.2, 1.0, 0.05, 2, 14, 0.5, 0.8, 0.95, 5, 15) + + +@pytest.fixture(scope="session") +def cxx_ref(): + env_bin = os.environ.get("FORECAST_CXX_REF_BIN") + if env_bin and Path(env_bin).exists(): + return env_bin + out = Path(tempfile.gettempdir()) / "frepple_forecast_reference" + subprocess.run(["g++", "-O2", "-o", str(out), str(CXX_SRC)], check=True) + return str(out) + + +def _history(case): + if "history" in case: + return [float(x) for x in case["history"]] + if "generate_cycle" in case: + g = case["generate_cycle"] + return [float(v) for _ in range(g["reps"]) for v in g["cycle"]] + g = case["generate"] + return [float(g["base"] + (i % g["mod"])) for i in range(g["n"])] + + +def _rust(method, history): + if method == "single_exp": + return frepple_forecast.single_exponential(history, *SE_PARAMS) + if method == "double_exp": + return frepple_forecast.double_exponential(history, *DE_PARAMS) + if method == "croston": + return frepple_forecast.croston(history, *CR_PARAMS) + if method == "seasonal": + return frepple_forecast.seasonal(history, *SEAS_PARAMS) + return frepple_forecast.moving_average(history, *MA_PARAMS) + + +def _cxx_argv(method): + if method == "single_exp": + return ["single_exp", *[str(x) for x in SE_PARAMS]] + if method == "double_exp": + return ["double_exp", *[str(x) for x in DE_PARAMS]] + if method == "croston": + return ["croston", *[str(x) for x in CR_PARAMS]] + if method == "seasonal": + return ["seasonal", *[str(x) for x in SEAS_PARAMS]] + return ["moving_average", *[str(x) for x in MA_PARAMS]] + + +def _cxx(cxx_ref, method, history): + res = subprocess.run( + [cxx_ref, *_cxx_argv(method)], + input=" ".join(repr(x) for x in history), + capture_output=True, + text=True, + check=True, + ) + return json.loads(res.stdout) + + +def _approx(a, b, rel=1e-9, abs_=1e-12): + if a == b: # handles DBL_MAX sentinel + exact zeros + return True + return abs(a - b) <= max(rel * max(abs(a), abs(b)), abs_) + + +@pytest.mark.parametrize("case", VECTORS, ids=lambda c: c["name"]) +def test_forecast_parity(case, cxx_ref): + method = case.get("method", "moving_average") + history = _history(case) + out = _rust(method, history) + ref = _cxx(cxx_ref, method, history) + smape, stdev, forecast = out[0], out[1], out[2] + assert _approx(smape, ref["smape"]), f"[{method}] smape rust={smape} cxx={ref['smape']}" + assert _approx( + stdev, ref["standarddeviation"] + ), f"[{method}] stdev rust={stdev} cxx={ref['standarddeviation']}" + assert _approx( + forecast, ref["forecast"] + ), f"[{method}] forecast rust={forecast} cxx={ref['forecast']}" + if method == "seasonal": + # Rust returns (...s_i, l_i, t_i, cycleindex). The l_i/t_i/cycleindex are + # the per-bucket apply-state (phase 7): the one-step `forecast` only pins + # l_i + t_i/period, so these are checked independently here. + _, _, _, period, force, s_i, l_i, t_i, cycleindex = out + assert period == ref["period"], f"period rust={period} cxx={ref['period']}" + assert force == ref["force"], f"force rust={force} cxx={ref['force']}" + assert len(s_i) == len(ref["s_i"]), f"s_i len rust={len(s_i)} cxx={len(ref['s_i'])}" + for k, (a, b) in enumerate(zip(s_i, ref["s_i"])): + assert _approx(a, b), f"s_i[{k}] rust={a} cxx={b}" + assert _approx(l_i, ref["l_i"]), f"l_i rust={l_i} cxx={ref['l_i']}" + assert _approx(t_i, ref["t_i"]), f"t_i rust={t_i} cxx={ref['t_i']}" + assert cycleindex == ref["cycleindex"], ( + f"cycleindex rust={cycleindex} cxx={ref['cycleindex']}" + ) + else: + outliers = out[3] + assert set(outliers) == set(ref["outliers"]), ( + f"[{method}] outliers rust={sorted(outliers)} cxx={sorted(ref['outliers'])}" + ) diff --git a/test/rust_parity/test_parity.py b/test/rust_parity/test_parity.py new file mode 100644 index 0000000000..13d07fccc2 --- /dev/null +++ b/test/rust_parity/test_parity.py @@ -0,0 +1,57 @@ +"""Rust/PyO3 pilot parity (Engine track E4). + +Diffs the Rust `frepple_num` extension against a standalone C++ reference that +replicates src/utils/json.cpp:790-890 verbatim. "agree" vectors must match the +C++ byte-for-byte; "rust_safe" vectors exercise inputs the C++ leaves undefined +(NaN, negative->unsigned) where Rust is defined and safe by construction. +""" + +import json +import os +import subprocess +import tempfile +from pathlib import Path + +import pytest + +# The Rust wheel must be installed (maturin); skip cleanly if not (e.g. a plain +# checkout without the pilot built). +frepple_num = pytest.importorskip("frepple_num") + +HERE = Path(__file__).resolve().parent +REPO = HERE.parent.parent +VECTORS = json.loads((HERE / "vectors.json").read_text()) +CXX_SRC = REPO / "tools" / "rust-pilot" / "cxx_reference.cpp" + + +@pytest.fixture(scope="session") +def cxx_ref(): + """Path to the compiled C++ reference (env CXX_REF_BIN, else compile here).""" + env_bin = os.environ.get("CXX_REF_BIN") + if env_bin and Path(env_bin).exists(): + return env_bin + out = Path(tempfile.gettempdir()) / "frepple_cxx_reference" + subprocess.run(["g++", "-O2", "-o", str(out), str(CXX_SRC)], check=True) + return str(out) + + +def _rust(op, value): + fn = getattr(frepple_num, op) + return fn(str(value)) if op == "parse_long" else fn(float(value)) + + +def _cxx(cxx_ref, cxx_op, value): + res = subprocess.run( + [cxx_ref, cxx_op, str(value)], capture_output=True, text=True, check=True + ) + return int(res.stdout.strip()) + + +@pytest.mark.parametrize("case", VECTORS, ids=lambda c: f"{c['op']}({c['input']!r})") +def test_parity(case, cxx_ref): + got = _rust(case["op"], case["input"]) + if case["mode"] == "agree": + expected = _cxx(cxx_ref, case["cxx"], case["input"]) + assert got == expected, f"rust={got} cxx={expected} for {case}" + else: # rust_safe: C++ is UB here; assert Rust's defined result + assert got == case["rust_expected"], f"rust={got} for {case}" diff --git a/test/rust_parity/vectors.json b/test/rust_parity/vectors.json new file mode 100644 index 0000000000..da126abc32 --- /dev/null +++ b/test/rust_parity/vectors.json @@ -0,0 +1,27 @@ +[ + { "op": "clamp_to_long", "cxx": "long", "input": 42.9, "mode": "agree" }, + { "op": "clamp_to_long", "cxx": "long", "input": -42.9, "mode": "agree" }, + { "op": "clamp_to_long", "cxx": "long", "input": 5.0, "mode": "agree", "note": "ordinary double; the C++ inverted-bound bug returned LONG_MIN here" }, + { "op": "clamp_to_long", "cxx": "long", "input": 0.0, "mode": "agree" }, + { "op": "clamp_to_long", "cxx": "long", "input": 1e30, "mode": "agree" }, + { "op": "clamp_to_long", "cxx": "long", "input": -1e30, "mode": "agree" }, + { "op": "clamp_to_int", "cxx": "int", "input": 7.9, "mode": "agree" }, + { "op": "clamp_to_int", "cxx": "int", "input": -7.9, "mode": "agree" }, + { "op": "clamp_to_int", "cxx": "int", "input": 1e30, "mode": "agree" }, + { "op": "clamp_to_int", "cxx": "int", "input": -1e30, "mode": "agree" }, + { "op": "clamp_to_unsigned_long", "cxx": "ulong", "input": 5.5, "mode": "agree" }, + { "op": "clamp_to_unsigned_long", "cxx": "ulong", "input": 0.0, "mode": "agree" }, + { "op": "clamp_to_unsigned_long", "cxx": "ulong", "input": 1e30, "mode": "agree" }, + { "op": "parse_long", "cxx": "parse_long", "input": "42", "mode": "agree" }, + { "op": "parse_long", "cxx": "parse_long", "input": "-17", "mode": "agree" }, + { "op": "parse_long", "cxx": "parse_long", "input": "+9", "mode": "agree" }, + { "op": "parse_long", "cxx": "parse_long", "input": " 123abc", "mode": "agree" }, + { "op": "parse_long", "cxx": "parse_long", "input": "abc", "mode": "agree" }, + { "op": "parse_long", "cxx": "parse_long", "input": "", "mode": "agree" }, + + { "op": "clamp_to_long", "input": "nan", "mode": "rust_safe", "rust_expected": 0, "note": "C++ static_cast(NaN) is undefined behaviour" }, + { "op": "clamp_to_long", "input": "inf", "mode": "rust_safe", "rust_expected": 9223372036854775807, "note": "i64::MAX" }, + { "op": "clamp_to_long", "input": "-inf", "mode": "rust_safe", "rust_expected": -9223372036854775808, "note": "i64::MIN" }, + { "op": "clamp_to_unsigned_long", "input": -5.0, "mode": "rust_safe", "rust_expected": 0, "note": "C++ has no lower clamp; a negative wraps to a huge value" }, + { "op": "clamp_to_unsigned_long", "input": "nan", "mode": "rust_safe", "rust_expected": 0, "note": "C++ NaN cast is UB" } +] diff --git a/test/stress_1/stress_1.1.expect b/test/stress_1/stress_1.1.expect new file mode 100644 index 0000000000..d2079e8c91 --- /dev/null +++ b/test/stress_1/stress_1.1.expect @@ -0,0 +1 @@ +STRESS_OK diff --git a/test/stress_1/stress_1.py b/test/stress_1/stress_1.py new file mode 100644 index 0000000000..ef17bc611d --- /dev/null +++ b/test/stress_1/stress_1.py @@ -0,0 +1,74 @@ +# Engine track E2 - stress scenario. Builds a large model (~N items, each a +# make-from-purchased-component with a demand), solves it fully constrained, and +# records operationplan count + solve time + peak memory. Regression-gated: a +# hard floor on the operationplan count (proves the stress size) plus GENEROUS +# ceilings on time/memory (catch catastrophic regressions - O(n^2) blowups, +# leaks - without flaking on CI-runner variance). Actual metrics are printed for +# trend tracking; the golden output is a deterministic STRESS_OK. +import datetime, time, os + +N = 8000 # tune to keep operationplan count comfortably above 10k + +frepple.settings.current = datetime.datetime(2024, 1, 1) +loc = frepple.location(name="factory") +cust = frepple.customer(name="cust") +sup = frepple.supplier(name="sup") +res = frepple.resource(name="machine", maximum=100000, location=loc) +due = datetime.datetime(2024, 3, 1) + +t_build = time.perf_counter() +for i in range(N): + it = frepple.item(name="I%d" % i) + comp = frepple.item(name="C%d" % i) + op = frepple.operation_fixed_time(name="make%d" % i, location=loc, item=it, duration=86400) + # Flows are item-based; buffers are auto-created at the operation location. + frepple.flow(operation=op, item=it, quantity=1, type="flow_end") # produce the item + frepple.flow(operation=op, item=comp, quantity=-1, type="flow_start") # consume the component + frepple.itemsupplier(item=comp, supplier=sup, location=loc, leadtime=7 * 86400) + frepple.load(operation=op, resource=res, quantity=1) + frepple.demand(name="D%d" % i, item=it, location=loc, quantity=10, due=due, + customer=cust, priority=1) +build_s = time.perf_counter() - t_build + +t0 = time.perf_counter() +frepple.solver_mrp(constraints=15, plantype=1, loglevel=0).solve() +solve_s = time.perf_counter() - t0 + +count = sum(1 for _ in frepple.operationplans()) + +def peak_rss_mb(): + try: + import resource + kb = resource.getrusage(resource.RUSAGE_SELF).ru_maxrss + return kb / 1024.0 # Linux ru_maxrss is KB + except Exception: + try: + for line in open("/proc/self/status"): + if line.startswith("VmHWM:"): + return int(line.split()[1]) / 1024.0 + except Exception: + return -1.0 + return -1.0 + +mb = peak_rss_mb() +# Recorded for trend-tracking (printed, not byte-asserted - timings vary by host). +print("STRESS items=%d operationplans=%d build_s=%.2f solve_s=%.2f peak_rss_mb=%.0f" + % (N, count, build_s, solve_s, mb)) + +# Regression gate. The count is a hard, deterministic floor (proves the stress +# size). Time/memory use GENEROUS ceilings - they catch a catastrophic regression +# (an O(n^2) blow-up or a leak) without flaking on CI-runner variance. Baseline on +# an optimised Release build: ~24k operationplans, ~1.5 s solve, ~80 MB peak. +# NB: run in the Release suite only - excluded from engine-asan/engine-ubsan, where +# the Debug+sanitizer build makes this ~1000x slower (allocation-heavy at scale). +# Explicit raises (not asserts) so the gate fires even under python -O. +if count < 10000: + raise Exception("stress regression: expected >= 10000 operationplans, got %d" % count) +if solve_s >= 180.0: + raise Exception("stress regression: solve %.1fs exceeds the 180s ceiling (baseline ~1.5s)" % solve_s) +if mb > 0 and mb >= 1500.0: + raise Exception("stress regression: %.0f MB peak exceeds the 1500 MB ceiling (baseline ~80 MB)" % mb) + +# Deterministic golden output (host-independent) so this is a stable golden test. +with open("output.1.xml", "wt") as out: + print("STRESS_OK", file=out) diff --git a/test/structural_1/structural_1.xml b/test/structural_1/structural_1.xml new file mode 100644 index 0000000000..66a3b4fa5f --- /dev/null +++ b/test/structural_1/structural_1.xml @@ -0,0 +1,266 @@ + + + Material constraint test model + + This model tests the buffer solver code in situations where the minimum onhand + limit is varying. + - 1: Scenario of a constant, non-zero limit. + - 2: Same as 1, but now the supply is limited. The inventory target can't be + reached and all supply is used to satisfy demand. + - 3: Same as 1, but with minimum target varying DEcreasing over time. + - 4: Same as 3, but with minimum target varying INcreasing over time. + + 2009-01-01T00:00:00 + + + + + 4 + 10 + + + + + + + + 4 + 10 + + + + + + + + + + + + + + 10 + + + + + + + + + + + + + + 10 + + + + + + + + + 0 + + + 1 + + + + + 2 + + + + + -2 + + + + + -2 + + + + 0 + + + 1 + + + + + 2 + + + + + -2 + + + + + -2 + + + + 0 + + + 1 + + + + + 2 + + + + + -2 + + + + + -2 + + + + 0 + + + 1 + + + + + 2 + + + + + -2 + + + + + -2 + + + + + + 100 + 1 + 2009-01-10T00:00:00 + + + 10 + P1D + + + + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 100 + confirmed + + + + 2009-01-17T00:00:00 + 2009-01-17T00:00:00 + 200 + confirmed + + + + + + 100 + 1 + 2009-01-10T00:00:00 + + + P1D + + + + + + + 2009-01-10T00:00:00 + 2009-01-10T00:00:00 + 100 + confirmed + + + + 2009-01-17T00:00:00 + 2009-01-17T00:00:00 + 50 + confirmed + + + + + + 100 + 1 + 2009-01-10T00:00:00 + + + P1D + + + + + + + 2009-01-15T00:00:00 + 2009-01-15T00:00:00 + 1000 + confirmed + + + + + + 100 + 1 + 2009-01-10T00:00:00 + + + P1D + + + + + + + 2009-01-15T00:00:00 + 2009-01-15T00:00:00 + 1000 + confirmed + + + + + diff --git a/test/structural_2/structural_2.xml b/test/structural_2/structural_2.xml new file mode 100644 index 0000000000..9f1a0382ef --- /dev/null +++ b/test/structural_2/structural_2.xml @@ -0,0 +1,194 @@ + + + Test model for suppliers + + This test model demonstrates the distribution network modeling features. + + 2015-01-01T00:00:00 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + P7D + + 1 + + + + P7D + + 2 + + + + + + P10D + + 1 + + + + P12D + + 1 + + + + + P3D + 1 + + + + + P3D + 1 + + + + + P3D + 1 + + + + + P3D + 1 + + + + + + + 2 + + + + + + + + + + + 50 + 2015-01-01T00:00:00 + 1 + + + + + 50 + 2015-01-01T00:00:00 + 1 + + + + + + 100 + 2015-03-01T00:00:00 + 1 + + + + + + 100 + 2015-01-01T00:00:00 + 1 + + + + + + 100 + 2015-03-01T00:00:00 + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/test/structural_3/structural_3.xml b/test/structural_3/structural_3.xml new file mode 100644 index 0000000000..f6194e18d1 --- /dev/null +++ b/test/structural_3/structural_3.xml @@ -0,0 +1,101 @@ + + + actual plan + + resource constraint test model. In this model, the capacity problems can + easily be solved by moving operation plans earlier in time. + + 2009-01-01T00:00:00 + + + P1D + + + P1D + + + + + + + + + + + 1 + P4D + + + + + + + + + + + + -1 + + + + + 1 + + + + + 10 + 2009-01-20T00:00:00 + 1 + + + + + 10 + 2009-01-20T00:00:00 + 2 + + + + + 10 + 2009-01-17T12:00:00 + 3 + + + + + 10 + 2009-01-20T00:00:00 + 4 + + + + + This demand can't be planned in time since there is no + capacity in the acceptable time window before the ask date. + + 10 + 2009-01-20T00:00:00 + 5 + + + + + + + diff --git a/tools/modernization/asan_pegging_repro.sh b/tools/modernization/asan_pegging_repro.sh new file mode 100755 index 0000000000..4646ecfb1b --- /dev/null +++ b/tools/modernization/asan_pegging_repro.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# Reproduce the pegging-alternate crash under Linux Debug+ASan and capture a +# symbolized backtrace. Runs INSIDE ubuntu:24.04 with: +# - the macOS repo bind-mounted read-only at /frepple +# - a persistent host scratch dir bind-mounted at /work (clean Linux build tree) +# so rebuilds after a source edit are incremental. +# No `set -e`: runtest.py is expected to fail (it reproduces a crash) and we want +# to print its exit code below. Instead the build steps are guarded explicitly so +# a build failure aborts before the test runs against a stale/missing binary. +set -uxo pipefail + +export DEBIAN_FRONTEND=noninteractive +if ! command -v cmake >/dev/null; then + apt-get update -qq + apt-get install -y -qq cmake g++ git libxerces-c-dev libssl-dev libpq-dev \ + python3 python3-dev python3-venv binutils rsync >/dev/null +fi + +# One-time: seed /work with a clean copy (no macOS venv / build / native binaries). +if [ ! -f /work/.seeded ]; then + rsync -a --delete \ + --exclude='/venv/' --exclude='/build/' --exclude='/node_modules/' \ + --exclude='/bin/frepple' --exclude='/bin/libfrepple.*' \ + /frepple/ /work/ + touch /work/.seeded +fi + +cmake -B /work/build -S /work -DCMAKE_BUILD_TYPE=Debug || exit 1 +# The engine's static libs have a build-order dep on the 'venv' target, which +# pip-installs dev requirements. We don't need that to compile/run the C++ +# engine, so satisfy the stamp to skip it (fast, deterministic). +python3 -m venv /work/venv >/dev/null 2>&1 || true +touch /work/build/venv.stamp 2>/dev/null || true +cmake --build /work/build -j"$(nproc)" || exit 1 + +export FREPPLE_DATE_STYLE="day-month-year" +export ASAN_OPTIONS="detect_leaks=0:halt_on_error=1:abort_on_error=1:symbolize=1" +cd /work/test +echo "===== RUNNING pegging_4/5/7 UNDER ASAN =====" +./runtest.py pegging_4 pegging_5 pegging_7 -d +echo "===== runtest exit: $? =====" diff --git a/tools/modernization/clang-tidy-baseline.md b/tools/modernization/clang-tidy-baseline.md new file mode 100644 index 0000000000..75c6a475c9 --- /dev/null +++ b/tools/modernization/clang-tidy-baseline.md @@ -0,0 +1,51 @@ +# clang-tidy baseline — Engine track E1 + +Third leg of the E1 static-analysis gate (alongside ASan + UBSan — see +[ubsan-baseline.md](ubsan-baseline.md)). clang-tidy is **parse-only** (no codegen), so it's lighter than +the sanitizer Debug builds and complements them: the sanitizers find UB/memory bugs at *runtime* over the +golden scenarios; clang-tidy finds them *statically*, including on paths the golden data never exercises. + +## Configuration (`.clang-tidy`) + +Bug-finders only — `clang-analyzer-*` (the path-sensitive analyzer: null derefs, leaks, uninitialised +reads) + high-signal `bugprone-*`. Style / readability / modernize families are **off**: on a mature +C++23 codebase they're thousands of cosmetic hits that bury real findings. Two `bugprone` checks are also +off as high-volume/low-signal here: + +- `bugprone-unhandled-self-assignment` — fires on every `operator=` lacking an explicit `this == &other` + guard. Defensive style, not a bug (84 of 142 findings in the sample). +- `bugprone-exception-escape` — legacy destructors/move ops that *could* throw (26 of 142). + +`HeaderFilterRegex: frepple/.*\.h$` keeps findings to frePPLe code (not Python.h / xerces). + +## Baseline shape + +Configure exports `compile_commands.json` (57 engine TUs). A header finding is reported once **per +including TU**, so the raw line count (~182) over-states reality; deduped by `path:line:col`+check the +baseline is **54 distinct findings**. Breakdown (full `src/`, the check set below): + +| Check | n | Class | Triage | +| --- | --- | --- | --- | +| `clang-analyzer-deadcode.DeadStores` | 16 | dead store | usually benign; a few may be logic slips | +| `bugprone-switch-missing-default-case` | 10 | missing default | low risk | +| `clang-analyzer-core.CallAndMessage` | 5 | null/uninit call | **triage** — call through a possibly-null/uninit arg | +| `bugprone-integer-division` | 5 | precision loss | **triage** — `int/int` fed to a double | +| `clang-analyzer-cplusplus.NewDelete` | 4 | manual-memory misuse | **triage** — use-after-free / double-free shapes | +| `clang-analyzer-security.FloatLoopCounter` | 2 | float loop counter | review | +| `bugprone-suspicious-string-compare` / `-empty-catch` / `-copy-constructor-init` | 2 ea | misc | low | +| `clang-analyzer-core.NullDereference` | 1 | **null deref** | **triage first** | +| `clang-analyzer-core.uninitialized.{Assign,Branch}` | 1 ea | uninitialised read | **triage** | +| `clang-analyzer-security.insecureAPI.strcpy`, `bugprone-unused-raii`, `-throw-keyword-missing` | 1 ea | misc | low | + +The live count + breakdown is also emitted by the `engine-clang-tidy` CI job (step summary, distinct vs +raw) and the uploaded `clang-tidy-report` artifact. The numbers above are a snapshot for orientation; CI +is the source of truth. The ~10 **triage** findings (NullDereference, CallAndMessage, integer-division, +NewDelete, uninitialised) are the E2 work-list before the gate tightens to "no new findings". + +## Gate posture + +`engine-clang-tidy.yml` is **advisory**: it runs the bug-finder check set over `src/`, reports the count + +breakdown in the step summary, and never fails the job (the baseline is non-zero). It runs on PRs into +`modernization` and on push. The tightening to **"no NEW findings on changed files"** (via +`clang-tidy-diff`) lands once the high-signal baseline above is triaged — tracked as **E2**, mirroring how +the UBSan gate went advisory → blocking. diff --git a/tools/modernization/engine-review-E1.md b/tools/modernization/engine-review-E1.md new file mode 100644 index 0000000000..6479d1df1f --- /dev/null +++ b/tools/modernization/engine-review-E1.md @@ -0,0 +1,146 @@ +# Engine review — debt catalog, TODO triage, risk hotspots (Engine track E1) + +The first E1 gate item: a structured review of the C++ engine — prioritised debt, the scary-TODO triage, +and a risk-hotspot map (pegging, solver state machine, memory). It pairs with the runtime/static gates +([ubsan-baseline.md](ubsan-baseline.md), [clang-tidy-baseline.md](clang-tidy-baseline.md)) to complete E1. + +**Method.** Four parallel surveys (TODO sweep, pegging, solver, memory-safety) followed by **direct +verification of every load-bearing claim** — which corrected several. The corrections are themselves part +of the value (see the last section); treat anything below as verified against the current source, not the +survey output. + +## Overall posture + +The engine is modern C++23 and the *algorithms* are mature, but it is **pointer-heavy with a hand-rolled +object model welded to CPython**, and its safety net is golden-output diffing with thin coverage in the +riskiest subsystem. Grounded magnitudes (current `src/` + `include/`): + +| Signal | Value | +| --- | --- | +| `TODO`/`FIXME`/`XXX`/`HACK` markers | **99** | +| explicit `delete` sites (`src/`) | 122 | +| smart-pointer usages (whole engine) | **16** (i.e. ~all ownership is raw) | +| `Py_INCREF`/`DECREF` hand-management sites | 109 | +| clang-tidy distinct findings (bug-finders) | 54 ([breakdown](clang-tidy-baseline.md)) | +| both sanitizers | **blocking + clean** (ASan, UBSan) | + +This is the evidence base for the §1 principle "earn any rewrite decision" — it is a *legitimate* Rust +argument (manual memory + UB surface) **and** a hard one (deep CPython coupling). Neither a "rewrite now" +nor "never" conclusion is supported; the scoped E4 pilot is the right next probe. + +## Risk hotspots + +### H1 — Solver state machine: fixed recursion bound + manual undo (MED–HIGH) +- **Fixed depth:** `State statestack[MAXSTATES]` with `MAXSTATES = 256` (`include/frepple/solver.h:946,1001`). + Deep alternate/split/routing descent that overruns throws `RuntimeException` (`solverplan.cpp`) — a + *handled* failure (a plan dies), not UB/crash, but a hard ceiling with no graceful degradation. +- **Manual push/pop discipline:** `data->state` is a raw pointer into `statestack`; exception cleanup is + `while (data->state > topstate) data->pop();` (`solverdemand.cpp`). A missed `copy_answer` or a cached + `State*` used across a pop is UB. Correctness rests on hand-maintained stack discipline across many + nested `solve()` calls. +- **Undo via CommandManager bookmarks:** rollback must unwind all model mutations; a throw *during* + rollback (e.g. `CommandDeleteOperationPlan::rollback` re-creating flowloads) leaves partial state — no + inner guard. Bookmarks aren't RAII. +- **Threading:** per-cluster parallel solve gives each thread its own `SolverData`, but the **shared model + graph** (Buffer/Resource flowplan timelines) is accessed lock-free on the assumption clusters are + disjoint; complex cross-item supply can violate that → data-race surface. + +### H2 — Memory ownership: recursive destructors + a union-of-heap-iterators (HIGH) +- **`~OperationPlan`** (`operationplan.cpp`) deletes owned sub-plans **and its owner** (`setOwner(nullptr); delete o;`). + The self-link is broken before the delete, but the recursive owner/child teardown is the single most + intricate ownership path in the engine — the place a double-free would most plausibly hide. +- **`HasProblems::EntityIterator`** (`src/model/problem.cpp`) is a `union` of five heap-allocated iterator + pointers tracked by a `type` tag; the destructor deletes the active arm. If a `new …::iterator(...)` + throws between clearing one arm and assigning the next, the dtor reads a stale `type` and deletes the + wrong pointer. No RAII / exception guard. **Real, if low-probability.** +- 16 smart pointers across the whole engine ⇒ every other owning relationship is hand-managed. + +### H3 — CPython coupling: refcount on the exception path (MED) + separability (LOW for now) +- 109 hand-managed refcount sites. The factory `Object::create` (`include/frepple/utils.h`) does its + `Py_INCREF(x)` **after** `setField`/`setProperty`, which can throw → an early throw orphans the C++ + object's Python wrapper. Same shape in `PythonData`'s assignment (`src/utils/python.cpp`). +- The model object base carries a `PyObject* dict`; lifetime is Python-refcount-driven via + `Object::deallocator`. The engine is **not** separable from CPython without reworking the object model / + C-ABI boundary — directly relevant to scoping any Rust port (favour an isolated leaf, as E4 did, not the + core). + +### H4 — Pegging: thin validation on the fragile cases (MED) — *corrected, see below* +- Pegging traverses the supply graph with a stateful `PeggingIterator` over a thread-local pool. Cycle + guarding **does exist and is correct** — `set visited` (`model.h:9819`), a default- + constructed member, checked in `followPegging` (`pegging.cpp:325-327`). +- The real gap is **test coverage of the fragile inputs**: of 12 `test/pegging_*`, **9 carry golden + `.expect` files and 3 (pegging_4/5/7) are smoke-only** (run-without-crash, no output assertion) — and + those three are exactly the alternate/routing/infinite-buffer cases. No golden coverage of deep (5+ + level) BOMs or cyclic dependencies. +- **E2 finding — why those 3 can't be golden as-is (verified, not just asserted).** An attempt to capture + golden baselines for pegging_4/5 confirmed the smoke-only decision is correct and structural, not a + missing baseline: the pegging report is **deterministic within an environment** (30× identical) but its + **`operationplans()` iteration order varies *across* environments** — Docker Release vs Debug+ASan **and** + the GitHub `ubuntu-24.04` runner each order the blocks differently (identical content, reordered). It is + **single-threaded** (loglevel>0 forces `setMaxParallel(1)`, `solverplan.cpp:840-842`) and + **PYTHONHASHSEED-independent**, so it's neither a thread race nor Python hash-seed; the order tracks the + build/stdlib (allocation/pointer-ordering among equivalent operationplans). Proof: golden `.expect` + captured in two Docker builds **passed locally but failed pegging_4/5 on the GitHub runner**. ⇒ Closing + H4's golden gap needs a **deterministic tiebreaker**. +- **RESOLVED.** Root-caused to `OperationPlan::operator<` (`operationplan.cpp:1048`), whose final tie-breaker + for otherwise-identical operationplans was a **pointer comparison** (`return this < &a;`) — self-documented + as "not reproducible across platforms and runs". It drives the per-operation sorted linked list that + `operationplans()` iterates, so the order tracked heap addresses (build/ASLR-dependent). Replaced it with a + **monotonic creation-sequence** tie-breaker (an `atomic` counter + per-operationplan + `sequence`, deterministic for the single-threaded reproducible case). Verified: **zero blast radius** (the + full 97-test golden suite passes unchanged on Release **and** Debug+ASan), and pegging_4/5/7 output is now + **byte-identical across Release and Debug** — so they are converted to full golden tests. + +## Static-analysis cross-reference + +The 54 clang-tidy findings concentrate the actionable static signal; the ~10 to triage first +(`clang-analyzer-core.NullDereference` ×1, `…CallAndMessage` ×5, `bugprone-integer-division` ×5, +`…cplusplus.NewDelete` ×4, `…uninitialized.{Assign,Branch}` ×2) overlap H2's manual-memory surface and +should be walked alongside the pegging/solver hotspots in E2. UBSan already retired its findings +(operationdependency null member-call **fixed**; iterator idiom annotated); ASan is clean. + +## TODO triage (highlights of the 99) + +Prioritised; full sweep available by `grep -rnE 'TODO|FIXME|XXX|HACK' src include`. + +**Higher concern (correctness / data shape):** +- `src/solver/solverload.cpp:435,557` — alternate-resource selection is **incomplete**: qualified + resources are not cost-sorted (only the current one is tried), and on restore the selected resource + isn't fully reset. Affects plan optimality + state hygiene. +- `src/solver/solverdemand.cpp:657` — `POLICY_INRATIO` demand case is `break; // TODO` — **unimplemented + policy** silently bypasses the replenishment loop. +- `src/forecast/measure.cpp:1011` — a workaround masks a `removeValue()` vs `setValue(-1)` semantic + mismatch ("a unit test fails if we remove it") — unresolved inconsistency. +- `src/utils/database.cpp:348` — no automatic reconnect on a dropped DB connection (`PQreset` TODO). + +**Disabled / cautionary (not live bugs):** +- `src/solver/operatordelete.cpp:217` — the "**dangerous side effects** … plan quality is better without + this" shortage-deletion feature is **inside a `/* */` block (disabled)**. Dead code carrying a warning, + not an active hot-path bug — but a candidate for deletion to stop it misleading readers. + +**Performance / design (cold or bounded):** +- `src/model/resource.cpp:715`, `src/model/operationplan.cpp:1424`, `src/solver/operatorforward.cpp:99` + — setup-time / propagation loops that re-scan more than needed (potential O(n²) on pathological inputs). +- Numerous routing/slack accuracy + code-duplication notes in `operationplan.cpp` / `operation.cpp` / + `forecast.cpp` — maintainability debt, low correctness risk. + +## Corrections to prior assumptions (verification value) + +The plan and two sub-surveys carried stale or wrong claims; verified against current source: + +| Claim (plan / survey) | Reality | +| --- | --- | +| `solveroperation.cpp:123` "increment a_penalty incorrectly???" — open bug | **Already fixed.** The comment now reads "Previously this loop double-counted … fixed by resetting to beforePenalty/beforeCost each pass" (`:137`, snapshot at `:19-25`). Not an open finding. | +| pegging has "only 2 tests" | **9 golden + 3 smoke-only** (12 `test/pegging_*`). | +| pegging `visited` set "uninitialised → UB / corruption" | **False.** `set visited` is a member object, auto-default-constructed; the cycle guard works. | +| `operatordelete` dangerous code is active | **Disabled** — inside a `/* */` block. | +| `MAXSTATES` overrun = crash / no fallback | Throws a **catchable `RuntimeException`** (handled). | + +## E1 status & handoff to E2 + +E1 gate now complete: review report (this) ✅, ASan+UBSan blocking+clean ✅, clang-tidy baseline ✅. The +prioritised E2 work-list this review hands off: +1. **Pegging golden coverage** for the 3 smoke-only cases + deep-BOM + a cycle case (closes H4, the biggest oracle gap). +2. **Structural-invariant assertions** in the runner (capacity never exceeded, demand ≤ due-or-flagged) — catches H1 class issues line-diffing misses. +3. **Triage the ~10 high-signal clang-tidy findings** (NullDereference / CallAndMessage / NewDelete / uninitialised), then flip clang-tidy to a diff-gate. +4. **Stress scenario** (10k+ operationplans) with solve-time + peak-memory baselines — exercises H1/H2 at scale. diff --git a/tools/modernization/gates.py b/tools/modernization/gates.py new file mode 100644 index 0000000000..3e7b007468 --- /dev/null +++ b/tools/modernization/gates.py @@ -0,0 +1,556 @@ +#!/usr/bin/env python3 +""" +Modernization progress tracker — single source of truth for the verification gates +described in MODERNIZATION_PLAN.md. + +Each gate has a status: + - "active": implemented; its check() runs and MUST pass (failing one fails CI). + - "pending": not implemented yet; listed for visibility, never fails the build. + +As each phase is built, flip its gates from "pending" to "active" and give them a real +check(). The CI job renders the table below to the GitHub step summary so progress is +visible on every push. + +Stdlib only. Run: python tools/modernization/gates.py +Exit code: 0 if all ACTIVE gates pass; 1 if any active gate fails. +""" + +import os +import sys + +REPO = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + + +def has_file(*parts): + return os.path.isfile(os.path.join(REPO, *parts)) + + +def has_dir(*parts): + return os.path.isdir(os.path.join(REPO, *parts)) + + +def asan_blocking(): + """True if the engine ASan CI job exists and is a hard gate (not informational).""" + f = os.path.join(REPO, ".github", "workflows", "engine-asan.yml") + if not os.path.isfile(f): + return False + return "continue-on-error" not in open(f, encoding="utf-8").read() + + +def file_contains(parts, *needles): + """True if the file exists and contains every given substring.""" + f = os.path.join(REPO, *parts) + if not os.path.isfile(f): + return False + content = open(f, encoding="utf-8").read() + return all(n in content for n in needles) + + +def file_contains_any(parts, needles): + """True if the file exists and contains at least one of the substrings.""" + f = os.path.join(REPO, *parts) + if not os.path.isfile(f): + return False + content = open(f, encoding="utf-8").read() + return any(n in content for n in needles) + + +# Each gate: (phase, id, title, status, check) +# check is a zero-arg callable returning bool (only invoked for "active" gates). +GATES = [ + # ---- Bootstrap (this scaffolding) — active from day one ---- + ( + "Bootstrap", + "plan-present", + "Modernization plan committed", + "active", + lambda: has_file("MODERNIZATION_PLAN.md"), + ), + ( + "Bootstrap", + "gates-harness", + "Progress-gates harness runs", + "active", + lambda: True, + ), + ( + "Bootstrap", + "ci-workflow", + "Modernization CI workflow present", + "active", + lambda: has_file(".github", "workflows", "modernization.yml"), + ), + # ---- Phase 0 — API foundation ---- + ( + "Phase 0", + "openapi-schema", + "OpenAPI schema endpoint (drf-spectacular) served at /api/schema/", + "active", + lambda: file_contains(("requirements.txt",), "drf-spectacular") + and file_contains(("freppledb", "urls.py"), "SpectacularAPIView"), + ), + ( + "Phase 0", + "ts-client", + "Typed TS client generates from the schema + tsc-compiles (run in CI build job)", + "active", + lambda: file_contains( + ("tools", "modernization", "gen_api_client.sh"), + "spectacular", + "openapi-typescript", + "tsc", + ) + and file_contains( + (".github", "workflows", "ubuntu24.yml"), "gen_api_client.sh" + ), + ), + ( + "Phase 0", + "output-endpoints", + "Plan/forecast OUTPUT JSON endpoints (forecast/inventory/resource/demand/pegging)", + "active", + lambda: file_contains( + ("freppledb", "common", "api", "output.py"), + "JSONStreamView", + "report_class", + ), + ), + ( + "Phase 0", + "no-drf-serializer-output", + "Output endpoints reuse the report raw-SQL path, no DRF serializer", + "active", + lambda: file_contains( + ("freppledb", "common", "api", "output.py"), "report_class" + ) + and not file_contains_any( + ("freppledb", "common", "api", "output.py"), + # Catch both `import serializers` and `from ... import XSerializer`. + ("serializers import", "Serializer", "import serializ"), + ), + ), + ( + "Phase 0", + "jwt-auth", + "JWT auth works for REST + WS; 401/close on bad token (shared jwtauth util)", + "active", + lambda: file_contains(("freppledb", "common", "jwtauth.py"), "def decode_jwt") + and file_contains(("freppledb", "asgi.py"), "decode_jwt") + and file_contains( + ("freppledb", "common", "tests", "test_api_phase0.py"), + "test_ws_rejects_bad_token", + ), + ), + ( + "Phase 0", + "ws-scenario-routing", + "WS layer reads scenario from URL/header (not env var); websocket protocol enabled", + "active", + lambda: file_contains(("freppledb", "asgi.py"), "extract_scenario") + and file_contains( + ("freppledb", "asgi.py"), '"websocket": AllowedHostsOriginValidator' + ), + ), + # ---- Phase 1A — Websocket beachhead (Execute screen) ---- + ( + "Phase 1A", + "ws-task-progress", + "Live task progress over WS (ws/tasks/ + Task post_save broadcast; replaces 5s polling)", + "active", + lambda: file_contains( + ("freppledb", "asgi.py"), "TaskProgressConsumer", "ws/tasks/" + ) + and file_contains( + ("freppledb", "execute", "models.py"), "broadcast_task_progress" + ) + and file_contains(("requirements.txt",), "channels-redis"), + ), + ( + "Phase 1A", + "ws-log-tail", + "Live log tail over ws/tasks//log/ (poll<1s); pure stream_logfile + consumer", + "active", + lambda: file_contains( + ("freppledb", "asgi.py"), "TaskLogConsumer", "stream_logfile", "/log/" + ) + and file_contains( + ("freppledb", "common", "tests", "test_api_phase1a.py"), + "test_stream_logfile_tails_and_finishes", + ), + ), + ( + "Phase 1A", + "ws-fanout", + "Two clients on different pods see same updates", + "pending", + None, + ), + ( + "Phase 1A", + "spa-execute", + "Execute screen E2E-verified: auth->ws/tasks/ + real runplan -> live progress (engine compose)", + "active", + lambda: has_dir("frontend", "app", "execute") + and file_contains(("frontend", "app", "execute", "page.tsx"), "useTaskProgress") + and file_contains( + (".github", "workflows", "modernization.yml"), "Build Next.js frontend" + ) + and has_file("e2e", "docker-compose.engine.yml") + and file_contains( + ("e2e", "playwright", "tests", "live-progress.spec.ts"), "Run plan" + ), + ), + # ---- Phase 1B — Forecast Editor ---- + ( + "Phase 1B", + "fc-edit-parity", + "Edit+save re-nets; parity with legacy editor", + "pending", + None, + ), + ( + "Phase 1B", + "fc-bulk-edit", + "Bulk fill/copy/±% math (unit-tested) + bulk-row save; persistence E2E needs engine", + "active", + lambda: file_contains( + ("frontend", "lib", "forecastEdit.ts"), "applyPercent", "detectOutliers" + ) + and file_contains(("frontend", "lib", "forecastEdit.test.ts"), "applyPercent") + and file_contains(("frontend", "lib", "forecastSave.ts"), "saveBulkOverrides"), + ), + ( + "Phase 1B", + "fc-no-truncation", + "Forecast pivot returns all series (no top-300 cap); unit-tested with 500", + "active", + lambda: file_contains(("frontend", "lib", "forecast.ts"), "pivotForecast") + and file_contains(("frontend", "lib", "forecast.test.ts"), "fc-no-truncation"), + ), + ( + "Phase 1B", + "fc-a11y", + "Grid a11y: 0 critical axe violations (E2E scan on forecast + execute)", + "active", + lambda: file_contains( + ("e2e", "playwright", "tests", "a11y.spec.ts"), "AxeBuilder", "critical" + ) + and file_contains(("frontend", "app", "forecast", "page.tsx"), "aria-label"), + ), + # ---- Phase 2 — Odoo rework ---- + ( + "Phase 2", + "odoo-json-parity", + "JSON export byte-parity vs XML path (golden diff)", + "pending", + None, + ), + ( + "Phase 2", + "odoo-n1-fixed", + "N+1 eliminated: export_items/boms O(1) per entity", + "pending", + None, + ), + ( + "Phase 2", + "odoo-writeback-parity", + "Plan write-back creates same PO/MO/DO/WO", + "pending", + None, + ), + ( + "Phase 2", + "odoo-delta-sync", + "Delta sync: 1 changed BOM re-syncs only that BOM", + "pending", + None, + ), + # ---- Phase 3 — Expand UI (per-screen) ---- + ( + "Phase 3", + "inventory-report", + "Inventory/Buffer screen on /api/output/inventory/ (enriched pivot); E2E smoke + a11y", + "active", + lambda: has_dir("frontend", "app", "inventory") + and file_contains(("frontend", "app", "inventory", "page.tsx"), "PivotScreen") + and file_contains( + ("freppledb", "common", "api", "output.py"), "PivotJSONStreamView" + ) + and file_contains(("freppledb", "output", "urls.py"), "PivotJSONStreamView") + and file_contains(("frontend", "lib", "pivot.test.ts"), "parsePivot") + and file_contains( + ("e2e", "playwright", "tests", "smoke.spec.ts"), "Inventory report loads" + ) + and file_contains( + ("e2e", "playwright", "tests", "a11y.spec.ts"), "Inventory report" + ), + ), + ( + "Phase 3", + "pegging-gantt", + "Pegging Gantt with drag-drop reschedule", + "pending", + None, + ), + ( + "Phase 3", + "resource-capacity", + "Resource utilization report on /api/output/resource/ (enriched pivot); E2E smoke + a11y (timeline Gantt deferred)", + "active", + lambda: has_dir("frontend", "app", "resource") + and file_contains(("frontend", "app", "resource", "page.tsx"), "PivotScreen") + and file_contains(("frontend", "lib", "resource.ts"), "/api/output/resource/") + and file_contains(("freppledb", "output", "urls.py"), "PivotJSONStreamView") + and file_contains( + ("e2e", "playwright", "tests", "smoke.spec.ts"), "Resource report loads" + ) + and file_contains( + ("e2e", "playwright", "tests", "a11y.spec.ts"), "Resource report" + ), + ), + ("Phase 3", "constraint-problem", "Constraint/problem views", "pending", None), + ("Phase 3", "crud-grids", "Remaining CRUD grids migrated", "pending", None), + # ---- Phase 3.5 — Helm / deployment ---- + ( + "Phase 3.5", + "helm-install", + "helm install green on clean cluster; helm test passes", + "pending", + None, + ), + ( + "Phase 3.5", + "lb-ws-fanout", + "WS message crosses pods (channels_redis)", + "pending", + None, + ), + ( + "Phase 3.5", + "probes", + "Liveness/readiness gate traffic correctly", + "pending", + None, + ), + ( + "Phase 3.5", + "no-oomkill", + "Large plan does not OOMKill worker (MAXMEMORYSIZE≤limit)", + "pending", + None, + ), + ( + "Phase 3.5", + "zero-downtime", + "helm upgrade = zero-downtime rollout", + "pending", + None, + ), + # ---- Phase 4 — optional Go/Rust BFF ---- + ( + "Phase 4", + "bff-justified", + "BFF justified by a measured metric, not preference", + "pending", + None, + ), + ( + "Phase 4", + "bff-contract", + "BFF passes same API contract tests as Django", + "pending", + None, + ), + # ---- Engine track (parallel) — review, tests, DDMRP, Rust decision ---- + ( + "Engine E1", + "review-report", + "Engine code-review + debt/TODO triage report committed", + "active", + lambda: has_file("ENGINE_REVIEW.md"), + ), + ( + "Engine E1", + "sanitizers", + "ASan runs green over the golden suite in CI (UBSan TBD)", + "active", + lambda: has_file(".github", "workflows", "engine-asan.yml"), + ), + ( + "Engine E1", + "clang-tidy-baseline", + "clang-tidy baseline captured; no new warnings", + "pending", + None, + ), + ( + "Engine E2", + "pegging-tests", + "Pegging tests 2->12 (split/transfer/multi-level/offset/alternate/routing); followPegging casts guarded, crash was macOS-local", + "active", + lambda: sum( + 1 + for d in os.listdir(os.path.join(REPO, "test")) + if d.startswith("pegging_") and os.path.isdir(os.path.join(REPO, "test", d)) + ) + >= 12, + ), + ( + "Engine E2", + "structural-asserts", + "Golden-free structural-invariant scenarios (qty>=0, end>=start) over material/distribution/resource", + "active", + lambda: sum( + 1 + for d in os.listdir(os.path.join(REPO, "test")) + if d.startswith("structural_") + and os.path.isdir(os.path.join(REPO, "test", d)) + ) + >= 3, + ), + ( + "Engine E2", + "stress-baseline", + "10k+ operationplan stress scenario w/ time+mem baseline", + "pending", + None, + ), + ( + "Engine E2", + "sanitizer-ci", + "Blocking ASan CI job; golden suite ASan-clean", + "active", + asan_blocking, + ), + ( + "Engine E3", + "ddmrp-optin", + "Per-buffer DDMRP opt-in; MRP buffers unchanged (parity)", + "pending", + None, + ), + ( + "Engine E3", + "ddmrp-zones", + "R/Y/G zone + NFP-triggered replenishment match oracle", + "pending", + None, + ), + ( + "Engine E3", + "ddmrp-spike", + "Spike-horizon qualification filters order spikes", + "pending", + None, + ), + ( + "Engine E3", + "ddmrp-decouple", + "Decoupling point stops BOM explosion at buffer", + "pending", + None, + ), + ( + "Engine E4", + "rust-pilot-parity", + "Rust/PyO3 pilots (json kernel + forecast MovingAverage) pass Rust-vs-C++ parity diffs", + "active", + lambda: file_contains( + ("rust", "frepple-num", "src", "num.rs"), "forbid(unsafe_code)" + ) + and file_contains(("test", "rust_parity", "test_parity.py"), "frepple_num") + and file_contains( + ("rust", "frepple-forecast", "src", "forecast.rs"), "forbid(unsafe_code)" + ) + and file_contains( + ("test", "rust_parity", "test_forecast_parity.py"), "frepple_forecast" + ) + and has_file(".github", "workflows", "rust-pilot.yml"), + ), + ( + "Engine E4", + "rust-measured", + "Measured LOC/perf/safety comparison vs C++ recorded", + "active", + lambda: file_contains( + ("tools", "modernization", "rust-pilot.md"), "Measurements", "Safety", "LOC" + ), + ), + ( + "Engine E4", + "rust-decision", + "Rust go/no-go documented from evidence (stop = success)", + "active", + lambda: file_contains( + ("tools", "modernization", "rust-pilot.md"), "Decision", "GO" + ), + ), +] + +STATUS_ICON = {"pass": "✅", "fail": "❌", "pending": "⬜"} + + +def evaluate(): + rows = [] + failures = 0 + for phase, gid, title, status, check in GATES: + if status == "active": + try: + ok = bool(check()) + except Exception as e: # a check that errors counts as a failure + ok = False + title = f"{title} (error: {e})" + result = "pass" if ok else "fail" + if not ok: + failures += 1 + else: + result = "pending" + rows.append((phase, gid, title, result)) + return rows, failures + + +def render(rows): + active = [r for r in rows if r[3] in ("pass", "fail")] + passing = [r for r in active if r[3] == "pass"] + total = len(rows) + done = len(passing) + pct = int(100 * done / total) if total else 0 + + lines = [] + lines.append("# Modernization progress\n") + lines.append( + f"**{done}/{total} gates passing ({pct}%)** — " + f"{len(active)} active, {total - len(active)} pending\n" + ) + bar_len = 24 + filled = int(bar_len * done / total) if total else 0 + lines.append("`[" + "█" * filled + "·" * (bar_len - filled) + f"] {pct}%`\n") + + current_phase = None + for phase, gid, title, result in rows: + if phase != current_phase: + lines.append(f"\n## {phase}\n") + current_phase = phase + lines.append(f"- {STATUS_ICON[result]} `{gid}` — {title}") + return "\n".join(lines) + "\n" + + +def main(): + rows, failures = evaluate() + summary = render(rows) + print(summary) + + step_summary = os.environ.get("GITHUB_STEP_SUMMARY") + if step_summary: + with open(step_summary, "a", encoding="utf-8") as fh: + fh.write(summary) + + if failures: + print(f"::error::{failures} active gate(s) failing", file=sys.stderr) + return 1 + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/tools/modernization/gen_api_client.sh b/tools/modernization/gen_api_client.sh new file mode 100755 index 0000000000..ca34abf41e --- /dev/null +++ b/tools/modernization/gen_api_client.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# Regenerate the typed TypeScript client from the frePPLe OpenAPI schema (Phase 0). +# +# Two committed artifacts are the source of truth (so the SPA builds/typechecks +# without a Django runtime), and CI runs this script + `git diff --exit-code` to +# guarantee they never drift from the live API: +# +# generated/openapi.yaml - the OpenAPI 3 schema (drf-spectacular) +# frontend/lib/api-types.ts - the typed client (openapi-typescript) +# +# Run where a frePPLe Django runtime + Node.js are available (a dev box or CI). +# Usage: tools/modernization/gen_api_client.sh +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +cd "$ROOT" + +SCHEMA="generated/openapi.yaml" +mkdir -p generated + +# 1. Emit the OpenAPI 3 schema from the Django app. --validate fails on an +# invalid schema (operationId collisions are warnings, auto-resolved). +echo ">> generating OpenAPI schema -> ${SCHEMA}" +./frepplectl.py spectacular --validate --file "${SCHEMA}" + +# 2. Generate the typed client into the SPA (openapi-typescript). npx auto-fetches +# it, so CI needn't `pnpm install` the frontend. Same output path as the +# frontend's own `gen:api` script (lib/api-types.ts). +CLIENT="frontend/lib/api-types.ts" +# Pin to the frontend's openapi-typescript version so CI regeneration is +# byte-identical to a local `pnpm gen:api` (the drift gate compares them). +OAT_VERSION="$(node -p "const p=require('./frontend/package.json'); (p.devDependencies||{})['openapi-typescript'] || (p.dependencies||{})['openapi-typescript'] || '7.0.0'" 2>/dev/null || echo 7.0.0)" +echo ">> generating typed client -> ${CLIENT} (openapi-typescript@${OAT_VERSION})" +npx --yes "openapi-typescript@${OAT_VERSION}" "${SCHEMA}" -o "${CLIENT}" + +# 3. Type-check the generated client (strict) so a bad schema fails the build. +echo ">> type-checking the generated client" +npx --yes -p typescript tsc --noEmit --strict --skipLibCheck "${CLIENT}" + +echo ">> done: ${SCHEMA}, frontend/lib/api-types.ts" diff --git a/tools/modernization/rust-pilot.md b/tools/modernization/rust-pilot.md new file mode 100644 index 0000000000..0c37250c90 --- /dev/null +++ b/tools/modernization/rust-pilot.md @@ -0,0 +1,181 @@ +# Rust/PyO3 pilot — evidence + decision (Engine track E4) + +**Question (MODERNIZATION_PLAN.md §E4):** pilot one isolated module in Rust via PyO3, prove parity +with the C++, measure LOC/perf/safety, and document a go/no-go — "stop = success". + +**What was ported.** The JSON number-conversion kernel of `src/utils/json.cpp` — +`getLong` (790–815), `getUnsignedLong` (817–841), `getInt` (865–890): the `double → clamped integer` +and `string → integer` (`atol`) logic. This is the **exact site of the inverted-bound bug** fixed +earlier in the C++ (`< LONG_MIN` had been `> LONG_MIN`, which made `getLong` return `LONG_MIN` for +ordinary doubles). Implementation: `rust/frepple-num/` (a PyO3 extension built with maturin), +decoupled from the C++/CMake build. + +## Parity (rust-pilot-parity) + +`test/rust_parity/test_parity.py` diffs the Rust extension against a standalone C++ reference +(`tools/rust-pilot/cxx_reference.cpp`) that copies the json.cpp branches **verbatim** — a true +Rust-vs-C++ diff, not a hand-authored expectation. **24/24 vectors pass** locally and in CI: + +- **19 "agree" vectors** (normal values, truncation toward zero, out-of-range saturation, string + parses) match the C++ reference byte-for-byte — including the regression case `clamp_to_long(5.0) → 5` + (the C++ bug returned `LONG_MIN` here). +- **5 "rust_safe" vectors** exercise inputs the C++ leaves **undefined**, where Rust is defined: + `NaN → 0` (C++ `static_cast(NaN)` is UB), `±inf → i64::MIN/MAX`, and `negative → unsigned` + saturates to `0` (the C++ has no lower clamp and wraps to a huge value). + +## Measurements (rust-measured) + +| Metric | C++ (`json.cpp` getters) | Rust (`frepple-num`) | +| --- | --- | --- | +| LOC (the three getters / the four ported fns) | 98 | 35 | +| Clamp kernel — the bug site | ~4 hand-written lines per getter (`if d > MAX … else if d < MIN …`) | **1 line** (`x as i64`, saturating) | +| `unsafe` blocks in the conversion logic | n/a (all C++ is unsafe by nature) | **0** (compile-enforced by `#![forbid(unsafe_code)]`) | +| Float→int out-of-range | manual clamp (got it wrong once) | saturating by language definition | +| `NaN` handling | UB (`static_cast`) | defined (`→ 0`) | +| Perf | in-process, a few instructions | ~39 ns/call **including** the Python→Rust FFI hop; the arithmetic itself is a few instructions, same as C++ | + +Notes on fairness: the C++ getters are larger partly because they dispatch the full JSON tagged union +(8 type cases); the Rust port covers the numeric kernel that carried the bug. Perf is **not** the +differentiator — this is a leaf conversion, not a hot path, and both compile to a handful of +instructions; the ~39 ns is dominated by the Python FFI boundary (irrelevant here). The headline is +**safety**, not speed. + +## Safety / maintainability assessment + +- The specific shipped bug (inverted clamp) and two latent UB sinks (`NaN` cast, negative→unsigned + wrap) are **all impossible or defined** in safe Rust — for free, via saturating `as` casts. +- `#![forbid(unsafe_code)]` makes "zero unsafe in our logic" a **compile-time guarantee**, not a + review promise. The only `unsafe` is inside the vetted `pyo3` FFI glue — unavoidable for *any* + Python extension, and a tiny audited surface vs. the C++ engine which is unsafe in its entirety. +- `cargo test` runs the logic with **zero Python/toolchain dependency** (pyo3 is an optional, + feature-gated dep), so the numeric core is unit-tested in isolation in ~5 s. + +## Build / integration cost + +- maturin wheel, **no CMake/Cargo coupling** (Option A). Standalone CI job + (`.github/workflows/rust-pilot.yml`): `cargo test` + build the C++ ref + maturin build + parity + `pytest`, ~1–2 min, independent of the slow engine build and the staging deploy. +- A Rust toolchain in the *engine image* (~150 MB) is **deferred** — only needed if we ship the wheel, + which is a "go"-only fast-follow. + +## Forecast-method conversion progress + +Porting `src/forecast/` method-by-method (each a CI-only parity slice). Status: + +| Method | Rust | Parity vs C++ ref | Notes | +| --- | --- | --- | --- | +| MovingAverage | `forecast.rs` | ✅ | the `weight[]` OOB site | +| SingleExponential | `single_exp.rs` | ✅ | 1D Levenberg-Marquardt; shared `common.rs` extracted | +| DoubleExponential | `double_exp.rs` | ✅ | 2D Marquardt + 2x2 Hessian (shared `solve_2x2_marquardt`) | +| Croston | `croston.rs` | ✅ | intermittent demand; alfa grid-search, upper-only outliers | +| Seasonal | `seasonal.rs` | ✅ | Holt-Winters; autocorrelation cycle detection + seasonal-factor state (period/force/S_i all parity-checked) | + +**All five forecast methods ported and parity-verified** (57 parity tests; smape/stddev/forecast within +1e-9, outliers/period/force/seasonal-factors exact). Shared helpers in `common.rs` (`smape_weight`, +weight table, constants, the `Forecast` result, `solve_2x2_marquardt` — bit-for-bit with the C++ +damping/singular-retry order, used by DoubleExp + Seasonal). + +## Phase 7 — engine integration (C ABI + the byte-parity finding) + +**Done + CI-covered:** the crate now also builds a `staticlib` with a C ABI +(`src/capi.rs` + `tools/rust-pilot/frepple_forecast.h`) — `extern "C"` wrappers for all five methods, +the *only* `unsafe` in the crate (the FFI boundary; the numeric modules stay `#![forbid(unsafe_code)]`). +A committed C harness (`tools/rust-pilot/capi_harness.c`) links `libfrepple_forecast.a` and calls the +methods exactly as `libfrepple` would (MovingAverage→8.0, Seasonal→period 7), run in the `rust-pilot` +CI. So the FFI link that the engine integration needs is proven. + +**Key finding — byte-exact parity needs `-ffp-contract=off`.** The Rust matches the standalone C++ +reference to ~1e-9 but **not** bit-for-bit (~14/33 vectors exact). Cause: `g++ -O2` defaults to +`-ffp-contract=fast` (FMA fusion of `a*b+c` into one rounding); rustc does not contract. So the engine +(C++, FMA-on) and the Rust port differ by a few ULPs on the same inputs. Implications for the remaining +flag-gated dispatch + `forecast_*` golden run: +- Method *selection* is robust — a ULP-level `smape` difference won't flip the lowest-error winner. +- The forecast *values* differ by ULPs; whether the `.expect` golden output stays byte-identical depends + on its print precision. The clean way to guarantee it is to build the forecast translation unit with + `-ffp-contract=off` (matching Rust), which the integration should set. + +**Implemented (default-OFF, CI-gated):** +- `option(FREPPLE_RUST_FORECAST OFF)` in the root `CMakeLists.txt`; when ON, `src/CMakeLists.txt` + cargo-builds `libfrepple_forecast.a`, links it into the `forecast` lib, defines + `FREPPLE_RUST_FORECAST=1`, and compiles the forecast TU with `-ffp-contract=off` (match rustc, no FMA). +- `src/forecast/timeseries.cpp`: **all five methods** (MovingAverage, SingleExponential, Croston, + DoubleExponential, Seasonal) `generateForecast` now dispatch to their `extern "C"` Rust functions behind + the flag (**5/5**). The engine passes `timeseries.data(), count` — verified to be the same `[0..count-1]` + data points (trailing-0 placeholder at `[count]`) the parity reference + Rust port consume. Engine model + mutation (`ProblemOutlier`, `applyForecast`) stays in C++; the Rust returns the numbers + the state apply + needs. MA/SE/Croston write a **constant** forecast (`avg`/`f_i`) — the scalar C-ABI suffices. +- **DoubleExp + Seasonal needed C-ABI extensions, because `applyForecast` extrapolates per bucket:** + - DoubleExp returns the **decomposed** `constant`/`trend` (`double_exponential_state`; the + `double_exponential` wrapper stays for the PyO3/parity path). + - Seasonal returns `L_i`/`T_i`/`cycleindex` alongside `period`/`s_i`. Because the one-step `forecast` only + pins `l_i + t_i/period` (not the components) and never checked `cycleindex`, a **dedicated apply-state + parity check** was added first — the verbatim C++ reference + Rust now emit `L_i`/`T_i`/`cycleindex` and + `test_forecast_parity` asserts they match (cycleindex = count%period; level/trend within 1e-9). **They + match**, so wiring was verified-safe, not a gamble. +- **Golden gate CI:** `.github/workflows/forecast-phase7.yml` builds `-DFREPPLE_RUST_FORECAST=ON` and runs + `runtest.py` over `test/forecast_1..11` (byte-exact vs `.expect`). **GREEN at 5/5** — every forecast + method runs in Rust in-engine with byte-exact golden parity under `-ffp-contract=off`. The FP-contraction + question is settled positively across the whole forecast surface. + +**The forecast C++→Rust conversion is functionally complete and is now the forecast source of truth on +staging.** The 5/5 golden gate + the 57+3 parity tests were the evidence; the product decision to flip ON +has been taken for the review env. + +**Shipped to staging (Rust ON) — PR #12, helm REVISION 7:** +- `deploy-staging.yml` builds the `frepple-app` image with `FREPPLE_RUST_FORECAST=ON`; everywhere else + (e2e stack, local) stays default-OFF on the C++ path. +- `e2e/Dockerfile.engine` installs a minimal `rustup` toolchain (only when the flag is ON) before the C++ + compile, and threads the flag into cmake so cargo builds + links the staticlib. +- Two build bugs found + fixed while validating on aarch64 (neither caught by x86 CI): + - `src/CMakeLists.txt` now `ranlib`s the cargo staticlib — rustc emits it without a symbol-table index + on some toolchains, which GNU `ld` rejects (`archive has no index`). No-op where one already exists. + - `.dockerignore` excludes `**/target` — a host-arch `target/` leaking into the build context shadowed + the in-image cargo build (CMake saw the OUTPUT present, skipped it), linking a wrong-arch staticlib. +- **Live verification:** the deployed `libfrepple.so` embeds all five `extern "C"` wrappers, and + `runtest.py forecast_1..11` run *inside the deployed pod* pass byte-exact (11/11) against the `.expect` + baselines. Reversible by the flag (rebuild OFF + helm rollback). + +The conversion is the last step of Engine track **E4**. Default stays OFF outside staging until a broader +production decision; the flag makes the flip fully reversible. + +## Slice 2 — forecast (MovingAverage), the real algorithm + +Slice 1 was a trivial clamp; slice 2 ports an actual forecasting method: +`ForecastSolver::MovingAverage::generateForecast` (`src/forecast/timeseries.cpp:294-384`) + the +`smapeWeight` recency weighting (`forecast.h:3041-3054`) — the **other fixed memory-bug site** (the +`weight[]` out-of-bounds read on histories longer than `MAXBUCKETS=500`). Crate: `rust/frepple-forecast/`. + +- **Parity** (`test/rust_parity/test_forecast_parity.py`, **10/10**): the Rust `moving_average` is diffed + against a verbatim C++ reference (`tools/rust-pilot/forecast_reference.cpp`) over constant / trend / + outlier / intermittent / fractional series **and** two >`MAXBUCKETS` series (the OOB case). `smape`, + `standarddeviation` and `avg` match within a 1e-9 relative epsilon (same f64 op order); outlier index + sets match exactly. +- **LOC: comparable, not smaller** — Rust ~109 (incl. the weight-table helper + result struct + + explicit-index loops) vs ~73 for the C++ method body (+~10 for `smapeWeight`/weight init). On a tight + numeric loop, safe Rust is *roughly the same size*; the win here is **not** LOC. +- **Safety:** **0 `unsafe`** (compile-enforced); the `weight[]` OOB read is impossible — indexing is + bounds-checked and the clamp is one line. The engine-model coupling (the two `new ProblemOutlier(...)` + writes) is the only thing left in C++; the port returns outlier indices instead (numeric kernel, not + the model mutation). +- **Honest caveat:** parity required mirroring the C++ float operation order exactly. That's the cost of + a numeric port — bit-level reproducibility is a real constraint, and a careless rewrite would drift. + +## Decision (rust-decision) + +**Conditional GO — for targeted Rust on isolated, numeric, safety-critical leaf modules; NO-GO for a +wholesale engine rewrite.** + +The evidence across both slices is consistent: Rust eliminates *this exact class* of memory/UB bug by +construction (the json clamp and the forecast `weight[]` OOB), at a low, decoupled integration cost and +no meaningful perf trade-off. LOC is **not** the headline — slice 2 showed safe Rust is roughly the same +size as the C++ for tight numeric code; the value is the compile-enforced safety + the clean PyO3 linkage +(no manual refcounting — the very `python.cpp` refcount/UB bugs the modernization fixed). That justifies +continuing **incrementally**: the next forecast slices (SingleExponential / DoubleExponential / Seasonal / +Croston — iterative optimisers) port behind the same maturin/PyO3 pattern, with the C++ remaining the +shipping path until a method reaches full golden-parity; if a method proves too entangled to port cleanly, +that itself is recorded evidence. + +A full rewrite of the deeply C++-coupled engine (object graph, embedded CPython, solver) is **not** +justified by this evidence — the cost/risk is enormous and most of the engine is not the bug-prone, +isolatable, numeric code where Rust's guarantees pay off cleanly. "Targeted, evidence-gated, leaf-first" +is the supported path; "rewrite the engine" is not. diff --git a/tools/modernization/solver-spike.md b/tools/modernization/solver-spike.md new file mode 100644 index 0000000000..365c26a9f5 --- /dev/null +++ b/tools/modernization/solver-spike.md @@ -0,0 +1,64 @@ +# Solver spike — optimisation engine for finite-capacity planning (Engine track) + +**Question.** frePPLe's shipping MRP solver is a fast *constructive heuristic* (peg demand → supply, +operation by operation). It's strong at feasibility but doesn't optimise a global objective. Could an +**advanced optimisation engine, orchestrated from Rust**, power a *greenfield* finite-capacity / DDMRP +planning mode where that gap matters — and at what integration cost? Same evidence-gated, "stop = success" +playbook as the forecast Rust pilot (`rust-pilot.md`). + +**What was built.** `rust/solver-spike/` — a small **capacitated multi-period production-planning LP** +(2 products × 4 periods, one shared capacity-constrained resource) modelled with the **`good_lp`** modelling +layer on the **pure-Rust `microlp` backend**. It solves to optimality and compares against a **lot-for-lot** +plan (what a naive feasibility-first MRP pass produces). One file, ~190 LOC, no C++ dependency. + +## Result (`cargo run -p solver-spike`) + +``` +Lot-for-lot (feasibility-first heuristic, like a naive MRP pass): + product 1: produce [ 20.0, 20.0, 20.0, 60.0] + product 2: produce [ 15.0, 15.0, 15.0, 15.0] + ! capacity OVERLOAD per period: [0.0, 0.0, 0.0, 25.0] + -> the lot-for-lot plan is INFEASIBLE on the tight resource. + +Optimal (capacitated LP, microlp): + product 1: produce [ 20.0, 20.0, 30.0, 50.0] + product 2: produce [ 15.0, 25.0, 20.0, 0.0] + cost: 187.00 (capacity-feasible; pre-builds ahead of the period-4 spike) +``` + +**Interpretation.** Period-4 demand (60 + 15 = 75) exceeds one period's capacity (50), so lot-for-lot is +simply **infeasible**. The LP finds the cheapest **capacity-feasible** plan: pre-build product 1 (30 in +period 3) and product 2 (25 in period 2) so every period's load ≤ 50, paying a small, *quantified* holding +premium (187 vs the naive-but-infeasible 180 = the price of feasibility the heuristic couldn't even reach). +That trade-off — build-ahead vs capacity — is exactly what a constructive heuristic can't reason about and +an optimiser nails. + +## Measurements + +| Metric | Finding | +| --- | --- | +| **Integration cost** | One crate, one dependency (`good_lp` + `microlp`); compiles in ~5 s. No C++/CMake, no system solver. The model is ~40 LOC of declarative constraints. | +| **Backend portability** | `good_lp` is a *modelling layer* over CBC / HiGHS / SCIP / Clarabel / microlp. The same model swaps to **HiGHS** (top-tier open-source MILP) behind a feature flag when instances grow — no model rewrite. | +| **Pure-Rust option** | `microlp` solves the LP with **zero non-Rust deps** — the cleanest possible integration for small/medium instances; the only *pure-Rust* path. (HiGHS/CBC/SCIP are C++ — better-maintained solvers driven from Rust, not pure-Rust engines.) | +| **Capability vs the heuristic** | Demonstrated: finds a feasible plan where lot-for-lot overloads, and optimises the build-ahead/holding trade-off. The shipping solver does neither. | +| **Memory safety** | Same story as the forecast pilot — safe Rust orchestration; `#![forbid(unsafe_code)]`-compatible (no unsafe in the spike). | + +## Decision (solver-decision) + +**Conditional GO — as a greenfield, optional finite-capacity / DDMRP *mode*, NOT a replacement for the MRP +solver.** + +- The value is real and the heuristic genuinely can't produce it (feasible capacity-tight plans + explicit + cost trade-offs). `good_lp` makes the integration cheap and solver-portable (pure-Rust `microlp` now, + HiGHS for scale), and it's a *new* capability so there's **no parity tax** — unlike the forecast port, + there's no C++ behaviour to reproduce, just a new objective to optimise. +- **NO-GO on replacing the constructive solver.** It's fast, feasibility-strong on the full BOM/routing + object graph, and battle-tested; the optimiser is a **complement** for the capacity-tight sub-problem + (and the natural home for a DDMRP mode), not a wholesale swap. A real deployment also has to map frePPLe's + rich model (alternates, calendars, lot-size rules, lead times) onto the LP/MILP — a meaningful modelling + effort this spike deliberately scopes out. + +**Recommended next step (if pursued):** lift the toy instance to a real frePPLe sub-scenario (one resource, +its operations + demands over the bucket horizon), model it with `good_lp`, and benchmark the HiGHS backend +on instance sizes that matter — then a go/no-go on shipping a `plan --capacity-optimise` mode behind a flag, +exactly like the forecast `FREPPLE_RUST_FORECAST` pattern. diff --git a/tools/modernization/ubsan-baseline.md b/tools/modernization/ubsan-baseline.md new file mode 100644 index 0000000000..bd96d7a4aa --- /dev/null +++ b/tools/modernization/ubsan-baseline.md @@ -0,0 +1,85 @@ +# UBSan baseline — Engine track E1 + +**Goal (MODERNIZATION_PLAN.md §E1):** run the already-wired sanitizers over the golden test suite, +document findings by severity, establish a gate. ASan is done (`engine-asan.yml`, blocking, the golden +suite is ASan-clean). This is the **UndefinedBehaviorSanitizer** half. + +## How to reproduce + +```bash +cmake -B build -DCMAKE_BUILD_TYPE=Debug -DFREPPLE_SANITIZER=undefined +cmake --build build --target frepple-main -j +# run a golden test directly (runtest.py PIPEs+discards child stderr unless -d): +UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=0" \ + FREPPLE_HOME=bin LD_LIBRARY_PATH=bin bin/frepple -validate test/forecast_11/forecast_11.xml +# or the whole suite, streamed: +UBSAN_OPTIONS="print_stacktrace=1:halt_on_error=0" ./test/runtest.py -d +``` + +`CMakeLists.txt` selects the sanitizer for the Debug build via `-DFREPPLE_SANITIZER=` (`address` default += the existing ASan gate; `undefined` = UBSan; `address,undefined` = both). The UBSan build **excludes +`-fsanitize=vptr`** — see Finding 1. + +## Baseline run + +96 pure-engine (type-2) golden tests run directly under the executable (the 1 type-1 dir, which needs +Django/DB, skipped). The full suite incl. type-1 runs in CI (`engine-ubsan.yml`). UBSan output is +**memory-safe** (ASan is separately clean); these are *undefined-behaviour* diagnostics — code the +optimiser is permitted to miscompile, not live memory corruption. + +## Findings (by severity) + +### Finding 1 — `vptr`: custom MetaClass RTTI is incompatible with `-fsanitize=vptr` — **NOISE / by-design — EXCLUDED** +Sites: `include/frepple/utils.h:6252, 6359, 6363, 6372, 6469, 6678, 6691, 3358` (and others), reported as +*"member call / downcast on address which does not point to an object of type `Buffer`/`Resource`/`Flow`/ +`Operation`/`Demand`/…"*. frePPLe does not use C++ polymorphism for its model objects; it has a +hand-rolled type system (`MetaClass`/`MetaCategory`, downcast by type tag). UBSan's `vptr` check verifies +the C++ dynamic type via the vtable and cannot see frePPLe's tag-based identity, so **every** model +downcast trips it. These are not bugs. **Decision:** `-fno-sanitize=vptr` in the UBSan build +(`CMakeLists.txt`), documented here. Re-enabling vptr would require reworking the object model onto +standard RTTI — out of scope, and the cast sites are exercised billions of times in production. + +### Finding 2 — iterator `operator*` forms a reference to null at `end()` — **LOW (idiom) — RESOLVED** +Sites: `include/frepple/timeline.h:293` (`const Event& operator*() const { return *cur; }`) and +`include/frepple/model.h:8667` (`Problem& operator*() const { return *iter; }`). When the iterator is at +`end()` / default-constructed, `cur`/`iter` is null and `*cur` *binds a reference to null* — UB by the +letter, but the value is never dereferenced (`operator++` guards `if (cur)`, and callers compare against +`end()` before deref). This is the **same UB the standard library has** for `*v.end()`. It fired in nearly +every test (78× each across the full suite) because timelines/problem-lists are iterated everywhere. +**Resolution (E2 slice 1):** both `operator*` are marked `FREPPLE_NO_SANITIZE_NULL` +(`__attribute__((no_sanitize("null")))`, defined in `utils.h`; g++ accepts it, no runtime suppressions +file needed, a no-op in non-sanitized builds). Verified: g++ emits no attribute warning and the full +golden suite is then UBSan-clean (0 findings), which is what let the gate flip to **blocking**. + +### Finding 3 — `OperationDependency::set{Operation,BlockedBy}` null member-call — **MEDIUM (real) — FIXED** +Sites: `src/model/operationdependency.cpp:99` and `:122`. Symmetric bug: when only one side of a +dependency is set, the code called `blockedby->addDependency(this)` / `oper->addDependency(this)` on a +**null** receiver. It "worked" (and ASan stayed clean) only because `Operation::addDependency` early-returns +on an incomplete dependency (`if (!dpd->getOperation() || !dpd->getBlockedBy()) return;`) *before* touching +the null receiver's members — but forming a member call on a null `this` is UB regardless, and the +optimiser may assume `this != nullptr`. UBSan caught `:122` directly (1 test); `:99` is its mirror image, +unexercised by the current golden data but the identical defect. **Fix:** guard both calls +(`if (oper) …` / `if (blockedby) …`). Provably behaviour-preserving — `addDependency` no-ops in exactly +the guarded case — so it removes the UB without changing any output. Golden suite stays byte-identical. + +## Gate posture + +`engine-ubsan.yml` runs the full golden suite under UBSan as a **blocking** gate: `halt_on_error=1` + +`abort_on_error`, so a single UB diagnostic fails the job — the same contract as `engine-asan.yml`. The +suite is UBSan-clean after the three findings below were resolved/excluded, so a *new* UB site is a CI +failure, with the offending site shown in the job step-summary. `runtest.py -d` streams the child stderr +(otherwise PIPE'd + discarded) so the report is visible, not just a bare non-zero exit. + +This followed the same path as `engine-asan.yml`: it landed **advisory** (`halt_on_error=0`) as a documented +baseline (E1), then flipped to **blocking** in E2 slice 1 once Finding 2 was retired (Finding 1 excluded, +Finding 3 fixed) — zero remaining findings. + +## Status + +| Finding | Class | Severity | Disposition | +| --- | --- | --- | --- | +| 1. vptr on MetaClass RTTI | by-design false positive | noise | `-fno-sanitize=vptr` (excluded) | +| 2. iterator `operator*` null-binding | UB idiom (STL-parallel) | low | **resolved** (`FREPPLE_NO_SANITIZE_NULL`) | +| 3. operationdependency null member-call | real latent UB | medium | **fixed** (`:99`, `:122`) | + +Both sanitizers are now blocking + clean: ASan (`engine-asan.yml`) and UBSan (`engine-ubsan.yml`). diff --git a/tools/rust-pilot/capi_harness.c b/tools/rust-pilot/capi_harness.c new file mode 100644 index 0000000000..c0abb76025 --- /dev/null +++ b/tools/rust-pilot/capi_harness.c @@ -0,0 +1,51 @@ +/* Smoke test for the Rust forecast C ABI (Engine track E4, phase 7): links + * libfrepple_forecast.a and calls each method through the C boundary, the same + * way libfrepple will. Proves the FFI works end-to-end without the full engine. + * cc -O2 -I tools/rust-pilot capi_harness.c \ + * rust/frepple-forecast/target/release/libfrepple_forecast.a -o capi_harness + */ +#include +#include +#include "frepple_forecast.h" + +int main(void) { + double history[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; + double smape, stddev, forecast; + size_t outliers[16], olen; + int fail = 0; + + /* MovingAverage: mean of the last 5 (6..10) = 8.0 */ + double ma[4] = {5, 4.0, 0.95, 5}; + int rc = frepple_moving_average(history, 10, &smape, &stddev, &forecast, + outliers, 16, &olen, ma, 4); + printf("moving_average: rc=%d forecast=%.6f smape=%.6f\n", rc, forecast, smape); + if (rc != 0 || fabs(forecast - 8.0) > 1e-9) fail = 1; + + /* SingleExponential: just exercise the boundary + finiteness */ + double se[7] = {0.2, 0.03, 1.0, 4.0, 0.95, 5, 15}; + rc = frepple_single_exponential(history, 10, &smape, &stddev, &forecast, + outliers, 16, &olen, se, 7); + printf("single_exp: rc=%d forecast=%.6f\n", rc, forecast); + if (rc != 0 || !isfinite(forecast)) fail = 1; + + /* Seasonal: a clear period-7 cycle should detect period 7 */ + double cyc[70]; + double base[7] = {10, 25, 40, 55, 40, 25, 10}; + for (int i = 0; i < 70; ++i) cyc[i] = base[i % 7]; + double seas[14] = {0.2, 0.02, 1.0, 0.2, 0.2, 1.0, 0.05, + 2, 14, 0.5, 0.8, 0.95, 5, 15}; + unsigned int period; + int force; + double s_i[80]; + size_t s_i_len; + double l_i, t_i; + unsigned int cycleindex; + rc = frepple_seasonal(cyc, 70, seas, 14, &smape, &stddev, &forecast, &period, + &force, s_i, 80, &s_i_len, &l_i, &t_i, &cycleindex); + printf("seasonal: rc=%d period=%u force=%d s_i_len=%zu cycleindex=%u\n", rc, + period, force, s_i_len, cycleindex); + if (rc != 0 || period != 7) fail = 1; + + printf(fail ? "CAPI HARNESS: FAIL\n" : "CAPI HARNESS: OK\n"); + return fail; +} diff --git a/tools/rust-pilot/cxx_reference.cpp b/tools/rust-pilot/cxx_reference.cpp new file mode 100644 index 0000000000..c2373b2f88 --- /dev/null +++ b/tools/rust-pilot/cxx_reference.cpp @@ -0,0 +1,59 @@ +// Parity authority for the Rust pilot (Engine track E4). Standalone (no +// libfrepple): replicates the number-conversion semantics VERBATIM from +// src/utils/json.cpp:790-890 — the JSON_DOUBLE clamp branches of getLong / +// getInt / getUnsignedLong and the JSON_STRING (atol) branch — so the Rust +// implementation can be diffed against the real C++ behaviour, not a +// hand-authored expectation. +// +// NOTE: keep in sync with src/utils/json.cpp if those getters change. +// Build: g++ -O2 -o cxx_reference cxx_reference.cpp +// Usage: cxx_reference +#include +#include +#include +#include + +int main(int argc, char** argv) { + if (argc < 3) { + fprintf(stderr, "usage: %s \n", argv[0]); + return 2; + } + const std::string op = argv[1]; + + // JSON_STRING branch: atol (json.cpp:810/835/885). + if (op == "parse_long") { + printf("%ld\n", atol(argv[2])); + return 0; + } + + const double data_double = strtod(argv[2], nullptr); + if (op == "long") { + // getLong, JSON_DOUBLE (json.cpp:803-808). + if (data_double > LONG_MAX) + printf("%ld\n", LONG_MAX); + else if (data_double < LONG_MIN) + printf("%ld\n", LONG_MIN); + else + printf("%ld\n", static_cast(data_double)); + } else if (op == "int") { + // getInt, JSON_DOUBLE (json.cpp:878-883). + if (data_double > INT_MAX) + printf("%d\n", INT_MAX); + else if (data_double < INT_MIN) + printf("%d\n", INT_MIN); + else + printf("%d\n", static_cast(data_double)); + } else if (op == "ulong") { + // getUnsignedLong, JSON_DOUBLE (json.cpp:830-833). Note: no lower clamp - + // a negative double here is undefined/wraps; the parity test only feeds + // this op non-negative, in-range values. + if (data_double > static_cast(ULONG_MAX)) + printf("%lu\n", ULONG_MAX); + else + printf("%lu\n", static_cast(static_cast(data_double))); + } else { + fprintf(stderr, "unknown op: %s\n", op.c_str()); + return 2; + } + return 0; +} diff --git a/tools/rust-pilot/forecast_reference.cpp b/tools/rust-pilot/forecast_reference.cpp new file mode 100644 index 0000000000..78ed434f3c --- /dev/null +++ b/tools/rust-pilot/forecast_reference.cpp @@ -0,0 +1,756 @@ +// Parity authority for the Rust forecast pilot (Engine track E4). Standalone +// (no libfrepple): replicates the numeric cores of the frePPLe forecast methods +// VERBATIM from src/forecast/timeseries.cpp, with each `new ProblemOutlier(...)` +// write replaced by recording the outlier index. The Rust ports are diffed +// against this -> true Rust-vs-C++ parity. +// +// NOTE: keep in sync with src/forecast/timeseries.cpp if those methods change. +// Build: g++ -O2 -o forecast_reference forecast_reference.cpp +// Usage (history on stdin): +// forecast_reference moving_average +// forecast_reference single_exp +#include +#include +#include +#include +#include +#include +#include + +static const int MAXBUCKETS = 500; +static const double ROUNDING_ERROR = 0.000001; // include/frepple/utils.h:64 +static const double ACCURACY = 0.01; // timeseries.cpp:30 +static double weight[MAXBUCKETS]; + +// forecast.h:3051-3054 +static inline double smapeWeight(long idx) { + if (idx < 0) idx = 0; + if (idx >= MAXBUCKETS) idx = MAXBUCKETS - 1; + return weight[idx]; +} + +static void init_weights(double alfa) { + weight[0] = 1.0; + for (int i = 0; i < MAXBUCKETS - 1; ++i) weight[i + 1] = weight[i] * alfa; +} + +static std::vector read_history() { + std::vector ts; + double v; + while (std::cin >> v) ts.push_back(v); + return ts; +} + +static void emit(double smape, double stddev, double forecast, + const std::vector& outliers) { + printf("{\"smape\":%.17g,\"standarddeviation\":%.17g,\"forecast\":%.17g,\"outliers\":[", + smape, stddev, forecast); + for (size_t k = 0; k < outliers.size(); ++k) + printf("%s%ld", k ? "," : "", outliers[k]); + printf("]}\n"); +} + +// ---- MovingAverage (timeseries.cpp:294-384) ---- +static int moving_average(int argc, char** argv) { + if (argc < 6) return 2; + unsigned int order = static_cast(atol(argv[2])); + if (order < 1) order = 1; + const double Forecast_maxDeviation = atof(argv[3]); + const double Forecast_SmapeAlfa = atof(argv[4]); + const unsigned long skip = static_cast(atol(argv[5])); + init_weights(Forecast_SmapeAlfa); + + std::vector timeseries = read_history(); + const unsigned int count = static_cast(timeseries.size()); + timeseries.push_back(0.0); + + std::vector clean_history(count + 1, 0.0); + std::vector outliers; + double error_smape = 0.0, error_smape_weights = 0.0; + double standarddeviation = 0.0, maxdeviation = 0.0, avg = 0.0; + for (short pass = 0; pass <= 1; ++pass) { + if (pass) clean_history[0] = timeseries[0]; + error_smape = 0.0; + error_smape_weights = 0.0; + for (unsigned int i = 1; i <= count; ++i) { + double actual = timeseries[i]; + if (pass == 0) { + double sum = 0.0; + for (unsigned int j = 0; j < order && j < i; ++j) + sum += timeseries[i - j - 1]; + avg = sum / order; + if (i == count) break; + standarddeviation += (avg - actual) * (avg - actual); + if (fabs(avg - actual) > maxdeviation) maxdeviation = fabs(avg - actual); + } else { + double sum = 0.0; + for (unsigned int j = 0; j < order && j < i; ++j) + sum += clean_history[i - j - 1]; + avg = sum / order; + if (i == count) break; + if (actual > avg + Forecast_maxDeviation * standarddeviation) { + clean_history[i] = avg + Forecast_maxDeviation * standarddeviation; + outliers.push_back(i); + } else if (actual < avg - Forecast_maxDeviation * standarddeviation) { + clean_history[i] = avg - Forecast_maxDeviation * standarddeviation; + outliers.push_back(i); + } else + clean_history[i] = actual; + } + if (i >= skip && i < count && fabs(avg + actual) > ROUNDING_ERROR) { + error_smape += fabs(avg - actual) / fabs(avg + actual) * smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); + } + } + if (pass == 0) { + if (count > 1) { + standarddeviation = sqrt(standarddeviation / (count - 1)); + maxdeviation /= standarddeviation; + if (maxdeviation < Forecast_maxDeviation) break; + } else { + standarddeviation = sqrt(standarddeviation); + maxdeviation = 0.0; + break; + } + } + } + if (error_smape_weights) error_smape /= error_smape_weights; + emit(error_smape, standarddeviation, avg, outliers); + return 0; +} + +// ---- SingleExponential (timeseries.cpp:420-593) ---- +static int single_exp(int argc, char** argv) { + if (argc < 9) return 2; + double alfa = atof(argv[2]); + const double min_alfa = atof(argv[3]); + const double max_alfa = atof(argv[4]); + const double Forecast_maxDeviation = atof(argv[5]); + const double Forecast_SmapeAlfa = atof(argv[6]); + const unsigned long skip = static_cast(atol(argv[7])); + const unsigned long iters = static_cast(atol(argv[8])); + if (alfa < min_alfa) alfa = min_alfa; + init_weights(Forecast_SmapeAlfa); + + std::vector timeseries = read_history(); + const unsigned int count = static_cast(timeseries.size()); + timeseries.push_back(0.0); + + if (count < skip + 5) { + emit(DBL_MAX, DBL_MAX, 0.0, {}); + return 0; + } + + std::vector outliers; + double error = 0.0, error_smape = 0.0, error_smape_weights = 0.0, best_smape = 0.0; + double delta, df_dalfa_i, sum_11, sum_12; + double best_error = DBL_MAX, best_f_i = 0.0, best_standarddeviation = 0.0; + double f_i = 0.0; + bool upperboundarytested = false, lowerboundarytested = false; + unsigned long iteration = 1; + for (; iteration <= iters; ++iteration) { + double standarddeviation = 0.0, maxdeviation = 0.0; + for (short outl = 0; outl <= 1; ++outl) { + df_dalfa_i = sum_11 = sum_12 = error_smape = error_smape_weights = error = 0.0; + double history_0 = timeseries[0], history_1 = timeseries[1], + history_2 = timeseries[2]; + f_i = (history_0 + history_1 + history_2) / 3; + if (outl == 1) { + double t = 0.0; + double hs[3] = {history_0, history_1, history_2}; + for (int k = 0; k < 3; ++k) { + if (hs[k] > f_i + Forecast_maxDeviation * standarddeviation) + t += f_i + Forecast_maxDeviation * standarddeviation; + else if (hs[k] < f_i - Forecast_maxDeviation * standarddeviation) + t += f_i - Forecast_maxDeviation * standarddeviation; + else + t += hs[k]; + } + f_i = t / 3; + } + double history_i = history_0; + for (unsigned long i = 1; i <= count; ++i) { + double history_i_min_1 = history_i; + history_i = timeseries[i]; + df_dalfa_i = history_i_min_1 - f_i + (1 - alfa) * df_dalfa_i; + f_i = history_i_min_1 * alfa + (1 - alfa) * f_i; + if (i == count) break; + if (outl == 0) { + standarddeviation += (f_i - history_i) * (f_i - history_i); + if (fabs(f_i - history_i) > maxdeviation) + maxdeviation = fabs(f_i - history_i); + } else { + if (history_i > f_i + Forecast_maxDeviation * standarddeviation) { + history_i = f_i + Forecast_maxDeviation * standarddeviation; + if (iteration == 1) outliers.push_back(i); + } else if (history_i < f_i - Forecast_maxDeviation * standarddeviation) { + history_i = f_i - Forecast_maxDeviation * standarddeviation; + if (iteration == 1) outliers.push_back(i); + } + } + sum_12 += df_dalfa_i * (history_i - f_i) * smapeWeight(count - i); + sum_11 += df_dalfa_i * df_dalfa_i * smapeWeight(count - i); + if (i >= skip) { + error += (f_i - history_i) * (f_i - history_i) * smapeWeight(count - i); + if (fabs(f_i + history_i) > ROUNDING_ERROR) { + error_smape += fabs(f_i - history_i) / (f_i + history_i) * smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); + } + } + } + if (outl == 0) { + standarddeviation = sqrt(standarddeviation / (count - 1)); + maxdeviation /= standarddeviation; + if (maxdeviation < Forecast_maxDeviation) break; + } + } + if (error < best_error) { + best_error = error; + best_smape = error_smape_weights ? error_smape / error_smape_weights : 0.0; + best_f_i = f_i; + best_standarddeviation = standarddeviation; + } + if (fabs(sum_11 + error / iteration) > ROUNDING_ERROR) sum_11 += error / iteration; + if (fabs(sum_11) < ROUNDING_ERROR) break; + delta = sum_12 / sum_11; + if (fabs(delta) < ACCURACY && iteration > 3) break; + alfa += delta; + if (alfa > max_alfa) { + alfa = max_alfa; + if (upperboundarytested) break; + upperboundarytested = true; + } else if (alfa < min_alfa) { + alfa = min_alfa; + if (lowerboundarytested) break; + lowerboundarytested = true; + } + } + emit(best_smape, best_standarddeviation, best_f_i, outliers); + return 0; +} + +// ---- DoubleExponential (timeseries.cpp:633-892) ---- +static int double_exp(int argc, char** argv) { + if (argc < 12) return 2; + double alfa = atof(argv[2]); + const double min_alfa = atof(argv[3]); + const double max_alfa = atof(argv[4]); + double gamma = atof(argv[5]); + const double min_gamma = atof(argv[6]); + const double max_gamma = atof(argv[7]); + const double Forecast_maxDeviation = atof(argv[8]); + const double Forecast_SmapeAlfa = atof(argv[9]); + const unsigned long skip = static_cast(atol(argv[10])); + const unsigned long iters = static_cast(atol(argv[11])); + init_weights(Forecast_SmapeAlfa); + + std::vector timeseries = read_history(); + const unsigned int count = static_cast(timeseries.size()); + timeseries.push_back(0.0); + if (count < skip + 5) { + emit(DBL_MAX, DBL_MAX, 0.0, {}); + return 0; + } + + std::vector outliers; + double error = 0.0, error_smape = 0.0, error_smape_weights = 0.0, delta_alfa, + delta_gamma, determinant; + double constant_i_prev, trend_i_prev, d_constant_d_gamma_prev, + d_constant_d_alfa_prev, d_constant_d_alfa, d_constant_d_gamma, + d_trend_d_alfa, d_trend_d_gamma, d_forecast_d_alfa, d_forecast_d_gamma, + sum11, sum12, sum22, sum13, sum23; + double best_error = DBL_MAX, best_smape = 0, best_constant_i = 0.0, + best_trend_i = 0.0, best_standarddeviation = 0.0; + double constant_i = 0.0, trend_i = 0.0; + unsigned int iteration = 1, boundarytested = 0; + for (; iteration <= iters; ++iteration) { + double standarddeviation = 0.0, maxdeviation = 0.0; + for (short outl = 0; outl <= 1; ++outl) { + error = error_smape = error_smape_weights = sum11 = sum12 = sum22 = sum13 = + sum23 = 0.0; + d_constant_d_alfa = d_constant_d_gamma = d_trend_d_alfa = d_trend_d_gamma = + 0.0; + d_forecast_d_alfa = d_forecast_d_gamma = 0.0; + double history_0 = timeseries[0], history_1 = timeseries[1], + history_2 = timeseries[2], history_3 = timeseries[3]; + constant_i = (history_0 + history_1 + history_2) / 3; + trend_i = (history_3 - history_0) / 3; + if (outl == 1) { + double t1 = 0.0; + if (history_0 > constant_i + Forecast_maxDeviation * standarddeviation) + t1 = constant_i + Forecast_maxDeviation * standarddeviation; + else if (history_0 < constant_i - Forecast_maxDeviation * standarddeviation) + t1 = constant_i - Forecast_maxDeviation * standarddeviation; + else + t1 = history_0; + double t2 = -t1; + if (history_1 > constant_i + trend_i + Forecast_maxDeviation * standarddeviation) + t1 += constant_i + trend_i + Forecast_maxDeviation * standarddeviation; + else if (history_1 < constant_i + trend_i - Forecast_maxDeviation * standarddeviation) + t1 += constant_i + trend_i - Forecast_maxDeviation * standarddeviation; + else + t1 += history_1; + if (history_2 > constant_i + 2 * trend_i + Forecast_maxDeviation * standarddeviation) { + t1 += constant_i + 2 * trend_i + Forecast_maxDeviation * standarddeviation; + t2 += constant_i + 2 * trend_i + Forecast_maxDeviation * standarddeviation; + } else if (history_2 < constant_i + 2 * trend_i - Forecast_maxDeviation * standarddeviation) { + t1 += constant_i + 2 * trend_i - Forecast_maxDeviation * standarddeviation; + t2 += constant_i + 2 * trend_i - Forecast_maxDeviation * standarddeviation; + } else { + t1 += history_2; + t2 += history_2; + } + constant_i = t1 / 3; + trend_i = t2 / 3; + } + double history_i = history_0; + for (unsigned long i = 1; i <= count; ++i) { + double history_i_min_1 = history_i; + history_i = timeseries[i]; + constant_i_prev = constant_i; + trend_i_prev = trend_i; + constant_i = history_i_min_1 * alfa + (1 - alfa) * (constant_i_prev + trend_i_prev); + trend_i = gamma * (constant_i - constant_i_prev) + (1 - gamma) * trend_i_prev; + if (i == count) break; + if (outl == 0) { + standarddeviation += (constant_i + trend_i - history_i) * (constant_i + trend_i - history_i); + if (fabs(constant_i + trend_i - history_i) > maxdeviation) + maxdeviation = fabs(constant_i + trend_i - history_i); + } else { + if (history_i > constant_i + trend_i + Forecast_maxDeviation * standarddeviation) { + history_i = constant_i + trend_i + Forecast_maxDeviation * standarddeviation; + if (iteration == 1) outliers.push_back(i); + } else if (history_i < constant_i + trend_i - Forecast_maxDeviation * standarddeviation) { + history_i = constant_i + trend_i - Forecast_maxDeviation * standarddeviation; + if (iteration == 1) outliers.push_back(i); + } + } + d_constant_d_gamma_prev = d_constant_d_gamma; + d_constant_d_alfa_prev = d_constant_d_alfa; + d_constant_d_alfa = history_i_min_1 - constant_i_prev - trend_i_prev + (1 - alfa) * d_forecast_d_alfa; + d_constant_d_gamma = (1 - alfa) * d_forecast_d_gamma; + d_trend_d_alfa = gamma * (d_constant_d_alfa - d_constant_d_alfa_prev) + (1 - gamma) * d_trend_d_alfa; + d_trend_d_gamma = constant_i - constant_i_prev - trend_i_prev + + gamma * (d_constant_d_gamma - d_constant_d_gamma_prev) + + (1 - gamma) * d_trend_d_gamma; + d_forecast_d_alfa = d_constant_d_alfa + d_trend_d_alfa; + d_forecast_d_gamma = d_constant_d_gamma + d_trend_d_gamma; + sum11 += smapeWeight(count - i) * d_forecast_d_alfa * d_forecast_d_alfa; + sum12 += smapeWeight(count - i) * d_forecast_d_alfa * d_forecast_d_gamma; + sum22 += smapeWeight(count - i) * d_forecast_d_gamma * d_forecast_d_gamma; + sum13 += smapeWeight(count - i) * d_forecast_d_alfa * (history_i - constant_i - trend_i); + sum23 += smapeWeight(count - i) * d_forecast_d_gamma * (history_i - constant_i - trend_i); + if (i >= skip) { + error += (constant_i + trend_i - history_i) * (constant_i + trend_i - history_i) * smapeWeight(count - i); + if (fabs(constant_i + trend_i + history_i) > ROUNDING_ERROR) { + error_smape += fabs(constant_i + trend_i - history_i) / + fabs(constant_i + trend_i + history_i) * smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); + } + } + } + if (outl == 0) { + standarddeviation = sqrt(standarddeviation / (count - 1)); + maxdeviation /= standarddeviation; + if (maxdeviation < Forecast_maxDeviation) break; + } + } + if (error < best_error) { + best_error = error; + best_smape = error_smape_weights ? error_smape / error_smape_weights : 0.0; + best_constant_i = constant_i; + best_trend_i = trend_i; + best_standarddeviation = standarddeviation; + } + sum11 += error / iteration; + sum22 += error / iteration; + determinant = sum11 * sum22 - sum12 * sum12; + if (fabs(determinant) < ROUNDING_ERROR) { + sum11 -= error / iteration; + sum22 -= error / iteration; + determinant = sum11 * sum22 - sum12 * sum12; + if (fabs(determinant) < ROUNDING_ERROR) break; + } + delta_alfa = (sum13 * sum22 - sum23 * sum12) / determinant; + delta_gamma = (sum23 * sum11 - sum13 * sum12) / determinant; + if (fabs(delta_alfa) + fabs(delta_gamma) < 2 * ACCURACY && iteration > 3) break; + alfa += delta_alfa; + gamma += delta_gamma; + if (alfa > max_alfa) + alfa = max_alfa; + else if (alfa < min_alfa) + alfa = min_alfa; + if (gamma > max_gamma) + gamma = max_gamma; + else if (gamma < min_gamma) + gamma = min_gamma; + if ((gamma == min_gamma || gamma == max_gamma) && (alfa == min_alfa || alfa == max_alfa)) { + if (boundarytested++ > 5) break; + } + } + emit(best_smape, best_standarddeviation, best_constant_i + best_trend_i, outliers); + return 0; +} + +// ---- Croston (timeseries.cpp:1307-1463) ---- +static int croston(int argc, char** argv) { + if (argc < 9) return 2; + const double min_alfa = atof(argv[2]); + const double max_alfa = atof(argv[3]); + const double decay_rate = atof(argv[4]); + const double Forecast_maxDeviation = atof(argv[5]); + const double Forecast_SmapeAlfa = atof(argv[6]); + const unsigned long skip = static_cast(atol(argv[7])); + const unsigned long niter = static_cast(atol(argv[8])); + init_weights(Forecast_SmapeAlfa); + + std::vector timeseries = read_history(); + const unsigned int count = static_cast(timeseries.size()); + timeseries.push_back(0.0); + + double nonzero = 0.0, totalsum = 0.0; + unsigned long lastnonzero = 0; + for (unsigned long i = 0; i < count; ++i) { + if (timeseries[i]) { + ++nonzero; + totalsum += timeseries[i]; + lastnonzero = i; + } + } + double periods_between_demands = count / nonzero; + if (!nonzero) { + emit(0, 0, 0, {}); + return 0; + } + + std::vector outliers; + unsigned int iteration = 0; + double error_smape = 0.0, error_smape_weights = 0.0, best_smape = 0.0; + double q_i, p_i, f_i = 0.0; + double best_error = DBL_MAX, best_f_i = 0.0, best_standarddeviation = 0.0; + unsigned int between_demands = 1; + double alfa = min_alfa; + double delta = (niter > 1) ? (max_alfa - min_alfa) / (niter - 1) : 0.0; + for (; iteration < niter; ++iteration) { + double standarddeviation = 0.0, maxdeviation = 0.0; + for (short outl = 0; outl <= 1; ++outl) { + error_smape = error_smape_weights = 0.0; + q_i = totalsum / nonzero; + p_i = count / nonzero; + f_i = (1 - alfa / 2) * q_i / p_i; + double history_i = timeseries[0]; + for (unsigned long i = 1; i <= count; ++i) { + double history_i_min_1 = history_i; + history_i = timeseries[i]; + if (history_i_min_1) { + q_i = alfa * history_i_min_1 + (1 - alfa) * q_i; + p_i = alfa * between_demands + (1 - alfa) * p_i; + f_i = (1 - alfa / 2) * q_i / p_i; + between_demands = 1; + } else if (i > lastnonzero && between_demands > 2 * periods_between_demands) { + f_i = f_i * (1 - decay_rate); + p_i = (1 - alfa / 2) * q_i / f_i; + } else + ++between_demands; + if (i == count) break; + if (outl == 0) { + standarddeviation += (f_i - history_i) * (f_i - history_i); + if (fabs(history_i - f_i) > maxdeviation) maxdeviation = fabs(f_i - history_i); + } else { + if (history_i > f_i + Forecast_maxDeviation * standarddeviation) { + history_i = f_i + Forecast_maxDeviation * standarddeviation; + if (iteration == 1) outliers.push_back(i); + } + } + if (i >= skip && p_i > 0) { + if (fabs(f_i + history_i) > ROUNDING_ERROR) { + error_smape += fabs(f_i - history_i) / fabs(f_i + history_i) * smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); + } + } + } + if (outl == 0) { + standarddeviation = (count > 1) ? sqrt(standarddeviation / (count - 1)) : 0.0; + if (standarddeviation > ROUNDING_ERROR) maxdeviation /= standarddeviation; + if (maxdeviation < Forecast_maxDeviation) break; + } + } + if (error_smape <= best_error) { + best_error = error_smape; + best_smape = error_smape_weights ? error_smape / error_smape_weights : 0.0; + best_f_i = f_i; + best_standarddeviation = standarddeviation; + } + if (delta) + alfa += delta; + else + break; + } + emit(best_smape, best_standarddeviation, best_f_i, outliers); + return 0; +} + +// ---- Seasonal (timeseries.cpp:942-1262) ---- +static void detect_cycle(const std::vector& ts, unsigned int count, + unsigned int min_period, unsigned int max_period, + double min_autocorrelation, unsigned short& period, + double& autocorrelation) { + period = 0; + autocorrelation = min_autocorrelation; + if (count < min_period * 2) return; + double average = 0.0; + for (unsigned int i = 0; i < count; ++i) average += ts[i]; + average /= count; + double variance = 0.0; + for (unsigned int i = 0; i < count; ++i) + variance += (ts[i] - average) * (ts[i] - average); + variance /= count; + unsigned short best_period = 0; + double best_autocorrelation = min_autocorrelation; + double correlations[7] = {10, 10, 10, 10, 10, 10, 10}; + for (auto p = min_period; p <= max_period && p < count / 2; ++p) { + for (short i = 6; i > 0; --i) correlations[i] = correlations[i - 1]; + correlations[0] = 0.0; + for (unsigned int i = p; i < count; ++i) + correlations[0] += (ts[i - p] - average) * (ts[i] - average); + correlations[0] /= count - p; + correlations[0] /= variance; + if (p > min_period + 1 && correlations[1] > correlations[2] * 1.1 && + correlations[1] > correlations[0] * 1.1 && + correlations[1] > best_autocorrelation) { + best_autocorrelation = correlations[1]; + best_period = p - 1; + } + if (p > min_period + 4 && correlations[2] > best_autocorrelation && + correlations[2] > (correlations[0] + correlations[1]) / 2 && + correlations[2] > (correlations[3] + correlations[4]) / 2) { + best_autocorrelation = correlations[2]; + best_period = p - 2; + } + if (p > min_period + 6 && correlations[3] > best_autocorrelation && + correlations[3] > (correlations[0] + correlations[1] + correlations[2]) / 3 && + correlations[3] > (correlations[4] + correlations[5] + correlations[6]) / 3) { + best_autocorrelation = correlations[3]; + best_period = p - 3; + } + } + autocorrelation = best_autocorrelation; + period = best_period; +} + +static int seasonal(int argc, char** argv) { + if (argc < 16) return 2; + double alfa = atof(argv[2]); + const double min_alfa = atof(argv[3]); + const double max_alfa = atof(argv[4]); + double beta = atof(argv[5]); + const double min_beta = atof(argv[6]); + const double max_beta = atof(argv[7]); + const double gamma = atof(argv[8]); + const unsigned int min_period = static_cast(atol(argv[9])); + const unsigned int max_period = static_cast(atol(argv[10])); + const double min_autocorrelation = atof(argv[11]); + const double max_autocorrelation = atof(argv[12]); + const double Forecast_SmapeAlfa = atof(argv[13]); + const unsigned long skip = static_cast(atol(argv[14])); + const unsigned long iters = static_cast(atol(argv[15])); + init_weights(Forecast_SmapeAlfa); + + std::vector timeseries = read_history(); + const unsigned int count = static_cast(timeseries.size()); + timeseries.push_back(0.0); + + unsigned short period; + double autocorrelation; + detect_cycle(timeseries, count, min_period, max_period, min_autocorrelation, + period, autocorrelation); + if (!period) { + printf("{\"smape\":%.17g,\"standarddeviation\":%.17g,\"forecast\":0," + "\"period\":0,\"force\":false,\"s_i\":[]," + "\"l_i\":0,\"t_i\":0,\"cycleindex\":0}\n", + DBL_MAX, DBL_MAX); + return 0; + } + + double error = 0.0, error_smape = 0.0, error_smape_weights = 0.0, determinant, + delta_alfa, delta_beta; + double forecast_i, d_forecast_d_alfa, d_forecast_d_beta; + double d_L_d_alfa, d_L_d_beta, d_T_d_alfa, d_T_d_beta; + double d_S_d_alfa[80], d_S_d_beta[80]; + double d_L_d_alfa_prev, d_L_d_beta_prev, d_T_d_alfa_prev, d_T_d_beta_prev, + d_S_d_alfa_prev, d_S_d_beta_prev; + double sum11, sum12, sum13, sum22, sum23; + double best_error = DBL_MAX, best_smape = 0, best_standarddeviation = 0.0; + double initial_S_i[80], best_S_i[80], S_i[80]; + double L_i, T_i; + + double L_i_initial = 0.0, T_i_initial = 0.0; + for (unsigned short i = 0; i < period; ++i) { + L_i_initial += timeseries[i]; + T_i_initial += timeseries[i + period] - timeseries[i]; + initial_S_i[i] = 0.0; + } + T_i_initial /= period; + L_i_initial = L_i_initial / period; + double best_L_i = L_i_initial, best_T_i = T_i_initial; + unsigned short cyclecount = 0; + for (unsigned int i = 0; i + period <= count; i += period) { + ++cyclecount; + double cyclesum = 0.0; + for (unsigned short j = 0; j < period; ++j) cyclesum += timeseries[i + j]; + if (cyclesum) + for (unsigned short j = 0; j < period; ++j) + initial_S_i[j] += timeseries[i + j] / cyclesum * period; + } + for (unsigned long i = 0; i < period; ++i) initial_S_i[i] /= cyclecount; + + double L_i_prev, cyclesum, standarddeviation = 0.0; + unsigned int iteration = 1, boundarytested = 0; + for (; iteration <= iters; ++iteration) { + error = error_smape = error_smape_weights = sum11 = sum12 = sum13 = sum22 = + sum23 = standarddeviation = 0.0; + d_L_d_alfa = d_L_d_beta = d_T_d_alfa = d_T_d_beta = 0.0; + L_i = L_i_initial; + T_i = T_i_initial; + cyclesum = 0.0; + for (unsigned short i = 0; i < period; ++i) { + S_i[i] = initial_S_i[i]; + d_S_d_alfa[i] = 0.0; + d_S_d_beta[i] = 0.0; + if (i) cyclesum += timeseries[i - 1]; + } + unsigned int prevcycleindex = period - 1, cycleindex = 0; + for (unsigned int i = period; i <= count; ++i) { + L_i_prev = L_i; + double actual = (i == count) ? 0 : timeseries[i]; + cyclesum += timeseries[i - 1]; + if (i > period) cyclesum -= timeseries[i - period - 1]; + L_i = alfa * cyclesum / period + (1 - alfa) * (L_i + T_i); + if (L_i < 0) L_i = 0.0; + T_i = beta * (L_i - L_i_prev) + (1 - beta) * T_i; + double factor = -S_i[prevcycleindex]; + if (L_i) + S_i[prevcycleindex] = + gamma * timeseries[i - 1] / L_i + (1 - gamma) * S_i[prevcycleindex]; + if (S_i[prevcycleindex] < 0.0) S_i[prevcycleindex] = 0.0; + factor = period / (period + factor + S_i[prevcycleindex]); + for (unsigned short i2 = 0; i2 < period; ++i2) S_i[i2] *= factor; + if (i == count) break; + d_L_d_alfa_prev = d_L_d_alfa; + d_L_d_beta_prev = d_L_d_beta; + d_T_d_alfa_prev = d_T_d_alfa; + d_T_d_beta_prev = d_T_d_beta; + d_S_d_alfa_prev = d_S_d_alfa[prevcycleindex]; + d_S_d_beta_prev = d_S_d_beta[prevcycleindex]; + d_L_d_alfa = cyclesum / period - (L_i + T_i) + + (1 - alfa) * (d_L_d_alfa_prev + d_T_d_alfa_prev); + d_L_d_beta = (1 - alfa) * (d_L_d_beta_prev + d_T_d_beta_prev); + if (L_i > ROUNDING_ERROR) { + d_S_d_alfa[prevcycleindex] = + -gamma * timeseries[i - 1] / L_i / L_i * d_L_d_alfa_prev + + (1 - gamma) * d_S_d_alfa_prev; + d_S_d_beta[prevcycleindex] = + -gamma * timeseries[i - 1] / L_i / L_i * d_L_d_beta_prev + + (1 - gamma) * d_S_d_beta_prev; + } else { + d_S_d_alfa[prevcycleindex] = (1 - gamma) * d_S_d_alfa_prev; + d_S_d_beta[prevcycleindex] = (1 - gamma) * d_S_d_beta_prev; + } + d_T_d_alfa = beta * (d_L_d_alfa - d_L_d_alfa_prev) + (1 - beta) * d_T_d_alfa_prev; + d_T_d_beta = (L_i - L_i_prev) + beta * (d_L_d_beta - d_L_d_beta_prev) - T_i + + (1 - beta) * d_T_d_beta_prev; + d_forecast_d_alfa = (d_L_d_alfa + d_T_d_alfa) * S_i[cycleindex] + + (L_i + T_i) * d_S_d_alfa[cycleindex]; + d_forecast_d_beta = (d_L_d_beta + d_T_d_beta) * S_i[cycleindex] + + (L_i + T_i) * d_S_d_beta[cycleindex]; + forecast_i = (L_i + T_i) * S_i[cycleindex]; + sum11 += smapeWeight(count - i) * d_forecast_d_alfa * d_forecast_d_alfa; + sum12 += smapeWeight(count - i) * d_forecast_d_alfa * d_forecast_d_beta; + sum22 += smapeWeight(count - i) * d_forecast_d_beta * d_forecast_d_beta; + sum13 += smapeWeight(count - i) * d_forecast_d_alfa * (actual - forecast_i); + sum23 += smapeWeight(count - i) * d_forecast_d_beta * (actual - forecast_i); + if (i >= skip) { + double fcst = (L_i + T_i) * S_i[cycleindex]; + error += (fcst - actual) * (fcst - actual) * smapeWeight(count - i); + if (fabs(fcst + actual) > ROUNDING_ERROR) { + error_smape += fabs(fcst - actual) / fabs(fcst + actual) * smapeWeight(count - i); + error_smape_weights += smapeWeight(count - i); + standarddeviation += (fcst - actual) * (fcst - actual); + } + } + if (++cycleindex >= period) cycleindex = 0; + if (++prevcycleindex >= period) prevcycleindex = 0; + } + if (error < best_error) { + best_error = error; + best_smape = error_smape_weights ? error_smape / error_smape_weights : 0.0; + best_L_i = L_i; + best_T_i = T_i; + best_standarddeviation = sqrt(standarddeviation / (count - period - 1)); + for (unsigned short i = 0; i < period; ++i) best_S_i[i] = S_i[i]; + } + sum11 += error / iteration; + sum22 += error / iteration; + determinant = sum11 * sum22 - sum12 * sum12; + if (fabs(determinant) < ROUNDING_ERROR) { + sum11 -= error / iteration; + sum22 -= error / iteration; + determinant = sum11 * sum22 - sum12 * sum12; + if (fabs(determinant) < ROUNDING_ERROR) break; + } + delta_alfa = (sum13 * sum22 - sum23 * sum12) / determinant; + delta_beta = (sum23 * sum11 - sum13 * sum12) / determinant; + if ((fabs(delta_alfa) + fabs(delta_beta)) < 3 * ACCURACY && iteration > 3) break; + alfa += delta_alfa; + beta += delta_beta; + if (alfa > max_alfa) + alfa = max_alfa; + else if (alfa < min_alfa) + alfa = min_alfa; + if (beta > max_beta) + beta = max_beta; + else if (beta < min_beta) + beta = min_beta; + if ((beta == min_beta || beta == max_beta) && (alfa == min_alfa || alfa == max_alfa)) { + if (boundarytested++ > 5) break; + } + } + if (period > skip) { + best_smape *= (count - skip); + best_smape /= (count - period); + } + L_i = best_L_i; + T_i = best_T_i; + for (unsigned short i = 0; i < period; ++i) S_i[i] = best_S_i[i]; + double forecast = (L_i + T_i / period) * S_i[count % period]; + + printf("{\"smape\":%.17g,\"standarddeviation\":%.17g,\"forecast\":%.17g," + "\"period\":%u,\"force\":%s,\"s_i\":[", + best_smape, best_standarddeviation, forecast, period, + (autocorrelation > max_autocorrelation) ? "true" : "false"); + for (unsigned short i = 0; i < period; ++i) + printf("%s%.17g", i ? "," : "", best_S_i[i]); + // phase-7 apply-state: the level/trend + cycle position applyForecast resumes + // from (cycleindex = count%period, the slot the first forecast bucket uses). + printf("],\"l_i\":%.17g,\"t_i\":%.17g,\"cycleindex\":%u}\n", L_i, T_i, + (unsigned int)(count % period)); + return 0; +} + +int main(int argc, char** argv) { + if (argc < 2) { + fprintf(stderr, + "usage: %s " + "\n", + argv[0]); + return 2; + } + const std::string method = argv[1]; + if (method == "moving_average") return moving_average(argc, argv); + if (method == "single_exp") return single_exp(argc, argv); + if (method == "double_exp") return double_exp(argc, argv); + if (method == "croston") return croston(argc, argv); + if (method == "seasonal") return seasonal(argc, argv); + fprintf(stderr, "unknown method: %s\n", method.c_str()); + return 2; +} diff --git a/tools/rust-pilot/frepple_forecast.h b/tools/rust-pilot/frepple_forecast.h new file mode 100644 index 0000000000..8b7197f486 --- /dev/null +++ b/tools/rust-pilot/frepple_forecast.h @@ -0,0 +1,55 @@ +/* C ABI for the Rust forecast methods (Engine track E4, phase 7). Links against + * libfrepple_forecast.a (cargo `staticlib`). Scalars are returned via out-pointers; + * variable-length outputs go into a caller buffer up to `*_cap`, with the true + * length via `*_len`. All functions return 0 on success. + * + * Params are passed as a small f64 array `p` (length `np`), in this order: + * moving_average: [order, max_deviation, smape_alfa, skip] + * single_exponential: [init_alfa, min_alfa, max_alfa, max_deviation, smape_alfa, skip, iters] + * double_exponential: [init_alfa, min_alfa, max_alfa, init_gamma, min_gamma, max_gamma, + * max_deviation, smape_alfa, skip, iters] + * croston: [min_alfa, max_alfa, decay_rate, max_deviation, smape_alfa, skip, iters] + * seasonal: [init_alfa, min_alfa, max_alfa, init_beta, min_beta, max_beta, gamma, + * min_period, max_period, min_autocorr, max_autocorr, smape_alfa, skip, iters] + */ +#ifndef FREPPLE_FORECAST_H +#define FREPPLE_FORECAST_H +#include +#include +#ifdef __cplusplus +extern "C" { +#endif + +#define FREPPLE_FORECAST_SCALAR_SIG \ + const double *history, size_t count, double *out_smape, double *out_stddev, \ + double *out_forecast, size_t *out_outliers, size_t out_cap, \ + size_t *out_len, const double *p, size_t np + +int frepple_moving_average(FREPPLE_FORECAST_SCALAR_SIG); +int frepple_single_exponential(FREPPLE_FORECAST_SCALAR_SIG); +int frepple_croston(FREPPLE_FORECAST_SCALAR_SIG); + +/* DoubleExp also returns the level/trend state applyForecast extrapolates from, + * via two extra trailing out-pointers (constant, trend). */ +int frepple_double_exponential(FREPPLE_FORECAST_SCALAR_SIG, double *out_constant, + double *out_trend); + +/* Seasonal (Holt-Winters). Unlike the scalar methods it has NO outlier indices, + * but DOES return the level/trend/cycle apply-state (out_l_i, out_t_i, + * out_cycleindex) the engine extrapolates from per bucket + * (`L_i += T_i; fcst = L_i * S_i[cycleindex]`, cycleindex wrapping at period). + * `p` order: [init_alfa, min_alfa, max_alfa, init_beta, min_beta, max_beta, + * gamma, min_period, max_period, min_autocorr, max_autocorr, smape_alfa, skip, + * iters]. `out_s_i` takes up to `s_i_cap` factors; `out_s_i_len` is the true + * count (== period). cycleindex = count % period. */ +int frepple_seasonal(const double *history, size_t count, const double *p, + size_t np, double *out_smape, double *out_stddev, + double *out_forecast, uint32_t *out_period, + int32_t *out_force, double *out_s_i, size_t s_i_cap, + size_t *out_s_i_len, double *out_l_i, double *out_t_i, + uint32_t *out_cycleindex); + +#ifdef __cplusplus +} +#endif +#endif