diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json index 2d4de938..2e6d9fc4 100644 --- a/.devcontainer/devcontainer.json +++ b/.devcontainer/devcontainer.json @@ -4,7 +4,6 @@ "remoteUser": "frappe", "shutdownAction": "stopCompose", "postCreateCommand": "bash /workspace/.devcontainer/scripts/init.sh", - "dockerComposeFile": "./docker-compose.yml", "service": "frappe", "workspaceFolder": "/workspace/frappe-bench" diff --git a/.github/helper/install.sh b/.github/helper/install.sh index ec90d40d..35455d21 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -5,7 +5,7 @@ export PIP_ROOT_USER_ACTION=ignore set -e # Check for merge conflicts before proceeding -python -m compileall -f "${GITHUB_WORKSPACE}" +# python -m compileall -f "${GITHUB_WORKSPACE}" if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" then echo "Found merge conflicts" exit 1 @@ -26,8 +26,7 @@ mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_si mysql --host 127.0.0.1 --port 3306 -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'root'" # match site_cofig mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES" -echo BRANCH_NAME: "${BRANCH_NAME}" -git clone https://github.com/frappe/frappe --branch ${BRANCH_NAME} +git clone https://github.com/frappe/frappe --branch "version-15" bench init frappe-bench --frappe-path ~/frappe --python "$(which python)" --skip-assets --ignore-exist mkdir ~/frappe-bench/sites/test_site @@ -40,7 +39,7 @@ sed -i 's/schedule:/# schedule:/g' Procfile sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile -bench get-app erpnext https://github.com/frappe/erpnext --branch ${BRANCH_NAME} --resolve-deps --skip-assets +bench get-app erpnext https://github.com/frappe/erpnext --branch "version-15" --resolve-deps --skip-assets bench get-app beam "${GITHUB_WORKSPACE}" --skip-assets printf '%s\n' 'frappe' 'erpnext' 'beam' > ~/frappe-bench/sites/apps.txt @@ -51,6 +50,9 @@ bench start &> bench_run_logs.txt & CI=Yes & bench --site test_site reinstall --yes --admin-password admin +bench --site test_site migrate +bench --site test_site build + bench setup requirements --dev echo "BENCH VERSION NUMBERS:" @@ -58,6 +60,4 @@ bench version echo "SITE LIST-APPS:" bench list-apps -bench start &> bench_run_logs.txt & -CI=Yes & bench execute 'beam.tests.setup.before_test' diff --git a/.github/validate_customizations.py b/.github/validate_customizations.py deleted file mode 100644 index 89cbf720..00000000 --- a/.github/validate_customizations.py +++ /dev/null @@ -1,169 +0,0 @@ -import json -import pathlib -import sys - - -def scrub(txt: str) -> str: - return txt.replace(" ", "_").replace("-", "_").lower() - - -def unscrub(txt: str) -> str: - return txt.replace("_", " ").replace("-", " ").title() - - -def get_customized_doctypes(): - apps_dir = pathlib.Path(__file__).resolve().parent.parent.parent - apps_order = pathlib.Path(__file__).resolve().parent.parent.parent.parent / "sites" / "apps.txt" - apps_order = apps_order.read_text().split("\n") - customized_doctypes = {} - for _app_dir in apps_order: - app_dir = (apps_dir / _app_dir).resolve() - if not app_dir.is_dir(): - continue - modules = (app_dir / _app_dir / "modules.txt").read_text().split("\n") - for module in modules: - if not (app_dir / _app_dir / scrub(module) / "custom").exists(): - continue - for custom_file in list((app_dir / _app_dir / scrub(module) / "custom").glob("**/*.json")): - if custom_file.stem in customized_doctypes: - customized_doctypes[custom_file.stem].append(custom_file.resolve()) - else: - customized_doctypes[custom_file.stem] = [custom_file.resolve()] - - return dict(sorted(customized_doctypes.items())) - - -def validate_module(customized_doctypes, set_module=False): - exceptions = [] - app_dir = pathlib.Path(__file__).resolve().parent.parent - this_app = app_dir.stem - modules = (app_dir / this_app / "modules.txt").read_text().split("\n") - for doctype, customize_files in customized_doctypes.items(): - for customize_file in customize_files: - if ( - not this_app == customize_file.parent.parent.parent.parent.stem - ): # Updated to accommodate local folders named same as app - continue - module = customize_file.parent.parent.stem - file_contents = json.loads(customize_file.read_text()) - if file_contents.get("custom_fields"): - for custom_field in file_contents.get("custom_fields"): - if set_module: - custom_field["module"] = unscrub(module) - continue - if not custom_field.get("module"): - exceptions.append( - f"Custom Field for {custom_field.get('dt')} in {this_app} '{custom_field.get('fieldname')}' does not have a module key" - ) - continue - elif custom_field.get("module") not in modules: - exceptions.append( - f"Custom Field for {custom_field.get('dt')} in {this_app} '{custom_field.get('fieldname')}' has module key ({custom_field.get('module')}) associated with another app" - ) - continue - if file_contents.get("property_setters"): - for ps in file_contents.get("property_setters"): - if set_module: - ps["module"] = unscrub(module) - continue - if not ps.get("module"): - exceptions.append( - f"Property Setter for {ps.get('doc_type')} in {this_app} '{ps.get('property')}' on {ps.get('field_name')} does not have a module key" - ) - continue - elif ps.get("module") not in modules: - exceptions.append( - f"Property Setter for {ps.get('doc_type')} in {this_app} '{ps.get('property')}' on {ps.get('field_name')} has module key ({ps.get('module')}) associated with another app" - ) - continue - if set_module: - with customize_file.open("w", encoding="UTF-8") as target: - json.dump(file_contents, target, sort_keys=True, indent=2) - - return exceptions - - -def validate_no_custom_perms(customized_doctypes): - exceptions = [] - this_app = pathlib.Path(__file__).resolve().parent.parent.stem - for doctype, customize_files in customized_doctypes.items(): - for customize_file in customize_files: - if ( - not this_app == customize_file.parent.parent.parent.parent.stem - ): # Updated to accommodate local folders named same as app - continue - file_contents = json.loads(customize_file.read_text()) - if file_contents.get("custom_perms"): - exceptions.append(f"Customization for {doctype} in {this_app} contains custom permissions") - return exceptions - - -def validate_duplicate_customizations(customized_doctypes): - exceptions = [] - common_fields = {} - common_property_setters = {} - app_dir = pathlib.Path(__file__).resolve().parent.parent - this_app = app_dir.stem - for doctype, customize_files in customized_doctypes.items(): - if len(customize_files) == 1: - continue - common_fields[doctype] = {} - common_property_setters[doctype] = {} - for customize_file in customize_files: - module = customize_file.parent.parent.stem - app = customize_file.parent.parent.parent.parent.stem - file_contents = json.loads(customize_file.read_text()) - if file_contents.get("custom_fields"): - fields = [cf.get("fieldname") for cf in file_contents.get("custom_fields")] - common_fields[doctype][module] = fields - if file_contents.get("property_setters"): - ps = [ps.get("name") for ps in file_contents.get("property_setters")] - common_property_setters[doctype][module] = ps - - for doctype, module_and_fields in common_fields.items(): - if this_app not in module_and_fields.keys(): - continue - this_modules_fields = module_and_fields.pop(this_app) - for module, fields in module_and_fields.items(): - for field in fields: - if field in this_modules_fields: - exceptions.append( - f"Custom Field for {unscrub(doctype)} in {this_app} '{field}' also appears in customizations for {module}" - ) - - for doctype, module_and_ps in common_property_setters.items(): - if this_app not in module_and_ps.keys(): - continue - this_modules_ps = module_and_ps.pop(this_app) - for module, ps in module_and_ps.items(): - for p in ps: - if p in this_modules_ps: - exceptions.append( - f"Property Setter for {unscrub(doctype)} in {this_app} on '{p}' also appears in customizations for {module}" - ) - - return exceptions - - -def validate_customizations(set_module): - customized_doctypes = get_customized_doctypes() - exceptions = validate_no_custom_perms(customized_doctypes) - exceptions += validate_module(customized_doctypes, set_module) - exceptions += validate_duplicate_customizations(customized_doctypes) - - return exceptions - - -if __name__ == "__main__": - exceptions = [] - set_module = False - for arg in sys.argv: - if arg == "--set-module": - set_module = True - exceptions.append(validate_customizations(set_module)) - - if exceptions: - for exception in exceptions: - [print(e) for e in exception] # TODO: colorize - - sys.exit(1) if all(exceptions) else sys.exit(0) diff --git a/.github/workflows/code-duplication.yml b/.github/workflows/code-duplication.yml new file mode 100644 index 00000000..d70489ee --- /dev/null +++ b/.github/workflows/code-duplication.yml @@ -0,0 +1,29 @@ +name: Code Duplication + +on: + push: + branches: ["*"] + pull_request: + branches: ["*"] + +permissions: + contents: read + pull-requests: write + issues: write + +jobs: + duplication: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-node@v4 + with: + node-version: '18' + + - uses: agritheory/test_utils/actions/code_duplication@main + with: + max_clones: 60 + max_percentage: 5.0 diff --git a/.github/workflows/generate-changelog.yml b/.github/workflows/generate-changelog.yml new file mode 100644 index 00000000..99870a15 --- /dev/null +++ b/.github/workflows/generate-changelog.yml @@ -0,0 +1,23 @@ +name: Generate Changelog + +on: + pull_request: + types: [opened, reopened, synchronize] + issue_comment: + types: [created] + +jobs: + generate-changelog: + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - uses: actions/checkout@v4 + + - name: Generate Changelog + uses: agritheory/test_utils/actions/generate_changelog@main + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + anthropic-api-key: ${{ secrets.ANTHROPIC_API_KEY }} diff --git a/.github/workflows/generate_matrix.yaml b/.github/workflows/generate_matrix.yaml index 0be2f06f..4024f566 100644 --- a/.github/workflows/generate_matrix.yaml +++ b/.github/workflows/generate_matrix.yaml @@ -1,25 +1,25 @@ -name: Generate Matrix - -on: - push: - branches: - - version-14 - - version-15 - pull_request: - -env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - -jobs: - generate: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - - name: Generate Scanning Decision Matrix - run: python3 ./beam/docs/generate_matrix.py +name: Generate Matrix + +on: + push: + branches: + - version-14 + - version-15 + pull_request: + +env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + +jobs: + generate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Generate Scanning Decision Matrix + run: python3 ./beam/docs/generate_matrix.py diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 5dc7dd62..af64cb82 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -3,11 +3,9 @@ name: Linters on: push: branches: - - version-15 - version-14 - pull_request: - branches: - version-15 + pull_request: env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/overrides.yml b/.github/workflows/overrides.yml new file mode 100644 index 00000000..dd59d0ef --- /dev/null +++ b/.github/workflows/overrides.yml @@ -0,0 +1,20 @@ +name: Track Overrides + +on: + pull_request: + branches: + - version-14 + - version-15 + +jobs: + track_overrides: + runs-on: ubuntu-latest + name: Track Overrides + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Track Overrides + uses: agritheory/test_utils/actions/track_overrides@main + with: + app: beam diff --git a/.github/workflows/playwright.yaml b/.github/workflows/playwright.yaml new file mode 100644 index 00000000..4a58f43b --- /dev/null +++ b/.github/workflows/playwright.yaml @@ -0,0 +1,73 @@ +name: Tests + +on: + push: + branches: [ version-15 ] + pull_request: + +jobs: + test: + name: Mobile + runs-on: ubuntu-latest + + services: + mariadb: + image: mariadb:10.6 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: YES + MYSQL_ROOT_PASSWORD: 'admin' + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 + + steps: + - name: Clone + uses: actions/checkout@v4 + + - name: Setup Python + uses: actions/setup-python@v5 + with: + python-version: '3.10' + + - name: Setup Node + uses: actions/setup-node@v4 + with: + node-version: 20 + check-latest: true + cache: 'yarn' + + - name: Add to Hosts + run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install Poetry + uses: snok/install-poetry@v1 + + - name: Install JS Dependencies + run: yarn --prefer-offline + + - name: Install App Dependencies + run: bash ${{ github.workspace }}/.github/helper/install_dependencies.sh + + - name: Install Bench Site and Apps + env: + MYSQL_HOST: 'localhost' + MYSQL_PWD: 'admin' + run: bash ${{ github.workspace }}/.github/helper/install.sh + + - name: Run Mobile Tests + working-directory: /home/runner/frappe-bench + run: | + source env/bin/activate + cd apps/beam + poetry install + python -m playwright install --with-deps + pytest ./beam/tests/mobile --browser chromium --disable-warnings diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index 2df05b9d..782381c0 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -1,29 +1,14 @@ -name: Pytest CI +name: Tests on: push: - branches: - - version-14 - - version-15 + branches: [ version-15 ] pull_request: - branches: - - version-14 - - version-15 -env: - BRANCH_NAME: ${{ github.base_ref || github.ref_name }} - -# concurrency: -# group: develop-cloud_storage-${{ github.event.number }} -# cancel-in-progress: true jobs: - tests: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest] - fail-fast: false - name: Server + test: + name: Frappe + runs-on: ubuntu-latest services: mariadb: @@ -47,9 +32,9 @@ jobs: - name: Setup Node uses: actions/setup-node@v4 with: - node-version: 18 + node-version: 20 check-latest: true - cache: 'yarn' # Replaces `Get yarn cache directory path` and `yarn-cache` steps + cache: 'yarn' - name: Add to Hosts run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts @@ -58,7 +43,7 @@ jobs: uses: actions/cache@v4 with: path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} + key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml') }} restore-keys: | ${{ runner.os }}-pip- ${{ runner.os }}- @@ -76,20 +61,25 @@ jobs: env: MYSQL_HOST: 'localhost' MYSQL_PWD: 'admin' - BRANCH_NAME: ${{ env.BRANCH_NAME}} - run: | - bash ${{ github.workspace }}/.github/helper/install.sh + run: bash ${{ github.workspace }}/.github/helper/install.sh - - name: Run Tests + - name: Run Frappe Tests working-directory: /home/runner/frappe-bench run: | + set -o pipefail source env/bin/activate cd apps/beam poetry install - pytest --cov=beam --cov-report=xml --disable-warnings -s | tee pytest-coverage.txt + pytest --cov=beam --cov-report=xml ./beam --ignore=./beam/tests/mobile --disable-warnings -s --tracing=retain-on-failure | tee pytest-coverage.txt - name: Pytest coverage comment uses: MishaKav/pytest-coverage-comment@main with: pytest-coverage-path: /home/runner/frappe-bench/apps/beam/pytest-coverage.txt pytest-xml-coverage-path: /home/runner/frappe-bench/apps/beam/coverage.xml + + - uses: actions/upload-artifact@v4 + if: ${{ !cancelled() }} + with: + name: playwright-traces + path: test-results/ diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 7f93854a..2ca4fb9a 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -4,6 +4,7 @@ on: branches: - version-14 - version-15 + jobs: release: name: Release @@ -22,4 +23,4 @@ jobs: with: github_token: ${{ secrets.GITHUB_TOKEN }} git_committer_name: AgriTheory - git_committer_email: support@agritheory.dev \ No newline at end of file + git_committer_email: support@agritheory.dev diff --git a/.gitignore b/.gitignore index 99cc2796..c8f17c39 100644 --- a/.gitignore +++ b/.gitignore @@ -8,8 +8,8 @@ locale *.egg-info dist/ # build/ -cloud_storage/docs/current -cloud_storage/public/dist +beam/docs/current +beam/public/dist .vscode .vs node_modules @@ -44,6 +44,7 @@ wheels/ *.egg-info/ .installed.cfg *.egg +*.js.map MANIFEST # PyInstaller @@ -195,5 +196,24 @@ cypress/videos # JetBrains IDEs .idea/ +# built BEAM files beam/www/beam/index.js -beam/www/beam/style.css +beam/www/beam/index.js.map +beam/www/beam/index.css +beam/www/beam/index.*.js +beam/www/beam/index.*.js.map +beam/www/beam/index.*.css +beam/www/beam/assets +beam/www/beam/assets/* + +# unplugin-vue-components outputs +beam/www/beam/components.d.ts + +# unplugin-vue-router outputs +beam/www/beam/typed-router.d.ts + +# vite-plugin-pwa +sw.js +registerSW.js +manifest.webmanifest +workbox-* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index aa7cc233..32e6881e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,23 +4,39 @@ fail_fast: false repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v5.0.0 + rev: v6.0.0 hooks: - id: trailing-whitespace files: 'beam.*' exclude: '.*json$|.*txt$|.*csv|.*md|.*svg' - id: check-yaml - id: no-commit-to-branch - args: ['--branch', 'develop'] + args: ['--branch', 'version-15', '--branch', 'version-16'] - id: check-merge-conflict - id: check-ast - id: check-json - id: check-toml - - id: check-yaml - id: debug-statements + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + args: ["--ignore-words-list", "notin"] + exclude: 'yarn.lock|poetry.lock' + additional_dependencies: + - tomli + + - repo: local + hooks: + - id: no-titled-beam + name: "Use 'BEAM' not 'Beam'" + language: pygrep + entry: '\bBeam\b' + files: '\.md$' + - repo: https://github.com/asottile/pyupgrade - rev: v3.19.1 + rev: v3.20.0 hooks: - id: pyupgrade args: ['--py310-plus'] @@ -30,47 +46,43 @@ repos: hooks: - id: black - - repo: https://github.com/PyCQA/autoflake - rev: v2.3.1 - hooks: - - id: autoflake - args: [--remove-all-unused-imports, --in-place] - - - repo: https://github.com/PyCQA/isort - rev: 6.0.0 - hooks: - - id: isort - - repo: https://github.com/PyCQA/flake8 - rev: 7.1.1 + rev: 7.2.0 hooks: - id: flake8 additional_dependencies: ['flake8-bugbear'] - - repo: https://github.com/codespell-project/codespell - rev: v2.4.1 - hooks: - - id: codespell - additional_dependencies: - - tomli - - repo: https://github.com/agritheory/test_utils - rev: v0.17.0 + rev: v1.20.1 hooks: - id: update_pre_commit_config + - id: validate_frappe_project - id: validate_copyright files: '\.(js|ts|py|md)$' args: ['--app', 'beam'] + - id: bylines + exclude: 'README.md|CHANGELOG.md' - id: clean_customized_doctypes args: ['--app', 'beam'] - id: validate_customizations + - id: validate_patches + args: ['--app', 'beam'] + - id: track_overrides + args: ['--directory', '.', '--app', 'beam', '--base-branch', 'version-15'] + - id: check_code_duplication + args: ['--max-clones', '60', '--max-percentage', '5.0'] - - repo: local + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 hooks: - id: prettier - name: prettier - entry: npx prettier . --write --ignore-path .prettierignore - language: node + types_or: [javascript, vue, scss] + exclude: | + (?x)^( + .*node_modules.*| + beam/public/dist/.*| + beam/public/js/lib/.* + )$ ci: autoupdate_schedule: weekly diff --git a/.prettierrc.js b/.prettierrc.cjs similarity index 100% rename from .prettierrc.js rename to .prettierrc.cjs diff --git a/CHANGELOG.md b/CHANGELOG.md index 0ddb3c67..cc9e6965 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,6 @@ + + # CHANGELOG diff --git a/license.txt b/LICENSE similarity index 100% rename from license.txt rename to LICENSE diff --git a/README.md b/README.md index 1bb76b50..ac45bf9e 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ Set up a new bench, substitute a path to the python version to use, which should ``` # for linux development -bench init --frappe-branch version-15 {{ bench name }} --python ~/.pyenv/versions/3.10.13/bin/python3 +bench init --frappe-branch version-15 {{ bench name }} --python python3 ``` Create a new site in that bench ``` @@ -74,7 +74,10 @@ bench build Setup test data ```shell bench execute 'beam.tests.setup.before_test' -# for complete reset to run before tests: +``` + +For a complete database reset to re-run tests, run the following +```shell bench reinstall --yes --admin-password admin --mariadb-root-password admin && bench execute 'beam.tests.setup.before_test' ``` @@ -85,6 +88,28 @@ mypy ./apps/beam/beam --ignore-missing-imports pytest ./apps/beam/beam/tests -s --disable-warnings ``` +### Beam Portal setup + +
+Development + +```shell +# start the development server +yarn dev +``` +
+ +
+Production + +```shell +# build assets for the portal page(s) +bench build + +# visit `{server URL}/beam` to access the portal page. +``` +
+ ### Printer Server setup ```shell sudo apt-get install gcc cups python3-dev libcups2-dev -y @@ -94,9 +119,8 @@ sudo apt-get install gcc cups python3-dev libcups2-dev -y bench pip install pycups sudo usermod -a -G lpadmin {username} # the "frappe" user in most installations ``` -Go to `{server URL or localhost}:631` to access the CUPS web interface -Configuration on a remote server will take extra steps to secure: -https://askubuntu.com/questions/23936/how-do-you-administer-cups-remotely-using-the-web-interface + +Go to `{server URL or localhost}:631` to access the CUPS web interface. Configuration on a remote server will take [extra steps](https://askubuntu.com/questions/23936/how-do-you-administer-cups-remotely-using-the-web-interface) to secure. #### License diff --git a/beam/__init__.py b/beam/__init__.py index 1f0539c1..51b884c0 100644 --- a/beam/__init__.py +++ b/beam/__init__.py @@ -1,4 +1,4 @@ # Copyright (c) 2025, AgriTheory and contributors # For license information, please see license.txt -__version__ = "15.4.0" +__version__ = "15.8.0" diff --git a/beam/beam/barcodes.py b/beam/beam/barcodes.py index 3404ef31..3944a439 100644 --- a/beam/beam/barcodes.py +++ b/beam/beam/barcodes.py @@ -6,6 +6,7 @@ from io import BytesIO import frappe +import pyqrcode from barcode import Code128 from barcode.writer import ImageWriter from erpnext import get_default_company @@ -27,8 +28,19 @@ def create_beam_barcode(doc, method=None): ): # TODO: refactor this to be configurable to "Products" or "sold" items that do not require handling units return + company = get_default_company() + if frappe.db.exists("BEAM Settings", {"company": company}): + settings = frappe.get_cached_doc("BEAM Settings", {"company": company}) + try: + allowed = frappe.parse_json(settings.auto_barcode_doctypes or '["Item", "Warehouse"]') + except Exception: + allowed = ["Item", "Warehouse"] + if doc.doctype not in allowed: + return if any([b for b in doc.barcodes if b.barcode_type == "Code128"]): return + if doc.doctype == "User" and (doc.name == "Guest" or doc.name == "Administrator"): + return # move all other rows back for row_index, b in enumerate(doc.barcodes, start=1): b.idx = row_index + 1 @@ -53,10 +65,42 @@ def barcode128(barcode_text: str) -> str: ) font_size = settings.barcode_font_size or 0 temp = BytesIO() - instance = Code128(barcode_text, writer=ImageWriter()) - instance.write( - options={"module_width": 0.4, "module_height": 10, "font_size": font_size, "compress": True}, + + barcode_instance = Code128(barcode_text, writer=ImageWriter()) + options = {"module_width": 0.4, "module_height": 10, "font_size": font_size, "compress": True} + + barcode_instance.write(temp, options) + encoded = base64.b64encode(temp.getvalue()).decode("ascii") + return f'' + + +@frappe.whitelist() +@frappe.read_only() +def get_qr_code(qr_text: str) -> str: + if not qr_text: + return "" + + company = get_default_company() + settings = ( + create_beam_settings(company) + if not frappe.db.exists("BEAM Settings", {"company": company}) + else frappe.get_doc("BEAM Settings", {"company": company}) + ) + + qr_scale = getattr(settings, "qr_scale", 8) # Module size in pixels + qr_border = getattr(settings, "qr_border", 4) # Border size in modules + qr_error_correct = getattr(settings, "qr_error_correct", "M") # Error correction level + + qr = pyqrcode.create(qr_text, error=qr_error_correct) + temp = BytesIO() + qr.png( + temp, + scale=int(qr_scale), + module_color=(0, 0, 0, 255), + background=(255, 255, 255, 255), + quiet_zone=int(qr_border), ) + temp.seek(0) encoded = base64.b64encode(temp.getvalue()).decode("ascii") return f'' @@ -129,7 +173,12 @@ def add_to_label(label: Label, element: Printable): class ZPLLabelStringOutput(Label): def __init__( - self, width: int = 100, length: int = 100, dpi: int = 203, print_speed: int = 2, copies: int = 1 + self, + width: int = 100, + length: int = 100, + dpi: int = 203, + print_speed: int = 2, + copies: int = 1, ): super().__init__(width, length, dpi, print_speed, copies) diff --git a/beam/beam/boot.py b/beam/beam/boot.py index 494ed8c1..968cd6da 100644 --- a/beam/beam/boot.py +++ b/beam/beam/boot.py @@ -1,9 +1,36 @@ # Copyright (c) 2025, AgriTheory and contributors # For license information, please see license.txt +import frappe from beam.beam.scan.config import get_scan_doctypes def boot_session(bootinfo): bootinfo.beam = get_scan_doctypes() + bootinfo.beam["settings"] = get_beam_settings() + bootinfo.beam["default_hu_print_format"] = frappe.get_meta("Handling Unit").get( + "default_print_format" + ) + + +def get_beam_settings(): + """Get BEAM Settings for all companies, keyed by company name.""" + settings = {} + beam_settings = frappe.get_all( + "BEAM Settings", + fields=["company", "enable_handling_units"], + ) + for setting in beam_settings: + settings[setting.company] = { + "enable_handling_units": setting.enable_handling_units, + } + return settings + + +def redirect_to_beam(): + user_roles = frappe.get_all( + "Has Role", fields=["role"], filters={"parent": frappe.session.user}, pluck="role" + ) + if "BEAM Mobile User" in user_roles: + frappe.local.response["home_page"] = "/beam#/" diff --git a/beam/beam/custom/bom_scrap_item.json b/beam/beam/custom/bom_scrap_item.json index 95230daf..4037dd3e 100644 --- a/beam/beam/custom/bom_scrap_item.json +++ b/beam/beam/custom/bom_scrap_item.json @@ -6,9 +6,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "creation": "2023-08-22 15:23:16.272692", "default": null, - "docstatus": 0, "dt": "BOM Scrap Item", "fetch_if_empty": 0, "fieldname": "create_handling_unit", @@ -29,15 +27,11 @@ "is_virtual": 0, "label": "Create Handling Unit", "length": 0, - "modified": "2023-08-22 15:23:52.267428", - "modified_by": "Administrator", "module": "BEAM", "name": "BOM Scrap Item-create_handling_unit", "no_copy": 0, "non_negative": 0, - "owner": "Administrator", "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -49,9 +43,7 @@ "unique": 0 } ], - "custom_perms": [], "doctype": "BOM Scrap Item", - "links": [], "property_setters": [], "sync_on_migrate": 1 } diff --git a/beam/beam/custom/item.json b/beam/beam/custom/item.json index ee256d60..a342fc85 100644 --- a/beam/beam/custom/item.json +++ b/beam/beam/custom/item.json @@ -6,9 +6,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "creation": "2024-02-26 23:52:53.051024", - "default": "1", - "docstatus": 0, + "default": "0", "dt": "Item", "fetch_if_empty": 0, "fieldname": "enable_handling_unit", @@ -29,15 +27,11 @@ "is_virtual": 0, "label": "Enable Handling Unit", "length": 0, - "modified": "2024-02-26 23:52:53.051024", - "modified_by": "Administrator", "module": "BEAM", "name": "Item-enable_handling_unit", "no_copy": 0, "non_negative": 0, - "owner": "Administrator", "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -49,9 +43,7 @@ "unique": 0 } ], - "custom_perms": [], "doctype": "Item", - "links": [], "property_setters": [], "sync_on_migrate": 1 } diff --git a/beam/beam/custom/item_barcode.json b/beam/beam/custom/item_barcode.json index 2e6bb577..55b70641 100644 --- a/beam/beam/custom/item_barcode.json +++ b/beam/beam/custom/item_barcode.json @@ -1,20 +1,14 @@ { "custom_fields": [], - "custom_perms": [], "doctype": "Item Barcode", "property_setters": [ { - "creation": "2022-06-16 09:40:22.875922", "doc_type": "Item Barcode", - "docstatus": 0, "doctype_or_field": "DocField", "field_name": "barcode_type", "idx": 0, - "modified": "2022-06-16 09:40:22.875922", - "modified_by": "Administrator", "module": "BEAM", "name": "Item Barcode-barcode_type-options", - "owner": "Administrator", "property": "options", "property_type": "Text", "value": "\nEAN\nUPC-A\nCode128" diff --git a/beam/beam/custom/network_printer_settings.json b/beam/beam/custom/network_printer_settings.json new file mode 100644 index 00000000..aa230540 --- /dev/null +++ b/beam/beam/custom/network_printer_settings.json @@ -0,0 +1,98 @@ +{ + "custom_fields": [ + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "dt": "Network Printer Settings", + "fetch_if_empty": 0, + "fieldname": "printer_type", + "fieldtype": "Select", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "printer_name", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Printer Type", + "length": 0, + "module": "BEAM", + "name": "Network Printer Settings-printer_type", + "no_copy": 0, + "options": "\nGeneral Purpose\nLabel / RAW", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "dt": "Network Printer Settings", + "fetch_if_empty": 0, + "fieldname": "printer_location", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "printer_type", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Printer Location", + "length": 0, + "module": "BEAM", + "name": "Network Printer Settings-printer_location", + "no_copy": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "sort_options": 0, + "translatable": 0, + "unique": 0 + } + ], + "property_setters": [ + { + "doc_type": "Network Printer Settings", + "doctype_or_field": "DocField", + "field_name": "printer_name", + "idx": 0, + "module": "BEAM", + "name": "Network Printer Settings-printer_name-fieldtype", + "property": "fieldtype", + "property_type": "Data", + "value": "Autocomplete" + } + ], + "doctype": "Network Printer Settings", + "sync_on_migrate": 1 +} diff --git a/beam/beam/custom/stock_entry_detail.json b/beam/beam/custom/stock_entry_detail.json index 6fc6385b..1728d497 100644 --- a/beam/beam/custom/stock_entry_detail.json +++ b/beam/beam/custom/stock_entry_detail.json @@ -6,10 +6,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "creation": "2023-09-13 12:51:04.950175", "default": null, - "depends_on": "", - "docstatus": 0, "dt": "Stock Entry Detail", "fetch_if_empty": 0, "fieldname": "recombine_on_cancel", @@ -30,15 +27,11 @@ "is_virtual": 0, "label": "Recombine On Cancel", "length": 0, - "modified": "2023-09-13 12:51:04.950175", - "modified_by": "Administrator", "module": "BEAM", "name": "Stock Entry Detail-recombine_on_cancel", "no_copy": 1, "non_negative": 0, - "owner": "Administrator", "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 1, @@ -50,9 +43,7 @@ "unique": 0 } ], - "custom_perms": [], "doctype": "Stock Entry Detail", - "links": [], "property_setters": [], "sync_on_migrate": 1 } diff --git a/beam/beam/custom/user.json b/beam/beam/custom/user.json new file mode 100644 index 00000000..c7efa884 --- /dev/null +++ b/beam/beam/custom/user.json @@ -0,0 +1,85 @@ +{ + "custom_fields": [ + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": null, + "dt": "User", + "fetch_if_empty": 0, + "fieldname": "barcode_section", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 26, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "mobile_no", + "label": "Barcodes", + "length": 0, + "module": "BEAM", + "name": "User-barcode_section", + "no_copy": 0, + "non_negative": 0, + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0 + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "default": null, + "dt": "User", + "fetch_if_empty": 0, + "fieldname": "barcodes", + "fieldtype": "Table", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 27, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "barcode_section", + "length": 0, + "module": "BEAM", + "name": "User-barcodes", + "no_copy": 0, + "non_negative": 0, + "options": "Item Barcode", + "permlevel": 0, + "print_hide": 0, + "print_hide_if_no_value": 0, + "read_only": 0, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0 + } + ], + "doctype": "User", + "property_setters": [], + "sync_on_migrate": 1 +} \ No newline at end of file diff --git a/beam/beam/custom/warehouse.json b/beam/beam/custom/warehouse.json index 8861a08d..79dafeae 100644 --- a/beam/beam/custom/warehouse.json +++ b/beam/beam/custom/warehouse.json @@ -6,9 +6,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "creation": "2022-06-16 09:48:36.521275", "default": null, - "docstatus": 0, "dt": "Warehouse", "fetch_if_empty": 0, "fieldname": "barcode_section", @@ -27,15 +25,11 @@ "insert_after": "pin", "label": "Barcodes", "length": 0, - "modified": "2022-06-16 09:48:36.521275", - "modified_by": "Administrator", "module": "BEAM", "name": "Warehouse-barcode_section", "no_copy": 0, "non_negative": 0, - "owner": "Administrator", "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -51,9 +45,7 @@ "bold": 0, "collapsible": 0, "columns": 0, - "creation": "2022-06-16 09:48:36.701251", "default": null, - "docstatus": 0, "dt": "Warehouse", "fetch_if_empty": 0, "fieldname": "barcodes", @@ -70,18 +62,13 @@ "in_preview": 0, "in_standard_filter": 0, "insert_after": "barcode_section", - "label": "", "length": 0, - "modified": "2022-06-16 09:48:36.701251", - "modified_by": "Administrator", "module": "BEAM", "name": "Warehouse-barcodes", "no_copy": 0, "non_negative": 0, "options": "Item Barcode", - "owner": "Administrator", "permlevel": 0, - "precision": "", "print_hide": 0, "print_hide_if_no_value": 0, "read_only": 0, @@ -92,7 +79,6 @@ "unique": 0 } ], - "custom_perms": [], "doctype": "Warehouse", "property_setters": [], "sync_on_migrate": 1 diff --git a/beam/beam/demand/demand.py b/beam/beam/demand/demand.py new file mode 100644 index 00000000..621a5433 --- /dev/null +++ b/beam/beam/demand/demand.py @@ -0,0 +1,862 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +from collections import deque +from typing import TYPE_CHECKING, Any, Optional, Union + +import frappe +from frappe.query_builder import Criterion, DocType, Field +from frappe.query_builder.custom import ConstantColumn +from frappe.utils.data import flt +from frappe.utils.nestedset import get_descendants_of +from pypika import Query, Table +from pypika import functions as fn +from pypika.terms import Order, ValueWrapper + +from beam.beam.demand.sqlite import get_demand_db, reset_demand_db +from beam.beam.demand.utils import ( + Allocation, + Demand, + get_datetime_from_epoch, + get_epoch_from_datetime, + validate_demand_enabled, +) + +if TYPE_CHECKING: + from sqlite3 import Cursor + + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import PurchaseInvoice + from erpnext.accounts.doctype.purchase_invoice_item.purchase_invoice_item import ( + PurchaseInvoiceItem, + ) + from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice + from erpnext.accounts.doctype.sales_invoice_item.sales_invoice_item import SalesInvoiceItem + from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder + from erpnext.selling.doctype.sales_order.sales_order import SalesOrder + from erpnext.stock.doctype.delivery_note.delivery_note import DeliveryNote + from erpnext.stock.doctype.delivery_note_item.delivery_note_item import DeliveryNoteItem + from erpnext.stock.doctype.purchase_receipt.purchase_receipt import PurchaseReceipt + from erpnext.stock.doctype.purchase_receipt_item.purchase_receipt_item import PurchaseReceiptItem + from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry + from erpnext.stock.doctype.stock_entry_detail.stock_entry_detail import StockEntryDetail + from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import StockReconciliation + from erpnext.stock.doctype.stock_reconciliation_item.stock_reconciliation_item import ( + StockReconciliationItem, + ) + + +def get_qty_from_sle(item_code: str, warehouse: str | None = None, company: str | None = None): + warehouses = [] + if warehouse: + warehouses = [warehouse] + + if not warehouse or frappe.get_cached_value("Warehouse", warehouse, "is_group"): + warehouses = get_demand_warehouses(company) + + balance_qty = frappe.get_all( + "Stock Ledger Entry", + filters={"item_code": item_code, "warehouse": ["in", warehouses], "is_cancelled": False}, + fields=["qty_after_transaction", "warehouse"], + order_by="posting_date desc, posting_time desc, creation DESC", + ) + + if not warehouse: + return balance_qty + + return flt(balance_qty[0].qty_after_transaction) if balance_qty else 0.0 + + +def get_manufacturing_demand( + name: str | None = None, item_code: str | None = None +) -> list[Demand]: + + WorkOrder = DocType("Work Order") + WorkOrderItem = DocType("Work Order Item") + WorkOrderOperation = DocType("Work Order Operation") + Item = DocType("Item") + + workstation_subquery = ( + frappe.qb.from_(WorkOrderOperation) + .select(WorkOrderOperation.workstation) + .where(WorkOrderOperation.parent == WorkOrder.name) + .orderby(WorkOrderOperation.idx) + .limit(1) + ) + + total_required_qty = Field("required_qty") - Field("transferred_qty") + + work_order_query = ( + frappe.qb.from_(WorkOrder) + .join(WorkOrderItem) + .on(WorkOrder.name == WorkOrderItem.parent) + .left_join(Item) + .on(Item.item_code == WorkOrderItem.item_code) + .select( + ConstantColumn("Work Order").as_("doctype"), + WorkOrder.name.as_("parent"), + WorkOrder.company, + WorkOrder.wip_warehouse.as_("warehouse"), + WorkOrder.production_item, + WorkOrder.bom_no, + (workstation_subquery.as_("workstation")), + WorkOrderItem.name.as_("name"), + WorkOrderItem.idx, + WorkOrderItem.item_code, + WorkOrderItem.source_warehouse.as_("item_warehouse"), + WorkOrder.planned_start_date.as_("delivery_date"), + (total_required_qty).as_("total_required_qty"), + Item.stock_uom, + WorkOrder.creation, + ) + .where( + (WorkOrder.docstatus == 1) + & (WorkOrder.status == "Not Started") + & (WorkOrderItem.required_qty > WorkOrderItem.transferred_qty) + ) + .orderby(WorkOrder.planned_start_date, WorkOrder.creation, WorkOrderItem.idx) + ) + + if name: + work_order_query = work_order_query.where(WorkOrder.name == name) + + if item_code: + work_order_query = work_order_query.where(WorkOrderItem.item_code == item_code) + + return work_order_query.run(as_dict=True) + + +def get_sales_demand(name: str | None = None, item_code: str | None = None) -> list[Demand]: + SalesOrder = DocType("Sales Order") + SalesOrderItem = DocType("Sales Order Item") + Item = DocType("Item") + BEAMSettings = DocType("BEAM Settings") + + default_fg_warehouse = frappe.db.get_single_value( + "Manufacturing Settings", "default_fg_warehouse" + ) + + total_required_qty = Field("stock_qty") - Field("delivered_qty") + + sales_order_query = ( + frappe.qb.from_(SalesOrder) + .join(SalesOrderItem) + .on(SalesOrder.name == SalesOrderItem.parent) + .left_join(Item) + .on(Item.item_code == SalesOrderItem.item_code) + .left_join(BEAMSettings) + .on(BEAMSettings.company == SalesOrder.company) + .select( + ConstantColumn("Sales Order").as_("doctype"), + SalesOrder.name.as_("parent"), + SalesOrder.company, + ConstantColumn(default_fg_warehouse).as_("warehouse"), + (BEAMSettings.shipping_workstation).as_("workstation"), + SalesOrderItem.name.as_("name"), + SalesOrderItem.idx, + SalesOrderItem.item_code, + SalesOrder.delivery_date, + (total_required_qty).as_("total_required_qty"), + Item.stock_uom, + fn.Coalesce(SalesOrder.customer, "").as_("customer"), + SalesOrder.creation, + ) + .where( + (SalesOrder.docstatus == 1) + & (SalesOrder.status != "Closed") + & (SalesOrderItem.stock_qty > SalesOrderItem.delivered_qty) + ) + .where( + Criterion.any( + [ + ( + (BEAMSettings.ignore_drop_shipped_items.isnull()) + | (BEAMSettings.ignore_drop_shipped_items == 0) + ), + ( + (BEAMSettings.ignore_drop_shipped_items.notnull()) + & (BEAMSettings.ignore_drop_shipped_items == 1) + & (SalesOrderItem.delivered_by_supplier != 1) + ), + ] + ) + ) + .orderby(SalesOrder.delivery_date, SalesOrder.creation, SalesOrderItem.idx) + ) + + if name: + sales_order_query = sales_order_query.where(SalesOrder.name == name) + + if item_code: + sales_order_query = sales_order_query.where(SalesOrderItem.item_code == item_code) + + return sales_order_query.run(as_dict=True) + + +@validate_demand_enabled +def build_demand_allocation_map() -> None: + reset_demand_db() + build_demand_map() + build_allocation_map() + + +def get_demand_list(name: str | None = None, item_code: str | None = None) -> list[Demand]: + if name: + with get_demand_db() as conn: + cursor = conn.cursor() + demand_table = Table("demand") + + if item_code: + demand_query = ( + Query.from_(demand_table) + .select("*") + .where((demand_table.parent == name) & (demand_table.item_code == item_code)) + ) + else: + demand_query = Query.from_(demand_table).select("*").where(demand_table.parent == name) + + demand_query = cursor.execute(demand_query.get_sql()) + + sales_demand: list[Demand] = demand_query.fetchall() + if sales_demand: + return sales_demand + + manufacturing_demand = get_manufacturing_demand(name, item_code) + sales_demand = get_sales_demand(name, item_code) + return manufacturing_demand + sales_demand + + +def build_demand_map( + name: str | None = None, item_code: str | None = None, cursor: Optional["Cursor"] = None +) -> None: + output: list[Demand] = [] + + for row in get_demand_list(name, item_code): + row.key = row.get("key") or frappe.generate_hash() + row.delivery_date = str(row.delivery_date or get_epoch_from_datetime(row.delivery_date)) + row.creation = str(row.creation or get_epoch_from_datetime(row.creation)) + row.total_required_qty = str(row.total_required_qty) + row.idx = str(row.idx) + output.append(row) + + if output: + if cursor: + insert_demand(output, cursor) + else: + with get_demand_db() as conn: + cursor = conn.cursor() + insert_demand(output, cursor) + + +def insert_demand(output: list[Demand], cursor: "Cursor") -> None: + demand_table = Table("demand") + for row in output: + demand_row = {key: value for key, value in row.items() if value} + insert_query = Query.into(demand_table).columns(*demand_row.keys()).insert(*demand_row.values()) + cursor.execute(insert_query.get_sql()) + + +@validate_demand_enabled +def modify_demand(doc: Union["SalesOrder", "WorkOrder"], method: str | None = None) -> None: + if method == "on_submit": + add_demand_allocation(doc.name) + elif method == "on_cancel": + remove_demand_allocation(doc.name) + + +def get_allocation_list(name: str) -> list[Allocation]: + with get_demand_db() as conn: + allocation_table = Table("allocation") + cursor = conn.cursor() + query = Query.from_(allocation_table).select("*").where(allocation_table.parent == name) + return cursor.execute(query.get_sql()).fetchall() + + +@validate_demand_enabled +def add_demand_allocation(name: str) -> None: + build_demand_map(name) + build_allocation_map() + + +@validate_demand_enabled +def remove_demand_allocation(name: str) -> None: + with get_demand_db() as conn: + allocation_table = Table("allocation") + demand_table = Table("demand") + cursor = conn.cursor() + + # remove all allocated row(s) + allocations = get_allocation_list(name) + for allocation in allocations: + delete_query = ( + Query.from_(allocation_table).delete().where(allocation_table.key == allocation.key) + ) + cursor.execute(delete_query.get_sql()) + + # remove all demand row(s) + demand = get_demand_list(name) + for row in demand: + delete_query = Query.from_(demand_table).delete().where(demand_table.key == row.key) + cursor.execute(delete_query.get_sql()) + + +@validate_demand_enabled +def build_allocation_map( + row: Union[ + "DeliveryNoteItem", + "PurchaseInvoiceItem", + "PurchaseReceiptItem", + "SalesInvoiceItem", + "StockEntryDetail", + "StockReconciliationItem", + None, + ] = None, + action: dict | None = None, +): + if row and action: + update_allocations(row=row, action=action) + else: + create_allocations() + + +def get_demand_query( + row: Union[ + "DeliveryNoteItem", + "PurchaseInvoiceItem", + "PurchaseReceiptItem", + "SalesInvoiceItem", + "StockEntryDetail", + "StockReconciliationItem", + None, + ] = None, +): + demand_table = Table("demand") + allocation_table = Table("allocation") + + query = ( + Query.from_(demand_table) + .select( + demand_table.star, + fn.Coalesce(fn.Sum(allocation_table.allocated_qty), 0).as_("allocated_qty"), + (demand_table.total_required_qty - fn.Coalesce(fn.Sum(allocation_table.allocated_qty), 0)).as_( + "net_required_qty" + ), + ) + .left_join(allocation_table) + .on(allocation_table.demand == demand_table.key) + ) + + if row: + query = query.where(demand_table.item_code == row.item_code) + + query = query.groupby( + demand_table.key, + demand_table.item_code, + demand_table.total_required_qty, + demand_table.delivery_date, + ).orderby(demand_table.delivery_date) + + with get_demand_db() as conn: + cursor = conn.cursor() + return cursor.execute(query.get_sql()) + + +def get_item_demand_map( + row: Union[ + "DeliveryNoteItem", + "PurchaseInvoiceItem", + "PurchaseReceiptItem", + "SalesInvoiceItem", + "StockEntryDetail", + "StockReconciliationItem", + None, + ] = None, +) -> dict[str, list[Allocation | Demand]]: + demand_query = get_demand_query(row=row) + demand_rows: list[Allocation | Demand] = demand_query.fetchall() + + item_demand_map = frappe._dict() + for demand_row in demand_rows: + if demand_row.item_code in item_demand_map: + item_demand_map[demand_row.item_code].append(demand_row) + else: + item_demand_map[demand_row.item_code] = [demand_row] + + return item_demand_map + + +def update_allocations( + row: Union[ + "DeliveryNoteItem", + "PurchaseInvoiceItem", + "PurchaseReceiptItem", + "SalesInvoiceItem", + "StockEntryDetail", + "StockReconciliationItem", + ], + action: dict, +): + demand_table = Table("demand") + allocation_table = Table("allocation") + + with get_demand_db() as conn: + cursor = conn.cursor() + + quantity_field = action.get("quantity_field") + row_qty = row.get(quantity_field) if quantity_field else 0 + + warehouse_field = action.get("warehouse_field") + warehouse = row.get(warehouse_field) + + allocation_query = ( + Query.from_(allocation_table) + .select("*") + .where( + (allocation_table.item_code == row.item_code) + & (allocation_table.warehouse == warehouse) + & (allocation_table.allocated_qty > 0) + ) + ) + allocation_query = cursor.execute(allocation_query.get_sql()) + + # TODO: remove demand row if demand is fully satisfied + + existing_allocations: list[Allocation] = allocation_query.fetchall() + if existing_allocations: + allocation_effect = action.get("allocation_effect") + demand_effect = action.get("demand_effect") + + for allocation in existing_allocations: + demand_query = ( + Query.from_(demand_table).select("*").where(demand_table.key == allocation.demand) + ) + demand_query = cursor.execute(demand_query.get_sql()) + demand_row: Demand = demand_query.fetchone() + + if demand_row: + # demand is still pending, add/reverse allocation; + # process demand before allocation + + new_total_required_qty = float(demand_row.total_required_qty) + if demand_effect: + if demand_effect == "increase": + new_total_required_qty = float(demand_row.total_required_qty) + row_qty + elif demand_effect == "decrease": + new_total_required_qty = max(0, float(demand_row.total_required_qty) - row_qty) + + if new_total_required_qty <= 0: + # if demand is fully met, delete the demand row + delete_query = Query.from_(demand_table).delete().where(demand_table.key == demand_row.key) + cursor.execute(delete_query.get_sql()) + else: + # if demand is partially met, update demand row + update_query = ( + Query.update(demand_table) + .set(demand_table.total_required_qty, new_total_required_qty) + .where(demand_table.key == demand_row.key) + ) + cursor.execute(update_query.get_sql()) + + if allocation_effect == "increase": + new_allocated_qty = min(new_total_required_qty, float(allocation.allocated_qty) + row_qty) + elif allocation_effect == "decrease": + new_allocated_qty = max(0, float(allocation.allocated_qty) - row_qty) + elif allocation_effect == "adjustment": + new_allocated_qty = min(new_total_required_qty, row_qty) + + if new_allocated_qty <= 0: + # if partial allocation is reverted, delete the allocation row + delete_query = ( + Query.from_(allocation_table).delete().where(allocation_table.key == allocation.key) + ) + cursor.execute(delete_query.get_sql()) + else: + # if demand can be partially or fully met, update allocation row + update_query = ( + Query.update(allocation_table) + .set(allocation_table.allocated_qty, new_allocated_qty) + .where(allocation_table.key == allocation.key) + ) + cursor.execute(update_query.get_sql()) + else: + # demand is already satisfied, reverse allocation + + if allocation_effect == "increase": + new_allocated_qty = float(allocation.allocated_qty) + row_qty + update_query = ( + Query.update(allocation_table) + .set(allocation_table.allocated_qty, new_allocated_qty) + .where(allocation_table.key == allocation.key) + ) + cursor.execute(update_query.get_sql()) + elif allocation_effect in ["increase", "adjustment"]: + # TODO: are these cases possible? + pass + + if demand_effect == "increase": + build_demand_map(row.parent, row.item_code, cursor) + elif demand_effect == "decrease": + # TODO: is this case possible? + pass + else: + item_demand_map = get_item_demand_map(row=row) + demand_rows = item_demand_map.get(row.item_code) + if not demand_rows: + return + demand_queue = deque(demand_rows) + + allocations: list[Allocation] = [] + while demand_queue: + current_demand = demand_queue[0] + net_required_qty = float(current_demand.net_required_qty) + + allocated_qty = min(row_qty, net_required_qty) + new_alloc = new_allocation(current_demand) + new_alloc.update({"warehouse": warehouse, "allocated_qty": str(allocated_qty)}) + allocations.append(new_alloc) + + if row_qty >= net_required_qty: + # Full demand can be met + demand_queue.popleft() + else: + # Partial demand is met + current_demand.total_required_qty = float(current_demand.total_required_qty) - allocated_qty + break + + for allocation in allocations: + insert_query = ( + Query.into(allocation_table).columns(*allocation.keys()).insert(*allocation.values()) + ) + cursor.execute(insert_query.get_sql()) + + +def create_allocations(): + with get_demand_db() as conn: + cursor = conn.cursor() + + item_demand_map = get_item_demand_map() + + allocations = [] + for item_code, demand_rows in item_demand_map.items(): + demand_queue = deque(demand_rows) + supply_queue = deque(get_qty_from_sle(item_code)) + if not any([supply_queue, demand_queue]): + continue + + while supply_queue and demand_queue: + current_demand = demand_queue[0] + current_supply = supply_queue[0] + + net_required_qty = current_demand["total_required_qty"] - current_demand["allocated_qty"] + allocated_qty = min(current_supply["qty_after_transaction"], net_required_qty) + + allocation = { + **new_allocation(current_demand), + "warehouse": current_supply.get("warehouse"), + "allocated_qty": str(allocated_qty), + } + + if current_supply["qty_after_transaction"] >= net_required_qty: + # Full demand can be met + current_supply["qty_after_transaction"] -= allocated_qty + demand_queue.popleft() + + if current_supply["qty_after_transaction"] == 0: + supply_queue.popleft() + break + else: + # Partial demand is met + current_demand["total_required_qty"] -= allocated_qty + supply_queue.popleft() + + allocations.append(allocation) + + for allocation in allocations: + allocation_table = Table("allocation") + insert_query = ( + Query.into(allocation_table).columns(*allocation.keys()).insert(*allocation.values()) + ) + cursor.execute(insert_query.get_sql()) + + +def new_allocation(demand_row) -> Allocation: + return frappe._dict( + { + "key": frappe.generate_hash(), + "demand": demand_row.key, + "doctype": demand_row.doctype, + "company": demand_row.company, + "parent": demand_row.parent, + "name": demand_row.name, + "idx": str(demand_row.idx), + "item_code": demand_row.item_code, + "production_item": demand_row.production_item, + "bom_no": demand_row.bom_no, + "item_warehouse": demand_row.item_warehouse, + "allocated_date": str(get_epoch_from_datetime()), + "modified": str(get_epoch_from_datetime()), + "stock_uom": demand_row.stock_uom, + "status": "Soft Allocated", + "assigned": demand_row.assigned or "", + "creation": str(demand_row.creation), + } + ) + + +@validate_demand_enabled +def modify_allocations( + doc: Union[ + "DeliveryNote", + "PurchaseInvoice", + "PurchaseReceipt", + "SalesInvoice", + "StockEntry", + "StockReconciliation", + ], + method: str, +): + demand_hooks = frappe.get_hooks("demand") + + doctype_matrix: dict[str, list[dict[str, Any]]] = demand_hooks.get(doc.doctype) + if not doctype_matrix: + return + + method_matrix = doctype_matrix.get(method) + if not method_matrix: + return + + demand_warehouses = get_demand_warehouses(doc.get("company")) + for row in doc.get("items"): + for action in method_matrix: + # implicit conditions: skip allocation for non-demand warehouses + warehouse_field = action.get("warehouse_field") + if warehouse_field: + warehouse = row.get(warehouse_field) + if warehouse not in demand_warehouses: + continue + + # explicit conditions + conditions = action.get("conditions") + if conditions: + for key, value in conditions.items(): + if doc.get(key) == value: + build_allocation_map(row=row, action=action) + else: + build_allocation_map(row=row, action=action) + + +def get_demand_warehouses(company: str | None = None) -> list[str]: + if not company: + company = frappe.defaults.get_defaults().get("company") + + root_warehouse = frappe.get_all( + "Warehouse", + {"company": company, "is_group": True, "parent_warehouse": ["is", "not set"]}, + pluck="name", + )[0] + + return get_descendant_warehouses(company, root_warehouse) + + +def get_descendant_warehouses(company: str | None, warehouse: str) -> list[str]: + if not company: + company = frappe.defaults.get_defaults().get("company") + + beam_settings = frappe.get_doc("BEAM Settings", company) + warehouse_types = [wt.warehouse_type for wt in beam_settings.warehouse_types] + if not warehouse_types: + return get_descendants_of("Warehouse", warehouse, ignore_permissions=True, order_by="lft") + + order_by = "lft" + limit = None + lft, rgt = frappe.get_cached_value("Warehouse", warehouse, ["lft", "rgt"]) + + if rgt - lft <= 1: + return [] + + return frappe.get_all( + "Warehouse", + filters={ + "lft": [">", lft], + "rgt": ["<", rgt], + "company": beam_settings.company, + "warehouse_type": ["not in", warehouse_types], + }, + pluck="name", + order_by=order_by, + limit_page_length=limit, + ) + + +@frappe.whitelist() +def get_demand(*args, **kwargs) -> list[Demand]: + records_per_page = 20 + try: + page = int(kwargs.get("page", 1)) + except ValueError: + page = 1 + + demand = Table("demand") + allocation = Table("allocation") + + d_filters = [] + a_filters = [] + + if kwargs.get("filters"): + filters = kwargs["filters"] + if isinstance(filters, str): + filters = frappe.parse_json(filters) + for key, value in filters.items(): + if isinstance(value, str): + value = (value,) + d_filters.append([getattr(demand, key).isin(value)]) + a_filters.append([getattr(allocation, key).isin(value)]) + elif isinstance(value, list): + operator, value = value + if operator == "in": + d_filters.append([getattr(demand, key).isin(value)]) + a_filters.append([getattr(allocation, key).isin(value)]) + elif operator == ">": + d_filters.append([getattr(demand, key).gt(value)]) + a_filters.append([getattr(allocation, key).gt(value)]) + elif operator == "<": + d_filters.append([getattr(demand, key).lt(value)]) + a_filters.append([getattr(allocation, key).lt(value)]) + elif operator == ">=": + d_filters.append([getattr(demand, key).gte(value)]) + a_filters.append([getattr(allocation, key).gte(value)]) + elif operator == "<=": + d_filters.append([getattr(demand, key).lte(value)]) + a_filters.append([getattr(allocation, key).lte(value)]) + + demand_query = ( + Query.from_(demand) + .select( + demand.key, + ValueWrapper("").as_("demand"), + demand.doctype, + demand.company, + demand.parent, + demand.warehouse, + demand.production_item, + demand.bom_no, + demand.name, + demand.idx, + demand.item_code, + demand.item_warehouse, + demand.delivery_date.as_("allocated_date"), + demand.delivery_date, + demand.modified, + demand.stock_uom, + fn.Coalesce( + ( + Query.from_(allocation) + .select(fn.Sum(allocation.allocated_qty)) + .where(allocation.demand == demand.key) + ), + 0, + ).as_("allocated_qty"), + ( + demand.total_required_qty + - fn.Coalesce( + ( + Query.from_(allocation) + .select(fn.Sum(allocation.allocated_qty)) + .where(allocation.demand == demand.key) + ), + 0, + ) + ).as_("net_required_qty"), + demand.total_required_qty, + ValueWrapper("").as_("status"), + demand.assigned, + demand.creation, + fn.Coalesce(demand.customer, "").as_("customer"), + ) + .where( + fn.Coalesce( + ( + Query.from_(allocation) + .select(fn.Sum(allocation.allocated_qty)) + .where(allocation.demand == demand.key) + ), + 0, + ) + <= 0 + ) + ) + + for d_filter in d_filters: + demand_query = demand_query.where(*d_filter) + + allocation_query = ( + Query.from_(allocation) + .select( + allocation.key, + allocation.demand, + allocation.doctype, + allocation.company, + allocation.parent, + allocation.warehouse, + allocation.production_item, + allocation.bom_no, + allocation.name, + allocation.idx, + allocation.item_code, + allocation.item_warehouse, + allocation.allocated_date, + allocation.allocated_date.as_("delivery_date"), + allocation.modified, + allocation.stock_uom, + allocation.allocated_qty, + ( + fn.Coalesce( + Query.from_(demand).select(demand.total_required_qty).where(allocation.demand == demand.key), + 0, + ) + - fn.Coalesce( + Query.from_(Table("allocation").as_("c")) + .select(fn.Sum(Table("allocation").as_("c").allocated_qty)) + .where(Table("allocation").as_("c").demand == allocation.demand), + 0, + ) + ).as_("net_required_qty"), + ( + Query.from_(demand).select(demand.total_required_qty).where(allocation.demand == demand.key) + ).as_("total_required_qty"), + allocation.status, + allocation.assigned, + allocation.creation, + ValueWrapper("").as_("customer"), + ) + .where(allocation.allocated_qty > 0) + .orderby( + allocation.delivery_date, + allocation.idx, + allocation.creation, + allocation.parent, + order=Order.asc, + ) + ) + + for a_filter in a_filters: + allocation_query = allocation_query.where(*a_filter) + + record_offset = records_per_page * (page - 1) + query = ( + f"{demand_query} UNION ALL {allocation_query} LIMIT {records_per_page} OFFSET {record_offset}" + ) + + with get_demand_db() as conn: + cursor = conn.cursor() + rows: list[Allocation | Demand] = cursor.execute(query).fetchall() + for row in rows: + row.update( + { + "net_required_qty": max(0.0, float(row.net_required_qty)), + "delivery_date": get_datetime_from_epoch(row.delivery_date), + "allocated_date": get_datetime_from_epoch(row.allocated_date), + "modified": get_datetime_from_epoch(row.modified), + "creation": get_datetime_from_epoch(row.creation), + } + ) + return rows diff --git a/beam/beam/demand/receiving.py b/beam/beam/demand/receiving.py new file mode 100644 index 00000000..466c35a4 --- /dev/null +++ b/beam/beam/demand/receiving.py @@ -0,0 +1,303 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +from typing import TYPE_CHECKING, Optional, Union + +import frappe +from frappe.query_builder import DocType +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Coalesce +from pypika import Query, Table +from pypika.terms import ValueWrapper + +from beam.beam.demand.sqlite import get_demand_db, reset_receiving_db +from beam.beam.demand.utils import ( + Receiving, + get_datetime_from_epoch, + get_epoch_from_datetime, + validate_demand_enabled, +) + +if TYPE_CHECKING: + from sqlite3 import Cursor + + from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import PurchaseInvoice + from erpnext.buying.doctype.purchase_order.purchase_order import PurchaseOrder + + +def _get_receiving_demand( + name: str | None = None, item_code: str | None = None +) -> list[Receiving]: + PurchaseOrder = DocType("Purchase Order") + PurchaseOrderItem = DocType("Purchase Order Item") + Item = DocType("Item") + BEAMSettings = DocType("BEAM Settings") + + # Purchase Order + receiving_workstation_subquery = ( + frappe.qb.from_(BEAMSettings) + .select(BEAMSettings.receiving_workstation) + .where(BEAMSettings.company == PurchaseOrder.company) + .limit(1) + ) + + purchase_order_query = ( + frappe.qb.from_(PurchaseOrder) + .join(PurchaseOrderItem) + .on(PurchaseOrder.name == PurchaseOrderItem.parent) + .left_join(Item) + .on(Item.item_code == PurchaseOrderItem.item_code) + .select( + ConstantColumn("Purchase Order").as_("doctype"), + PurchaseOrder.name.as_("parent"), + PurchaseOrder.company, + PurchaseOrderItem.warehouse, + (receiving_workstation_subquery.as_("workstation")), + PurchaseOrderItem.name.as_("name"), + PurchaseOrderItem.idx, + PurchaseOrderItem.item_code, + PurchaseOrder.schedule_date, + PurchaseOrderItem.stock_qty.as_("stock_qty"), + PurchaseOrderItem.received_qty, + PurchaseOrder.supplier, + Item.stock_uom, + PurchaseOrder.creation, + ) + .where( + (PurchaseOrder.docstatus == 1) + & (PurchaseOrder.status != "Closed") + & (Item.is_stock_item == 1) + & (PurchaseOrderItem.delivered_by_supplier != 1) + ) + .orderby(PurchaseOrder.schedule_date, PurchaseOrder.creation, PurchaseOrderItem.idx) + ) + + if name: + purchase_order_query = purchase_order_query.where(PurchaseOrder.name == name) + + if item_code: + purchase_order_query = purchase_order_query.where(PurchaseOrderItem.item_code == item_code) + + purchase_orders = purchase_order_query.run(as_dict=True) + + # Purchase Invoice + PurchaseInvoice = frappe.qb.DocType("Purchase Invoice") + PurchaseInvoiceItem = frappe.qb.DocType("Purchase Invoice Item") + + receiving_workstation_subquery = ( + frappe.qb.from_(BEAMSettings) + .select(BEAMSettings.receiving_workstation) + .where(BEAMSettings.company == PurchaseInvoice.company) + .limit(1) + ) + + unreceived_purchase_invoices_query = ( + frappe.qb.from_(PurchaseInvoice) + .join(PurchaseInvoiceItem) + .on(PurchaseInvoice.name == PurchaseInvoiceItem.parent) + .left_join(Item) + .on(Item.item_code == PurchaseInvoiceItem.item_code) + .select( + ConstantColumn("Purchase Invoice").as_("doctype"), + PurchaseInvoice.name.as_("parent"), + PurchaseInvoice.company, + PurchaseInvoiceItem.warehouse, + (receiving_workstation_subquery.as_("workstation")), + PurchaseInvoiceItem.name.as_("name"), + PurchaseInvoiceItem.idx, + PurchaseInvoiceItem.item_code, + PurchaseInvoice.due_date.as_("schedule_date"), + PurchaseInvoiceItem.stock_qty.as_("stock_qty"), + PurchaseInvoiceItem.received_qty, + PurchaseInvoice.supplier, + Item.stock_uom, + PurchaseInvoice.creation, + ) + .where( + (PurchaseInvoice.docstatus == 1) + & (Coalesce(PurchaseInvoiceItem.purchase_order, "") == "") + & (PurchaseInvoiceItem.received_qty < PurchaseInvoiceItem.stock_qty) + & (Item.is_stock_item == 1) + ) + .orderby(PurchaseInvoice.due_date, PurchaseInvoice.creation, PurchaseInvoiceItem.idx) + ) + + if name: + unreceived_purchase_invoices_query = unreceived_purchase_invoices_query.where( + PurchaseInvoice.name == name + ) + + if item_code: + unreceived_purchase_invoices_query = unreceived_purchase_invoices_query.where( + PurchaseInvoiceItem.item_code == item_code + ) + + unreceived_purchase_invoices = unreceived_purchase_invoices_query.run(as_dict=True) + + # Subcontracting Order + return purchase_orders + unreceived_purchase_invoices + + +@validate_demand_enabled +def modify_receiving( + doc: Union["PurchaseOrder", "PurchaseInvoice"], method: str | None = None +) -> None: + if method == "on_submit": + add_receiving(doc.name) + elif method == "on_cancel": + remove_receiving(doc.name) + + +def add_receiving(name: str) -> None: + build_receiving_map(name) + + +def remove_receiving(name: str) -> None: + with get_demand_db() as conn: + cursor = conn.cursor() + # remove all receiving row(s) + receiving = get_receiving_list(name) + receiving_table = Table("receiving") + for row in receiving: + delete_query = Query.from_(receiving_table).delete().where(receiving_table.key == row.key) + cursor.execute(delete_query.get_sql()) + + +def get_receiving_list(name: str | None = None, item_code: str | None = None) -> list[Receiving]: + if name: + with get_demand_db() as conn: + cursor = conn.cursor() + receiving_table = Table("receiving") + + if item_code: + receiving_query = ( + Query.from_(receiving_table) + .select("*") + .where((receiving_table.parent == name) & (receiving_table.item_code == item_code)) + ) + else: + receiving_query = ( + Query.from_(receiving_table).select("*").where(receiving_table.parent == name) + ) + + receiving_query = cursor.execute(receiving_query.get_sql()) + + receiving_demand: list[Receiving] = receiving_query.fetchall() + if receiving_demand: + return receiving_demand + + return _get_receiving_demand(name, item_code) + + +@validate_demand_enabled +def reset_build_receiving_map() -> None: + reset_receiving_db() + build_receiving_map() + + +@validate_demand_enabled +def build_receiving_map( + name: str | None = None, item_code: str | None = None, cursor: Optional["Cursor"] = None +) -> None: + output: list[Receiving] = [] + for row in get_receiving_list(name, item_code): + row.key = row.get("key") or frappe.generate_hash() + row.schedule_date = str(row.schedule_date or get_epoch_from_datetime(row.schedule_date)) + row.creation = str(row.creation or get_epoch_from_datetime(row.creation)) + row.stock_qty = str(row.stock_qty) + row.received_qty = str(row.received_qty) + row.idx = str(row.idx) + output.append(row) + + if output: + if cursor: + insert_receiving(output, cursor) + else: + with get_demand_db() as conn: + cursor = conn.cursor() + insert_receiving(output, cursor) + + +def insert_receiving(output: list[Receiving], cursor: "Cursor") -> None: + receiving_table = Table("receiving") + for row in output: + receiving_row = {key: value for key, value in row.items() if value} + insert_query = ( + Query.into(receiving_table).columns(*receiving_row.keys()).insert(*receiving_row.values()) + ) + cursor.execute(insert_query.get_sql()) + + +@frappe.whitelist() +def get_receiving_demand(*args, **kwargs) -> list[Receiving]: + records_per_page = 20 + page = int(kwargs.get("page", 1)) + order_by = kwargs.get("order_by", "workstation, assigned") + + receiving = Table("receiving") + + r_filters = [] + + if kwargs.get("filters"): + filters = kwargs["filters"] + if isinstance(filters, str): + filters = frappe.parse_json(filters) + for key, value in filters.items(): + if isinstance(value, str): + value = (value,) + r_filters.append([getattr(receiving, key).isin(value)]) + elif isinstance(value, list): + operator, value = value + if operator == "in": + r_filters.append([getattr(receiving, key).isin(value)]) + elif operator == ">": + r_filters.append([getattr(receiving, key).gt(value)]) + elif operator == "<": + r_filters.append([getattr(receiving, key).lt(value)]) + elif operator == ">=": + r_filters.append([getattr(receiving, key).gte(value)]) + elif operator == "<=": + r_filters.append([getattr(receiving, key).lte(value)]) + + receiving_query = Query.from_(receiving).select( + receiving.key, + receiving.doctype, + receiving.company, + receiving.parent, + receiving.warehouse, + receiving.name, + receiving.idx, + receiving.item_code, + receiving.schedule_date, + receiving.modified, + receiving.stock_uom, + receiving.stock_qty, + receiving.received_qty, + receiving.supplier, + ValueWrapper("").as_("status"), + receiving.assigned, + receiving.creation, + ) + + for r_filter in r_filters: + receiving_query = receiving_query.where(*r_filter) + + record_offset = records_per_page * (page - 1) + + query = f"{receiving_query} LIMIT {records_per_page} OFFSET {record_offset}" + + with get_demand_db() as conn: + cursor = conn.cursor() + rows: list[Receiving] = cursor.execute(query).fetchall() + for row in rows: + row.update( + { + "stock_qty": max(0.0, row.stock_qty), + "received_qty": max(0.0, row.received_qty or 0.0), + "rejected_qty": max(0.0, row.rejected_qty or 0.0), + "schedule_date": get_datetime_from_epoch(row.schedule_date), + "modified": get_datetime_from_epoch(row.modified), + "creation": get_datetime_from_epoch(row.creation), + } + ) + return rows diff --git a/beam/beam/demand/sqlite.py b/beam/beam/demand/sqlite.py new file mode 100644 index 00000000..e01f1e70 --- /dev/null +++ b/beam/beam/demand/sqlite.py @@ -0,0 +1,160 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import pathlib +import sqlite3 + +import frappe +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions +from frappe.utils import get_site_path +from frappe.utils.synchronization import filelock + + +def get_demand_db_path() -> pathlib.Path: + return pathlib.Path(f"{get_site_path()}/demand.db").resolve() + + +def get_demand_db() -> sqlite3.Connection: + path = get_demand_db_path() + with filelock(str(path)), sqlite3.connect(path) as conn: + cursor = conn.cursor() + cursor.execute("SELECT name FROM sqlite_master WHERE type='table' AND name='demand';") + data = cursor.fetchone() + if data: + connection = sqlite3.connect(path) + else: + connection = create_demand_db(cursor) + connection.row_factory = dict_factory + return connection + + +def create_demand_db(cursor: sqlite3.Cursor) -> sqlite3.Connection: + path = get_demand_db_path() + + inventory_dimensions = get_inventory_dimensions() + if inventory_dimensions: + inventory_dimensions = ( + f""",{",".join([f"{dimension['fieldname']} text" for dimension in inventory_dimensions])}""" + ) + else: + inventory_dimensions = "" + + cursor.execute( + f""" + CREATE TABLE demand( + key text, + doctype text, + company text, + parent text, + warehouse text, + production_item text, + bom_no text, + workstation text, + name text, + idx int, + item_code text, + item_warehouse text, + delivery_date int, + modified int, + total_required_qty real, + stock_uom text, + assigned text, + creation int, + customer text + {inventory_dimensions} + ) + """ + ) + cursor.execute( + f""" + CREATE TABLE allocation( + key text, + demand text, + doctype text, + company text, + parent text, + warehouse text, + production_item text, + bom_no text, + workstation text, + name text, + idx int, + item_code text, + item_warehouse text, + allocated_date int, + modified int, + allocated_qty real, + stock_uom text, + status text, + assigned text, + creation int, + is_manual boolean + {inventory_dimensions} + ) + """ + ) + cursor.execute( + f""" + CREATE TABLE receiving( + key text, + doctype text, + company text, + parent text, + warehouse text, + workstation text, + name text, + idx int, + item_code text, + schedule_date int, + modified int, + stock_qty real, + received_qty real, + stock_uom text, + assigned text, + creation int, + supplier text + {inventory_dimensions} + ) + """ + ) + cursor.execute("CREATE INDEX idx_demand_key ON demand(key)") + cursor.execute("CREATE INDEX idx_demand_company ON demand(company)") + cursor.execute("CREATE INDEX idx_demand_warehouse ON demand(warehouse)") + cursor.execute("CREATE INDEX idx_demand_item_code ON demand(item_code)") + cursor.execute("CREATE INDEX idx_demand_delivery_date ON demand(delivery_date)") + + cursor.execute("CREATE INDEX idx_allocation_key ON allocation(key)") + cursor.execute("CREATE INDEX idx_allocation_demand ON allocation(demand)") + cursor.execute("CREATE INDEX idx_allocation_company ON allocation(company)") + cursor.execute("CREATE INDEX idx_allocation_warehouse ON allocation(warehouse)") + cursor.execute("CREATE INDEX idx_allocation_item_code ON allocation(item_code)") + + cursor.execute("CREATE INDEX idx_receiving_key ON receiving(key)") + cursor.execute("CREATE INDEX idx_receiving_company ON receiving(company)") + cursor.execute("CREATE INDEX idx_receiving_warehouse ON receiving(warehouse)") + cursor.execute("CREATE INDEX idx_receiving_item_code ON receiving(item_code)") + cursor.execute("CREATE INDEX idx_receiving_schedule_date ON receiving(schedule_date)") + return sqlite3.connect(path) + + +def reset_demand_db() -> None: + with get_demand_db() as conn: + cursor = conn.cursor() + # sqlite does not implement a TRUNCATE command + cursor.execute("DELETE FROM demand") + cursor.execute("DELETE FROM allocation") + cursor.execute("DELETE FROM receiving") + + +def reset_receiving_db() -> None: + with get_demand_db() as conn: + cursor = conn.cursor() + # sqlite does not implement a TRUNCATE command + cursor.execute("DELETE FROM receiving") + + +def dict_factory(cursor: sqlite3.Cursor, row: sqlite3.Row) -> frappe._dict: + _dict = frappe._dict() + for idx, col in enumerate(cursor.description): + _dict[col[0]] = row[idx] + return _dict diff --git a/beam/beam/demand/utils.py b/beam/beam/demand/utils.py new file mode 100644 index 00000000..e2f9fd41 --- /dev/null +++ b/beam/beam/demand/utils.py @@ -0,0 +1,86 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import calendar +import datetime +from time import localtime + +import frappe +from frappe import _dict +from frappe.utils import get_datetime + + +class Base(_dict): + assigned: str + company: str + creation: str | float | datetime.datetime | None + doctype: str + idx: str | int + item_code: str + key: str + modified: str | float | datetime.datetime | None + name: str + parent: str + stock_uom: str + warehouse: str + workstation: str + + +class Demand(Base): + allocated_date: str | float | datetime.datetime | None + delivery_date: str | float | datetime.datetime | None + net_required_qty: str | float # not set directly, calculated in set_demand_query, used in item_demand_map + total_required_qty: str | float # demand quantity that hasn't been satisfied + + +class Allocation(Base): + allocated_date: str | float | datetime.datetime | None + allocated_qty: str | float + demand: str + is_manual: str | float + status: str + + # not directly available in the database, but computed using the demand row + delivery_date: str | float | datetime.datetime | None + net_required_qty: str | float # demand quantity that has yet to be allocated + total_required_qty: str | float # demand quantity that hasn't been satisfied + + +class Receiving(Base): + schedule_date: str | float | datetime.datetime + stock_qty: str | float + + +def get_epoch_from_datetime(dt: str | float | datetime.datetime | None = None) -> int | float: + if isinstance(dt, (int, float)): + return dt + + datetime_obj = get_datetime(dt) + time_tuple = datetime_obj.timetuple() + return calendar.timegm(time_tuple) + + +def get_datetime_from_epoch( + seconds: str | float | datetime.datetime | None, +) -> datetime.datetime | None: + if isinstance(seconds, datetime.datetime): + return seconds + + if isinstance(seconds, str) or seconds is None: + return get_datetime(seconds) + + if isinstance(seconds, float): + local_epoch = localtime(seconds) + local_epoch_list = local_epoch[:6] + return datetime.datetime(*local_epoch_list) + + +def validate_demand_enabled(func): + def wrapper(*args, **kwargs): + # TODO: make this specific to the company for the BEAM Settings? + demand_enabled = any(frappe.get_all("BEAM Settings", pluck="enable_demand")) + if not demand_enabled: + return + return func(*args, **kwargs) + + return wrapper diff --git a/beam/beam/doctype/beam_mobile_route/__init__.py b/beam/beam/doctype/beam_mobile_route/__init__.py new file mode 100644 index 00000000..6b9109e0 --- /dev/null +++ b/beam/beam/doctype/beam_mobile_route/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt diff --git a/beam/beam/doctype/beam_mobile_route/beam_mobile_route.json b/beam/beam/doctype/beam_mobile_route/beam_mobile_route.json new file mode 100644 index 00000000..117ef1b9 --- /dev/null +++ b/beam/beam/doctype/beam_mobile_route/beam_mobile_route.json @@ -0,0 +1,63 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-10-31 16:33:23.848152", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": ["label", "component", "route", "column_break_apfm", "dt", "role"], + "fields": [ + { + "fieldname": "component", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Component", + "reqd": 1 + }, + { + "fieldname": "dt", + "fieldtype": "Link", + "in_list_view": 1, + "label": "DocType", + "options": "DocType", + "reqd": 1 + }, + { + "fieldname": "role", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Role", + "options": "Role" + }, + { + "fieldname": "route", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Route", + "reqd": 1 + }, + { + "fieldname": "column_break_apfm", + "fieldtype": "Column Break" + }, + { + "fieldname": "label", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Label", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-11-01 10:07:45.331621", + "modified_by": "Administrator", + "module": "BEAM", + "name": "BEAM Mobile Route", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/beam/beam/doctype/beam_mobile_route/beam_mobile_route.py b/beam/beam/doctype/beam_mobile_route/beam_mobile_route.py new file mode 100644 index 00000000..dcfdc7a1 --- /dev/null +++ b/beam/beam/doctype/beam_mobile_route/beam_mobile_route.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class BEAMMobileRoute(Document): + pass diff --git a/beam/beam/doctype/beam_settings/beam_settings.js b/beam/beam/doctype/beam_settings/beam_settings.js index c653f4d6..8fc5be36 100644 --- a/beam/beam/doctype/beam_settings/beam_settings.js +++ b/beam/beam/doctype/beam_settings/beam_settings.js @@ -1,8 +1,80 @@ // Copyright (c) 2024, AgriTheory and contributors // For license information, please see license.txt -// frappe.ui.form.on("BEAM Settings", { -// refresh(frm) { +frappe.ui.form.on('BEAM Mobile Route', { + routes_add: frm => { + frm.fields_dict.routes.grid.update_docfield_property('component', 'options', frm.doc.__onload.components) + }, +}) +frappe.dom.set_style(` + .barcode-auto-generate-editor input[type="checkbox"]:not(:checked) + .label-area { + text-decoration: line-through; + color: var(--text-muted); + } +`) -// }, -// }); +frappe.ui.form.on('BEAM Settings', { + refresh(frm) { + const wrapper = $(frm.fields_dict.barcode_exclusions_html.wrapper) + wrapper.empty() + wrapper.addClass('barcode-auto-generate-editor').css({ + border: '1px solid var(--border-color)', + borderRadius: 'var(--border-radius)', + padding: 'var(--padding-md)', + }) + frm.barcode_exclusions_editor = new BEAMBarcodeAutoGenerateEditor(wrapper, frm) + }, + onload_post_render: frm => { + frm.fields_dict.routes.grid.update_docfield_property('component', 'options', frm.doc.__onload.components) + }, +}) + +class BEAMBarcodeAutoGenerateEditor { + constructor(wrapper, frm) { + this.wrapper = wrapper + this.frm = frm + this.setup() + } + + get allowed() { + try { + return JSON.parse(this.frm.doc.auto_barcode_doctypes || '["Item", "Warehouse"]') + } catch { + return ['Item', 'Warehouse'] + } + } + + setup() { + this.multicheck = frappe.ui.form.make_control({ + parent: this.wrapper, + df: { + fieldname: 'auto_barcode_doctypes', + fieldtype: 'MultiCheck', + select_all: true, + columns: '15rem', + get_data: () => { + return frappe + .xcall('beam.beam.doctype.beam_settings.beam_settings.get_doctypes_with_item_barcodes') + .then(doctypes => { + const allowed = this.allowed + return doctypes.map(dt => ({ + label: __(dt), + value: dt, + checked: allowed.includes(dt), + })) + }) + }, + on_change: () => { + this.sync_json() + this.frm.dirty() + }, + }, + render_input: true, + }) + } + + sync_json() { + const checked = this.multicheck.get_checked_options() + frappe.model.set_value(this.frm.doctype, this.frm.docname, 'auto_barcode_doctypes', JSON.stringify(checked)) + } +} diff --git a/beam/beam/doctype/beam_settings/beam_settings.json b/beam/beam/doctype/beam_settings/beam_settings.json index e2f175ea..c5662dac 100644 --- a/beam/beam/doctype/beam_settings/beam_settings.json +++ b/beam/beam/doctype/beam_settings/beam_settings.json @@ -1,22 +1,39 @@ { "actions": [], - "allow_rename": 1, "autoname": "field:company", - "creation": "2024-03-18 17:06:58.552999", + "creation": "2024-03-18 17:06:58.552900", "doctype": "DocType", "engine": "InnoDB", - "field_order": ["company", "enable_handling_units", "barcode_font_size"], + "field_order": [ + "general_tab", + "company", + "barcode_font_size", + "enable_handling_units", + "scan_serial_no", + "ignore_drop_shipped_items", + "column_break_twrc", + "receiving_workstation", + "shipping_workstation", + "column_break_vhpb", + "qr_scale", + "qr_border", + "qr_error_correct", + "barcode_generation_section", + "barcode_exclusions_html", + "column_break_barcode", + "auto_barcode_doctypes", + "demand_tab", + "enable_demand", + "warehouse_types", + "mobile_tab", + "routes", + "section_break_yadp", + "enable_scan_to_login", + "restrict_ip", + "column_break_lwuv", + "show_scan_output" + ], "fields": [ - { - "fieldname": "company", - "fieldtype": "Link", - "in_list_view": 1, - "label": "Company", - "options": "Company", - "reqd": 1, - "set_only_once": 1, - "unique": 1 - }, { "default": "1", "fieldname": "enable_handling_units", @@ -24,30 +41,170 @@ "in_list_view": 1, "label": "Enable Handling Units" }, + { + "default": "0", + "fieldname": "ignore_drop_shipped_items", + "fieldtype": "Check", + "label": "Ignore Drop Shipped Items in Demand" + }, { "default": "12", "fieldname": "barcode_font_size", "fieldtype": "Int", "label": "Barcode Font Size" + }, + { + "depends_on": "enable_demand", + "description": "These Warehouse Types will be excluded when checking for inventory availability", + "fieldname": "warehouse_types", + "fieldtype": "Table MultiSelect", + "label": "Warehouse Types", + "options": "Warehouse Types" + }, + { + "fieldname": "general_tab", + "fieldtype": "Tab Break", + "label": "General" + }, + { + "fieldname": "column_break_twrc", + "fieldtype": "Column Break" + }, + { + "fieldname": "receiving_workstation", + "fieldtype": "Link", + "label": "Receiving Workstation", + "options": "Workstation" + }, + { + "fieldname": "shipping_workstation", + "fieldtype": "Link", + "label": "Shipping Workstation", + "options": "Workstation" + }, + { + "fieldname": "demand_tab", + "fieldtype": "Tab Break", + "label": "Demand" + }, + { + "fieldname": "mobile_tab", + "fieldtype": "Tab Break", + "label": "Mobile" + }, + { + "fieldname": "routes", + "fieldtype": "Table", + "options": "BEAM Mobile Route" + }, + { + "default": "0", + "fieldname": "enable_demand", + "fieldtype": "Check", + "label": "Enable Demand" + }, + { + "columns": 4, + "description": "Restrict user from this IP address only. Multiple IP addresses can be added by separating with commas. Also accepts partial IP addresses like (111.111.111)", + "fieldname": "restrict_ip", + "fieldtype": "Small Text", + "label": "Restrict IP" + }, + { + "fieldname": "section_break_yadp", + "fieldtype": "Section Break" + }, + { + "fieldname": "column_break_lwuv", + "fieldtype": "Column Break" + }, + { + "default": "Not Allowed", + "fieldname": "enable_scan_to_login", + "fieldtype": "Select", + "label": "Enable Scan to Login", + "options": "Not Allowed\nMobile Users Only\nAll Users" + }, + { + "default": "0", + "fieldname": "show_scan_output", + "fieldtype": "Check", + "label": "Show Scan Output in Mobile View" + }, + { + "default": "0", + "fieldname": "scan_serial_no", + "fieldtype": "Check", + "label": "Enable Scanning of Serial Numbers" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "options": "Company", + "unique": 1 + }, + { + "fieldname": "column_break_vhpb", + "fieldtype": "Column Break" + }, + { + "default": "8", + "fieldname": "qr_scale", + "fieldtype": "Int", + "label": "QR Scale", + "non_negative": 1 + }, + { + "default": "4", + "fieldname": "qr_border", + "fieldtype": "Int", + "label": "QR Border", + "non_negative": 1 + }, + { + "default": "M", + "fieldname": "qr_error_correct", + "fieldtype": "Select", + "label": "QR Error Correct", + "options": "L\nM\nQ\nH" + }, + { + "fieldname": "barcode_generation_section", + "fieldtype": "Section Break", + "label": "Barcode Generation" + }, + { + "fieldname": "barcode_exclusions_html", + "fieldtype": "HTML", + "label": "Disable Auto-Generation For" + }, + { + "fieldname": "column_break_barcode", + "fieldtype": "Column Break" + }, + { + "default": "[\"Item\", \"Warehouse\"]", + "fieldname": "auto_barcode_doctypes", + "fieldtype": "JSON", + "hidden": 1, + "label": "Auto Barcode Doctypes" } ], - "index_web_pages_for_search": 1, "links": [], - "modified": "2024-05-29 01:43:57.177980", + "modified": "2026-01-02 20:34:56.013615", "modified_by": "Administrator", "module": "BEAM", "name": "BEAM Settings", - "naming_rule": "Expression (old style)", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { "create": 1, "delete": 1, "email": 1, - "export": 1, "print": 1, "read": 1, - "report": 1, "role": "System Manager", "share": 1, "write": 1 @@ -56,15 +213,14 @@ "create": 1, "delete": 1, "email": 1, - "export": 1, "print": 1, "read": 1, - "report": 1, "role": "Stock Manager", "share": 1, "write": 1 } ], + "row_format": "Dynamic", "sort_field": "modified", "sort_order": "DESC", "states": [], diff --git a/beam/beam/doctype/beam_settings/beam_settings.py b/beam/beam/doctype/beam_settings/beam_settings.py index 23de90d8..feaafd48 100644 --- a/beam/beam/doctype/beam_settings/beam_settings.py +++ b/beam/beam/doctype/beam_settings/beam_settings.py @@ -6,12 +6,86 @@ class BEAMSettings(Document): - pass + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + from beam.beam.doctype.beam_mobile_route.beam_mobile_route import BEAMMobileRoute + from beam.beam.doctype.warehouse_types.warehouse_types import WarehouseTypes + + barcode_font_size: DF.Int + company: DF.Link + enable_demand: DF.Check + enable_handling_units: DF.Check + enable_scan_to_login: DF.Literal["Not Allowed", "Mobile Users Only", "All Users"] + ignore_drop_shipped_items: DF.Check + receiving_workstation: DF.Link | None + restrict_ip: DF.SmallText | None + routes: DF.Table[BEAMMobileRoute] + shipping_workstation: DF.Link | None + show_scan_output: DF.Check + warehouse_types: DF.TableMultiSelect[WarehouseTypes] + # end: auto-generated types + + def onload(self): + hooks = get_configuration_hooks() + self.set_onload("components", hooks.components) + self.set_onload("routes", hooks.routes) + + def get_beam_mobile_home_for_user(self, user): + allowed_routes = [] + for row in self.routes: + # if user has read permission on doctype + allowed_routes.append( + { + "label": row.label, + "route": row.route, + "doctype": row.doctype, + } + ) + return {"routes": allowed_routes, "company": self.company} @frappe.whitelist() def create_beam_settings(company: str) -> str: - beams = frappe.new_doc("BEAM Settings") - beams.company = company - beams.save() - return beams + beam_settings = frappe.new_doc("BEAM Settings") + beam_settings.company = company + beam_settings.save() + return beam_settings + + +@frappe.whitelist() +def get_beam_home(): + # get user company via employee + # get settings + # apply roles + user = frappe.session.user + beam_settings = frappe.get_last_doc("BEAM Settings") + return beam_settings.get_beam_mobile_home_for_user(user) + + +def get_configuration_hooks(): + bm = frappe.get_hooks().get("beam_mobile") + components = sorted(list(set(bm.get("components").keys()))) + return frappe._dict({"components": components}) + + +@frappe.whitelist() +def get_doctypes_with_item_barcodes() -> list[str]: + """Return all doctypes that have a Table field with options 'Item Barcode'.""" + existing_doctypes = set(frappe.get_all("DocType", pluck="name")) + standard = frappe.get_all( + "DocField", + filters={"fieldtype": "Table", "options": "Item Barcode"}, + pluck="parent", + ) + custom = frappe.get_all( + "Custom Field", + filters={"fieldtype": "Table", "options": "Item Barcode"}, + pluck="dt", + ) + return sorted(existing_doctypes.intersection(standard + custom)) diff --git a/beam/beam/doctype/beam_settings/test_beam_settings.py b/beam/beam/doctype/beam_settings/test_beam_settings.py deleted file mode 100644 index 61700c28..00000000 --- a/beam/beam/doctype/beam_settings/test_beam_settings.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2024, AgriTheory and Contributors -# See license.txt - -# import frappe -from frappe.tests.utils import FrappeTestCase - - -class TestBEAMSettings(FrappeTestCase): - pass diff --git a/beam/beam/doctype/handling_unit/handling_unit.py b/beam/beam/doctype/handling_unit/handling_unit.py index b458961a..eeba0de7 100644 --- a/beam/beam/doctype/handling_unit/handling_unit.py +++ b/beam/beam/doctype/handling_unit/handling_unit.py @@ -8,10 +8,23 @@ class HandlingUnit(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + handling_unit_name: DF.Data | None + # end: auto-generated types + def autoname(self): self.handling_unit_name = self.name = str(uuid.uuid4().int >> 64) def validate(self): + if frappe.db.exists("Item Barcode", {"barcode": self.name, "parent": self.name}): + return barcode = frappe.new_doc("Item Barcode") barcode.parenttype = "Handling Unit" barcode.barcode_type = "Code128" diff --git a/beam/beam/doctype/warehouse_types/__init__.py b/beam/beam/doctype/warehouse_types/__init__.py new file mode 100644 index 00000000..6b9109e0 --- /dev/null +++ b/beam/beam/doctype/warehouse_types/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt diff --git a/beam/beam/doctype/warehouse_types/warehouse_types.json b/beam/beam/doctype/warehouse_types/warehouse_types.json new file mode 100644 index 00000000..dc358e14 --- /dev/null +++ b/beam/beam/doctype/warehouse_types/warehouse_types.json @@ -0,0 +1,30 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-06-14 02:54:12.889848", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": ["warehouse_type"], + "fields": [ + { + "fieldname": "warehouse_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Warehouse Type", + "options": "Warehouse Type" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-06-14 03:07:41.341693", + "modified_by": "Administrator", + "module": "BEAM", + "name": "Warehouse Types", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} diff --git a/beam/beam/doctype/warehouse_types/warehouse_types.py b/beam/beam/doctype/warehouse_types/warehouse_types.py new file mode 100644 index 00000000..62d0584b --- /dev/null +++ b/beam/beam/doctype/warehouse_types/warehouse_types.py @@ -0,0 +1,9 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class WarehouseTypes(Document): + pass diff --git a/beam/beam/handling_unit.py b/beam/beam/handling_unit.py index b73182fe..1a9a44a9 100644 --- a/beam/beam/handling_unit.py +++ b/beam/beam/handling_unit.py @@ -49,6 +49,17 @@ def generate_handling_units(doc, method=None): row.to_handling_unit = handling_unit.name continue + if ( + doc.doctype == "Stock Entry" + and doc.purpose == "Repack" + and row.t_warehouse + and not row.handling_unit + ): + handling_unit = frappe.new_doc("Handling Unit") + handling_unit.save() + row.handling_unit = handling_unit.name + continue + if doc.doctype == "Subcontracting Receipt" and not row.handling_unit: handling_unit = frappe.new_doc("Handling Unit") handling_unit.save() diff --git a/beam/beam/overrides/company.py b/beam/beam/overrides/company.py new file mode 100644 index 00000000..86fbbb04 --- /dev/null +++ b/beam/beam/overrides/company.py @@ -0,0 +1,11 @@ +# Copyright (c) 2026, AgriTheory and contributors +# For license information, please see license.txt + +import frappe + +from beam.beam.doctype.beam_settings.beam_settings import create_beam_settings + + +def create_company_beam_settings(doc, method=None): + if not frappe.db.exists("BEAM Settings", {"company": doc.name}): + create_beam_settings(doc.name) diff --git a/beam/beam/overrides/inventory_dimension.py b/beam/beam/overrides/inventory_dimension.py new file mode 100644 index 00000000..84643378 --- /dev/null +++ b/beam/beam/overrides/inventory_dimension.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +from beam.beam.demand.demand import build_demand_allocation_map +from beam.beam.demand.receiving import reset_build_receiving_map + + +def reset_demand_map(dimension, method): + return build_demand_allocation_map() + + +def reset_receiving_map(dimension, method): + return reset_build_receiving_map() diff --git a/beam/beam/overrides/network_printer_settings.py b/beam/beam/overrides/network_printer_settings.py new file mode 100644 index 00000000..0579e59e --- /dev/null +++ b/beam/beam/overrides/network_printer_settings.py @@ -0,0 +1,62 @@ +# Copyright (c) 2025, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +from frappe import _ +from frappe.printing.doctype.network_printer_settings.network_printer_settings import ( + NetworkPrinterSettings, +) + + +class BEAMNetworkPrinterSettings(NetworkPrinterSettings): + @frappe.whitelist() + def get_printers_list(self, ip="127.0.0.1", port=631): + printer_list = [] + try: + import cups + except ImportError: + frappe.throw( + _( + """This feature can not be used as dependencies are missing. + Please contact your system manager to enable this by installing pycups!""" + ) + ) + return + try: + cups.setServer(self.server_ip) + cups.setPort(self.port) + conn = cups.Connection() + printers = conn.getPrinters() + for printer_id, printer in printers.items(): + make_model = printer["printer-make-and-model"] + location = printer.get("printer-location", "") + description = f"{make_model}, {location}" if location else make_model + printer_list.append( + { + "value": printer_id, + "label": printer_id, + "description": description, + "location": location, + } + ) + except RuntimeError: + frappe.throw(_("Failed to connect to server")) + except frappe.ValidationError: + frappe.throw(_("Failed to connect to server")) + return printer_list + + def validate(self): + self.push_location_to_cups() + + def push_location_to_cups(self): + if not self.printer_name: + return + try: + import cups + + cups.setServer(self.server_ip) + cups.setPort(self.port) + conn = cups.Connection() + conn.setPrinterLocation(self.printer_name, self.printer_location or "") + except Exception: + pass diff --git a/beam/beam/overrides/sales_order.py b/beam/beam/overrides/sales_order.py new file mode 100644 index 00000000..39f33182 --- /dev/null +++ b/beam/beam/overrides/sales_order.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +from erpnext.selling.doctype.sales_order.sales_order import SalesOrder + +from beam.beam.demand.demand import add_demand_allocation, remove_demand_allocation + + +class BEAMSalesOrder(SalesOrder): + def update_status(self, status): + super().update_status(status) + if self.docstatus == 1: + if status == "Draft": # status for resuming a held or closed sales order + add_demand_allocation(self.name) + elif status in ("Completed", "Cancelled", "Closed", "On Hold"): + remove_demand_allocation(self.name) diff --git a/beam/beam/overrides/stock_entry.py b/beam/beam/overrides/stock_entry.py index 10c4c3b8..24297311 100644 --- a/beam/beam/overrides/stock_entry.py +++ b/beam/beam/overrides/stock_entry.py @@ -19,6 +19,29 @@ def update_stock_ledger(self): finished_item_row = self.get_finished_item_row() self.get_sle_for_source_warehouse(sl_entries, finished_item_row) self.get_sle_for_target_warehouse(sl_entries, finished_item_row) + + # Ensure handling_unit is set on SLE entries if enabled + if settings.enable_handling_units: + for sle in sl_entries: + if hasattr(sle, "get") and "voucher_detail_no" in sle: + item_row = next( + (item for item in self.items if item.name == sle.get("voucher_detail_no")), None + ) + if item_row: + # For source warehouse (consumption), use handling_unit + if ( + sle.get("warehouse") == item_row.s_warehouse + and hasattr(item_row, "handling_unit") + and item_row.handling_unit + ): + sle["handling_unit"] = item_row.handling_unit + # For target warehouse (receipt), use to_handling_unit if it exists, otherwise handling_unit + elif sle.get("warehouse") == item_row.t_warehouse: + if hasattr(item_row, "to_handling_unit") and item_row.to_handling_unit: + sle["handling_unit"] = item_row.to_handling_unit + elif hasattr(item_row, "handling_unit") and item_row.handling_unit: + sle["handling_unit"] = item_row.handling_unit + if self.docstatus == 2: sl_entries.reverse() self.make_sl_entries(sl_entries) @@ -30,7 +53,12 @@ def update_stock_ledger(self): def make_handling_unit_sles(self): hu_sles = [] for d in self.get("items"): - if self.docstatus == 2 and not d.recombine_on_cancel and d.handling_unit and d.to_handling_unit: + # Only process when cancelling AND user wants to keep separate (NOT recombine) + if self.docstatus != 2 or d.recombine_on_cancel or not d.handling_unit: + continue + + if d.handling_unit and d.to_handling_unit: + # Material Transfer types: both HUs on the same row sle = self.get_sl_entries( d, { @@ -53,6 +81,32 @@ def make_handling_unit_sles(self): _sle["handling_unit"] = d.to_handling_unit _sle["is_cancelled"] = 0 hu_sles.append(_sle) + elif d.s_warehouse and not d.t_warehouse: + # Repack/Manufacture source row: re-consume from source HU + sle = self.get_sl_entries( + d, + { + "warehouse": cstr(d.s_warehouse), + "actual_qty": -flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate), + }, + ) + sle["handling_unit"] = d.handling_unit + sle["is_cancelled"] = 0 + hu_sles.append(sle) + elif d.t_warehouse and not d.s_warehouse: + # Repack/Manufacture target row: re-add to target HU + sle = self.get_sl_entries( + d, + { + "warehouse": cstr(d.t_warehouse), + "actual_qty": flt(d.transfer_qty), + "incoming_rate": flt(d.valuation_rate), + }, + ) + sle["handling_unit"] = d.handling_unit + sle["is_cancelled"] = 0 + hu_sles.append(sle) return hu_sles diff --git a/beam/beam/overrides/subcontracting_receipt.py b/beam/beam/overrides/subcontracting_receipt.py index 3e8e4454..4303a42c 100644 --- a/beam/beam/overrides/subcontracting_receipt.py +++ b/beam/beam/overrides/subcontracting_receipt.py @@ -87,14 +87,13 @@ def get_sle(self): if stock_entry := frappe.db.exists( "Stock Entry", {"subcontracting_order": row.subcontracting_order, "docstatus": 1} ): - sle_hu = frappe.db.sql( - f""" - Select name, handling_unit, item_code - From `tabStock Ledger Entry` - where voucher_type = "Stock Entry" and voucher_no = '{stock_entry}' and - warehouse = '{self.supplier_warehouse}' - """, - as_dict=True, + sle_hu_map[row.subcontracting_order] = frappe.get_all( + "Stock Ledger Entry", + filters={ + "voucher_type": "Stock Entry", + "voucher_no": stock_entry, + "warehouse": self.supplier_warehouse, + }, + fields=["name", "handling_unit", "item_code"], ) - sle_hu_map[row.subcontracting_order] = sle_hu return sle_hu_map diff --git a/beam/beam/overrides/work_order.py b/beam/beam/overrides/work_order.py new file mode 100644 index 00000000..9a452953 --- /dev/null +++ b/beam/beam/overrides/work_order.py @@ -0,0 +1,16 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +from erpnext.manufacturing.doctype.work_order.work_order import WorkOrder + +from beam.beam.demand.demand import add_demand_allocation, remove_demand_allocation + + +class BEAMWorkOrder(WorkOrder): + def update_status(self, status=None): + super().update_status(status) + if self.docstatus == 1: + if status == "Resumed": + add_demand_allocation(self.name) + elif status in ("Completed", "Cancelled", "Closed", "Stopped"): + remove_demand_allocation(self.name) diff --git a/beam/beam/print_format/labelary_print_preview/labelary_print_preview.json b/beam/beam/print_format/labelary_print_preview/labelary_print_preview.json index 3a015b3a..fba3563e 100644 --- a/beam/beam/print_format/labelary_print_preview/labelary_print_preview.json +++ b/beam/beam/print_format/labelary_print_preview/labelary_print_preview.json @@ -10,7 +10,7 @@ "docstatus": 0, "doctype": "Print Format", "font_size": 14, - "html": "
\n \n
\n", + "html": "
\n \n
\n", "idx": 0, "line_breaks": 0, "margin_bottom": 15.0, diff --git a/beam/beam/print_format/microqr_serial_no/__init__.py b/beam/beam/print_format/microqr_serial_no/__init__.py new file mode 100644 index 00000000..b1279b72 --- /dev/null +++ b/beam/beam/print_format/microqr_serial_no/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2025, AgriTheory and contributors +# For license information, please see license.txt diff --git a/beam/beam/print_format/microqr_serial_no/microqr_serial_no.json b/beam/beam/print_format/microqr_serial_no/microqr_serial_no.json new file mode 100644 index 00000000..ea87ff73 --- /dev/null +++ b/beam/beam/print_format/microqr_serial_no/microqr_serial_no.json @@ -0,0 +1,33 @@ +{ + "absolute_value": 0, + "align_labels_right": 0, + "creation": "2025-08-11 13:33:35.315005", + "css": "", + "custom_format": 1, + "default_print_language": "en-US", + "disabled": 0, + "doc_type": "Serial No", + "docstatus": 0, + "doctype": "Print Format", + "font_size": 14, + "html": "{% set sn = get_serial_no(doc.name) %}\n\n\n
\n
0mm
\n
0mm
\n
0mm
\n
0mm
\n\n
\n
\n \n
\n
\n {{get_qr_code(sn.serial_no)}}\n
\n
\n
{{ sn.item_code or doc.item_code }}
\n
{{ sn.serial_no }}
\n
{{ frappe.utils.format_datetime(sn.posting_datetime) }}
\n
{{ sn.company }}
\n
\n\n
\n
\n\n", + "idx": 0, + "line_breaks": 0, + "margin_bottom": 15.0, + "margin_left": 15.0, + "margin_right": 15.0, + "margin_top": 15.0, + "modified": "2025-08-11 15:14:09.683495", + "modified_by": "Administrator", + "module": "BEAM", + "name": "MicroQR Serial No", + "owner": "Administrator", + "page_number": "Hide", + "pdf_generator": "wkhtmltopdf", + "print_format_builder": 0, + "print_format_builder_beta": 0, + "print_format_type": "Jinja", + "raw_printing": 0, + "show_section_headings": 0, + "standard": "Yes" +} diff --git a/beam/beam/printing.py b/beam/beam/printing.py index fb126ad7..c78ece1b 100644 --- a/beam/beam/printing.py +++ b/beam/beam/printing.py @@ -31,9 +31,14 @@ def print_by_server( ): print_settings = frappe.get_doc("Network Printer Settings", printer_setting) if isinstance(doc, str): - doc = frappe._dict(json.loads(doc)) + _doc = frappe._dict(json.loads(doc)) + doc = frappe.get_doc(_doc.doctype, _doc.name) + doc.update(_doc) if not print_format: print_format = frappe.get_meta(doctype).get("default_print_format") + # Default to "Standard" print format if still empty + if not print_format: + print_format = "Standard" print_format = frappe.get_doc("Print Format", print_format) try: cups.setServer(print_settings.server_ip) @@ -99,8 +104,9 @@ def print_handling_units( doctype=None, name=None, printer_setting=None, print_format=None, doc=None ): if isinstance(doc, str): - doc = frappe._dict(json.loads(doc)) - + _doc = frappe._dict(json.loads(doc)) + doc = frappe.get_doc(_doc.doctype, _doc.name) + doc.update(_doc) for row in doc.get("items"): if not row.get("handling_unit"): continue @@ -145,6 +151,18 @@ def labelary_api(doc, print_format, settings=None): e.globals.update(methods) template = e.from_string(print_format.raw_commands) output = template.render(doc=doc) - url = "http://api.labelary.com/v1/printers/8dpmm/labels/6x4/0/" + + # Extract label dimensions and DPI from settings + # dpmm: dots per millimeter (default 8 = ~203 DPI) + # width: label width in inches (default 6) + # height: label height in inches (default 4) + # index: label index for multi-label formats (default 0) + dpmm = settings.get("dpmm", 8) # 8 dpmm ≈ 203 DPI, 12 dpmm ≈ 300 DPI + width = settings.get("width", 6) + height = settings.get("height", 4) + index = settings.get("index", 0) + + url = f"http://api.labelary.com/v1/printers/{dpmm}dpmm/labels/{width}x{height}/{index}/" r = requests.post(url, files={"file": output}) - return base64.b64encode(r.content).decode("ascii") + content = r.content + return base64.b64encode(content).decode("ascii") diff --git a/beam/beam/report/demand_map/__init__.py b/beam/beam/report/demand_map/__init__.py new file mode 100644 index 00000000..6b9109e0 --- /dev/null +++ b/beam/beam/report/demand_map/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt diff --git a/beam/beam/report/demand_map/demand_map.js b/beam/beam/report/demand_map/demand_map.js new file mode 100644 index 00000000..4e62dcc3 --- /dev/null +++ b/beam/beam/report/demand_map/demand_map.js @@ -0,0 +1,68 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +frappe.query_reports['Demand Map'] = { + onload: report => { + // TODO: assuming a single BEAM Settings is used + if (frappe.boot.enabled_beam_settings.length === 0) { + report.get_no_result_message = () => { + return `
+
+ Generic Empty State +
+

${__('Please enable demand in BEAM Settings')}

+
` + } + } + }, + + filters: [ + { + fieldname: 'order_by', + fieldtype: 'Select', + options: ['Oldest Unallocated', 'Oldest Allocated', 'Newest Allocated', 'Newest Unallocated'], + default: 'Newest Unallocated', + label: frappe._('Sort By'), + }, + { + fieldname: 'item_code', + fieldtype: 'Link', + label: frappe._('Item'), + options: 'Item', + }, + ], + + formatter: (value, row, column, data, default_formatter) => { + value = default_formatter(value, row, column, data) + if (data && ['net_required_qty', 'total_required_qty'].includes(column.fieldname)) { + if (data.net_required_qty <= data.allocated_qty) { + value = `${value}` + } else if (!data.allocated_qty && data.net_required_qty) { + value = `${value}` + } else if (data.allocated_qty > 0) { + value = `${value}` + } else if (data.indent == 1 && data.allocated_qty) { + value = `${value}` + } + } + if (data && column.fieldname == 'allocated_qty') { + if (data.net_required_qty <= data.allocated_qty) { + value = `${value}` + } else if (!data.allocated_qty) { + value = `${value}` + } else if (data.net_required_qty > data.allocated_qty) { + value = `${value}` + } + } + if (data && column.fieldname == 'status') { + if (data.status == 'Unallocated') { + value = `${value}` + } else if (data.status == 'Partially Allocated') { + value = `${value}` + } else if (data.status == 'Soft Allocated') { + value = `${value}` + } + } + return value + }, +} diff --git a/beam/beam/report/demand_map/demand_map.json b/beam/beam/report/demand_map/demand_map.json new file mode 100644 index 00000000..81e3551e --- /dev/null +++ b/beam/beam/report/demand_map/demand_map.json @@ -0,0 +1,50 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2024-07-22 10:40:58.797332", + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "letterhead": null, + "modified": "2024-07-22 10:40:58.797332", + "modified_by": "Administrator", + "module": "BEAM", + "name": "Demand Map", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Item", + "report_name": "Demand Map", + "report_type": "Script Report", + "roles": [ + { + "role": "Item Manager" + }, + { + "role": "Stock Manager" + }, + { + "role": "Stock User" + }, + { + "role": "Sales User" + }, + { + "role": "Purchase User" + }, + { + "role": "Maintenance User" + }, + { + "role": "Accounts User" + }, + { + "role": "Manufacturing User" + }, + { + "role": "All" + } + ] +} diff --git a/beam/beam/report/demand_map/demand_map.py b/beam/beam/report/demand_map/demand_map.py new file mode 100644 index 00000000..ef8a9d27 --- /dev/null +++ b/beam/beam/report/demand_map/demand_map.py @@ -0,0 +1,147 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +from frappe import _ +from frappe.utils.data import flt + +from beam.beam.demand.demand import get_demand_db +from beam.beam.demand.utils import get_datetime_from_epoch, validate_demand_enabled + + +def execute(filters=None): + return get_columns(filters), get_data(filters) + + +def get_columns(filters): + return [ + {"fieldname": "key", "fieldtype": "Data", "hidden": True}, + {"fieldname": "doctype", "fieldtype": "Link", "options": "DocType", "hidden": True}, + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": "250px", + }, + { + "label": _("Demand Warehouse"), + "fieldname": "demand_warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": "200px", + }, + { + "label": _("Source Warehouse"), + "fieldname": "source_warehouse", + "fieldtype": "Data", + "width": "200px", + }, + { + "label": _("Workstation"), + "fieldname": "workstation", + "fieldtype": "Data", + "width": "200px", + }, + { + "label": _("Document"), + "fieldname": "parent", + "fieldtype": "Dynamic Link", + "options": "doctype", + "width": "200px", + }, + {"fieldname": "name", "fieldtype": "Data", "hidden": True}, + { + "label": _("Delivery Date"), + "fieldname": "delivery_date", + "fieldtype": "Datetime", + "width": "200px", + "align": "Right", + }, + { + "label": _("Total Req Qty"), + "fieldname": "total_required_qty", + "fieldtype": "Float", + "width": "120px", + "align": "Right", + }, + { + "label": _("Net Req Qty"), + "fieldname": "net_required_qty", + "fieldtype": "Float", + "width": "120px", + "align": "Right", + }, + { + "label": _("Allocated"), + "fieldname": "allocated_qty", + "fieldtype": "Float", + "width": "100px", + "align": "Right", + }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Data", + "width": "100px", + }, + { + "label": _("Status"), + "fieldname": "status", + "fieldtype": "Data", + "width": "150px", + "align": "center", + }, + { + "label": _("Assigned"), + "fieldname": "assigned", + "fieldtype": "Data", + "width": "150px", + }, + ] + + +@validate_demand_enabled +def get_data(filters): + rows = [] + with get_demand_db() as conn: + cursor = conn.cursor() + filter_query = "" + if filters.item_code: + filter_query = f"WHERE item_code = '{filters.item_code}'" + demand = cursor.execute( + f""" + SELECT + d.*, + COALESCE( + (SELECT SUM(a.allocated_qty) FROM allocation a WHERE a.demand = d.key), + 0 + ) AS allocated_qty + FROM + demand d + {filter_query} + ORDER BY delivery_date ASC; + """ + ).fetchall() + + # TODO: implement sort filters here + indent_counter = 0 + + for row in demand: + row.indent = 0 + row.demand_warehouse = row.pop("warehouse") + rows.append(row) + allocations = cursor.execute(f"SELECT * FROM allocation WHERE demand = '{row.key}'").fetchall() + row.allocated_qty = sum(flt(allocation.allocated_qty) for allocation in allocations) + row.net_required_qty = row.total_required_qty - row.allocated_qty + for allocation in allocations: + allocation.indent = 1 + allocation.total_required_qty = None + allocation.net_required_qty = None + allocation.delivery_date = get_datetime_from_epoch(allocation.allocated_date) + allocation.source_warehouse = allocation.pop("warehouse") + if allocation.source_warehouse != row.demand_warehouse: + allocation.source_warehouse = ( + f'{allocation.source_warehouse}' + ) + rows.append(allocation) + return rows diff --git a/beam/beam/report/handling_unit_traceability/handling_unit_traceability.js b/beam/beam/report/handling_unit_traceability/handling_unit_traceability.js index b393753e..6159cfb8 100644 --- a/beam/beam/report/handling_unit_traceability/handling_unit_traceability.js +++ b/beam/beam/report/handling_unit_traceability/handling_unit_traceability.js @@ -1,6 +1,5 @@ // Copyright (c) 2023, AgriTheory and contributors // For license information, please see license.txt -/* eslint-disable */ frappe.query_reports['Handling Unit Traceability'] = { filters: [ diff --git a/beam/beam/scan/__init__.py b/beam/beam/scan/__init__.py index 20a37ba3..b322b157 100644 --- a/beam/beam/scan/__init__.py +++ b/beam/beam/scan/__init__.py @@ -1,13 +1,17 @@ # Copyright (c) 2025, AgriTheory and contributors # For license information, please see license.txt +import copy import datetime import json from typing import Any import frappe from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry -from erpnext.stock.get_item_details import get_item_details +from erpnext.stock.get_item_details import get_item_details, get_valuation_rate +from frappe.query_builder import Case, DocType +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Coalesce @frappe.whitelist() @@ -21,8 +25,8 @@ def scan( context_dict = frappe._dict(json.loads(context) if isinstance(context, str) else context) barcode_doc = get_barcode_context(barcode) if not barcode_doc: - frappe.msgprint("Barcode not found", alert=True) return None # mypy asked for this + # print(barcode_doc.as_json()) if "listview" in context_dict: return get_list_action(barcode_doc, context_dict) elif "frm" in context_dict: @@ -31,17 +35,56 @@ def scan( def get_barcode_context(barcode: str) -> frappe._dict | None: + # Get BEAM Settings for default company + company = frappe.defaults.get_defaults().get("company") + settings = None + if company and frappe.db.exists("BEAM Settings", {"company": company}): + settings = frappe.get_cached_doc("BEAM Settings", company) + item_barcode = frappe.db.get_value( "Item Barcode", {"barcode": barcode}, ["parent", "parenttype"], as_dict=True ) - if not item_barcode: - return None # mypy asked for this - return frappe._dict( - { - "doc": frappe.get_doc(item_barcode.parenttype, item_barcode.parent), - "barcode": barcode, - } - ) + if item_barcode: + return frappe._dict( + { + "doc": frappe.get_doc(item_barcode.parenttype, item_barcode.parent), + "barcode": barcode, + } + ) + elif not item_barcode and settings and settings.scan_serial_no: + serial_no_table = frappe.qb.DocType("Serial No") + bundle_entry_table = frappe.qb.DocType("Serial and Batch Entry") + bundle_table = frappe.qb.DocType("Serial and Batch Bundle") + serial_lookup = ( + ( + frappe.qb.from_(serial_no_table) + .select( + ConstantColumn("Serial No").as_("doctype"), + serial_no_table.name, + ) + .where(serial_no_table.name == barcode) + ) + .union( + frappe.qb.from_(bundle_entry_table) + .join(bundle_table) + .on(bundle_entry_table.parent == bundle_table.name) + .select( + ConstantColumn("Serial and Batch Bundle").as_("doctype"), + bundle_entry_table.parent, + ) + .where(bundle_entry_table.serial_no == barcode) + ) + .limit(1) + .run(as_dict=True) + ) + if serial_lookup: + return frappe._dict( + { + "doc": frappe.get_doc(serial_lookup[0].doctype, serial_lookup[0].name), + "barcode": barcode, + } + ) + return None def get_handling_unit(handling_unit: str, parent_doctype: str | None = None) -> frappe._dict: @@ -51,6 +94,7 @@ def get_handling_unit(handling_unit: str, parent_doctype: str | None = None) -> fields=[ "item_code", "SUM(actual_qty) AS stock_qty", + "company", "handling_unit", "voucher_no", "posting_date", @@ -138,6 +182,12 @@ def get_list_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di else: target = get_handling_unit(barcode_doc.doc.name) target = target.get("voucher_no") if target else None + elif barcode_doc.doc.doctype == "Serial No": + if context.get("listview") in ["Item", "Putaway Rule"]: + target = barcode_doc.doc.item_code + else: + target = get_serial_no(barcode_doc.doc.name, context.get("listview")) + target = target.get("voucher_no") if target else None if not target: return [] @@ -150,14 +200,23 @@ def get_list_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di override_action = override_doctype.get(context.listview) if override_action: for action in override_action: + if callable(action.get("target")): + target_fn = action.get("target") + target = target_fn(barcode_doc, context) action["context"] = target action["target"] = target + if action.get("action") == "route": + action["route"] = action.get("route").format(target=target) return override_action - actions = listview.get(barcode_doc.doc.doctype, {}).get(context.listview, []) + # avoid mutating the global `listview` dict + list_actions = copy.deepcopy(listview) + actions = list_actions.get(barcode_doc.doc.doctype, {}).get(context.listview, []) for action in actions: action["context"] = target action["target"] = target + action["parent"] = barcode_doc.doc.name + action["parenttype"] = barcode_doc.doc.doctype return actions @@ -167,6 +226,8 @@ def get_form_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di if barcode_doc.doc.doctype == "Handling Unit": hu_details = get_handling_unit(barcode_doc.doc.name, context.frm) if context.frm == "Stock Entry": + if not context.doc: + context.doc = {"doctype": "Stock Entry"} target = get_stock_entry_item_details(context.doc, hu_details.item_code) target.warehouse = hu_details.warehouse elif context.frm in ("Putaway Rule", "Warranty Claim", "Item Price", "Quality Inspection"): @@ -197,7 +258,7 @@ def get_form_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di "dn_detail": hu_details.dn_detail, } ) - elif barcode_doc.doc.doctype == "Item": + elif barcode_doc.doc.doctype == "Item" and context.doc: if context.frm == "Stock Entry": target = get_stock_entry_item_details(context.doc, barcode_doc.doc.name) elif context.frm in ("Putaway Rule", "Warranty Claim", "Item Price", "Quality Inspection"): @@ -216,13 +277,26 @@ def get_form_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di "currency": frappe.defaults.get_user_default("Currency"), } ) + valuation_rate = get_valuation_rate(barcode_doc.doc.name, target.company, target.warehouse) + if valuation_rate.get("valuation_rate"): + target.valuation_rate = valuation_rate.valuation_rate + target.barcode = barcode_doc.barcode + elif barcode_doc.doc.doctype == "Warehouse" and context.frm == "Stock Reconciliation": + target = frappe._dict( + { + "doctype": context.frm, + "warehouse": barcode_doc.doc.name, + } + ) target.barcode = barcode_doc.barcode + else: + target = frappe._dict(**barcode_doc) + if not target: return [] beam_override = frappe.get_hooks("beam_frm") - if beam_override: override_doctype = beam_override.get(barcode_doc.doc.doctype) if override_doctype: @@ -235,16 +309,134 @@ def get_form_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di action["target"] = target.get(serialized_target[1]) return override_action - actions = frm.get(barcode_doc.doc.doctype, {}).get(context.frm, []) + # avoid mutating the global `frm` dict + form_actions = copy.deepcopy(frm) + actions = form_actions.get(barcode_doc.doc.doctype, {}).get(context.frm, []) for action in actions: action["context"] = target - if isinstance(action.get("target"), str) and "." in action.get("target"): - serialized_target = action.get("target").split(".") + target_value = action.get("target") + if isinstance(target_value, str) and "." in target_value: + serialized_target = target_value.split(".") action["target"] = target.get(serialized_target[1]) return actions +def get_serial_no(serial_no: str, parent_doctype: str | None = None) -> frappe._dict: + sle = DocType("Stock Ledger Entry") + snb = DocType("Serial and Batch Entry") + snb_bundle = DocType("Serial and Batch Bundle") + se_detail = DocType("Stock Entry Detail") + pr_item = DocType("Purchase Receipt Item") + pi_item = DocType("Purchase Invoice Item") + dn_item = DocType("Delivery Note Item") + + main_query = ( + frappe.qb.from_(sle) + .left_join(snb_bundle) + .on(sle.serial_and_batch_bundle == snb_bundle.name) + .left_join(snb) + .on(snb_bundle.name == snb.parent) + .left_join(se_detail) + .on((sle.voucher_type == "Stock Entry") & (sle.voucher_detail_no == se_detail.name)) + .left_join(pr_item) + .on((sle.voucher_type == "Purchase Receipt") & (sle.voucher_detail_no == pr_item.name)) + .left_join(pi_item) + .on((sle.voucher_type == "Purchase Invoice") & (sle.voucher_detail_no == pi_item.name)) + .left_join(dn_item) + .on((sle.voucher_type == "Delivery Note") & (sle.voucher_detail_no == dn_item.name)) + .select( + sle.item_code, + sle.actual_qty.as_("stock_qty"), + sle.company, + sle.voucher_no, + sle.posting_date, + sle.posting_time, + sle.stock_uom, + sle.voucher_type, + sle.voucher_detail_no, + sle.warehouse, + sle.serial_and_batch_bundle, + Coalesce(snb.serial_no, sle.serial_no).as_("serial_no"), + # Item details from whichever child table matches + Coalesce(se_detail.uom, pr_item.uom, pi_item.uom, dn_item.uom).as_("uom"), + Coalesce(se_detail.qty, pr_item.qty, pi_item.qty, dn_item.qty).as_("qty"), + Coalesce( + se_detail.conversion_factor, + pr_item.conversion_factor, + pi_item.conversion_factor, + dn_item.conversion_factor, + ).as_("conversion_factor"), + Coalesce(se_detail.idx, pr_item.idx, pi_item.idx, dn_item.idx).as_("idx"), + Coalesce(se_detail.item_name, pr_item.item_name, pi_item.item_name, dn_item.item_name).as_( + "item_name" + ), + Coalesce(se_detail.name, pr_item.name, pi_item.name, dn_item.name).as_("detail_name"), + # Special field for Purchase Receipt + Case() + .when(sle.voucher_type == "Purchase Receipt", pr_item.stock_qty) + .else_(None) + .as_("stock_qty_field"), + # For Packing Slip case - get delivery note item details + Case() + .when( + (dn_item.docstatus == 0) + & ((snb.serial_no == serial_no) | (dn_item.serial_no.like(f"%{serial_no}%"))), + dn_item.name, + ) + .else_(None) + .as_("dn_detail"), + ) + .where( + (sle.is_cancelled == 0) + & ( + (snb.serial_no == serial_no) + | (sle.serial_no.like(f"%{serial_no}%")) # Serial and Batch method # Direct field method + ) + ) + .groupby(sle.voucher_no, sle.voucher_detail_no) + .orderby(sle.posting_date, order=frappe.qb.desc) + .orderby(sle.posting_time, order=frappe.qb.desc) + .limit(1) + ) + + result = main_query.run(as_dict=True) + + if not result: + return + + sle_data = frappe._dict(result[0]) + + if sle_data.stock_qty_field is not None: + sle_data.stock_qty = sle_data.stock_qty_field + + if parent_doctype == "Packing Slip" and sle_data.dn_detail: + sle_data.dn_detail = sle_data.dn_detail + + sle_data.qty = 1.0 + + if sle_data.conversion_factor and sle_data.conversion_factor != 0: + sle_data.stock_qty = sle_data.qty / sle_data.conversion_factor + else: + sle_data.stock_qty = sle_data.qty + + sle_data.posting_datetime = ( + datetime.datetime( + sle_data.posting_date.year, sle_data.posting_date.month, sle_data.posting_date.day + ) + + sle_data.posting_time + ) + + sle_data.user = frappe.session.user + sle_data.pop("posting_date", None) + sle_data.pop("posting_time", None) + sle_data.pop("voucher_detail_no", None) + sle_data.pop("stock_qty_field", None) + sle_data.pop("detail_name", None) + + return sle_data + + listview = { "Handling Unit": { "Delivery Note": [ @@ -396,10 +588,73 @@ def get_form_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di {"action": "route", "doctype": "Warehouse", "field": "Warehouse", "target": "target"} ], }, + "Serial No": { + "Delivery Note": [ + {"action": "filter", "doctype": "Delivery Note", "field": "name", "target": "target"} + ], + "Item": [{"action": "route", "doctype": "Item", "field": "Item", "target": "target"}], + "Packing Slip": [ + {"action": "filter", "doctype": "Packing Slip", "field": "name", "target": "target"} + ], + "Purchase Invoice": [ + { + "action": "filter", + "doctype": "Purchase Invoice", + "field": "name", + "target": "target", + } + ], + "Purchase Receipt": [ + { + "action": "route", + "doctype": "Purchase Receipt", + "field": "Purchase Receipt", + "target": "target", + } + ], + "Putaway Rule": [ + {"action": "filter", "doctype": "Putaway Rule", "field": "item_code", "target": "target"}, + ], + "Quality Inspection": [ + { + "action": "filter", + "doctype": "Quality Inspection", + "field": "handling_unit", + "target": "target", + }, + ], + "Stock Entry": [ + {"action": "filter", "doctype": "Stock Entry", "field": "name", "target": "target"} + ], + "Stock Reconciliation": [ + { + "action": "filter", + "doctype": "Stock Reconciliation", + "field": "name", + "target": "target", + } + ], + }, } frm = { "Handling Unit": { + "Work Order": [ + { + "action": "add_or_associate", + "doctype": "Stock Entry", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry", + "field": "qty", + "target": "target.qty", + "context": "target", + }, + ], "Delivery Note": [ { "action": "add_or_associate", @@ -583,6 +838,15 @@ def get_form_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di ], }, "Item": { + "Work Order": [ + { + "action": "add_or_increment", + "doctype": "Stock Entry", + "field": "item_code", + "target": "target.item_code", + "context": "target", + }, + ], "Delivery Note": [ { "action": "add_or_increment", @@ -739,4 +1003,178 @@ def get_form_action(barcode_doc: frappe._dict, context: frappe._dict) -> list[di }, ], }, + "Serial No": { + "Delivery Note": [ + { + "action": "add_or_associate", + "doctype": "Delivery Note Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Delivery Note Item", + "field": "rate", + "target": "target.rate", + "context": "target", + }, + ], + "Item Price": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Item Price", + "field": "item_code", + "target": "target.item_code", + "context": "target", + }, + ], + "Packing Slip": [ + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "conversion_factor", + "target": "target.conversion_factor", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "pulled_quantity", + "target": "target.qty", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "rate", + "target": "target.rate", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "stock_qty", + "target": "target.stock_qty", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "warehouse", + "target": "target.warehouse", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "dn_detail", + "target": "target.dn_detail", + "context": "target", + }, + ], + "Purchase Invoice": [ + { + "action": "add_or_associate", + "doctype": "Purchase Invoice Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target", + }, + ], + "Putaway Rule": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Putaway Rule", + "field": "item_code", + "target": "target.item_code", + "context": "target", + }, + ], + "Quality Inspection": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Quality Inspection", + "field": "item_code", + "target": "target.item_code", + "context": "target", + }, + { + "action": "set_item_code_and_handling_unit", + "doctype": "Quality Inspection", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target", + }, + ], + "Stock Entry": [ + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "basic_rate", + "target": "target.valuation_rate", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "conversion_factor", + "target": "target.conversion_factor", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "s_warehouse", + "target": "target.warehouse", + "context": "target", + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "transfer_qty", + "target": "target.stock_qty", + "context": "target", + }, + ], + "Stock Reconciliation": [ + { + "action": "add_or_associate", + "doctype": "Stock Reconciliation Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target", + }, + ], + "Warranty Claim": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Warranty Claim", + "field": "item_code", + "target": "target.item_code", + "context": "target", + }, + { + "action": "set_item_code_and_handling_unit", + "doctype": "Warranty Claim", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target", + }, + ], + }, } diff --git a/beam/beam/scan/config.py b/beam/beam/scan/config.py index b323cae5..f8c32488 100644 --- a/beam/beam/scan/config.py +++ b/beam/beam/scan/config.py @@ -34,9 +34,14 @@ def get_scan_doctypes(): scannable_doctypes.add(key) [frm_doctypes.add(value) for value in values.keys()] + # TODO: should this be filtered against a specific company? + _scan_last = frappe.get_all("BEAM Settings", fields=["show_scan_output"]) + scan_last = _scan_last[0] if _scan_last else {"show_scan_output": False} + return { "scannable_doctypes": list(scannable_doctypes), "listview": list(listview_doctypes), "frm": list(frm_doctypes), "client": beam_client, + **scan_last, } diff --git a/beam/beam/scan/form.json b/beam/beam/scan/form.json new file mode 100644 index 00000000..3e1d3aad --- /dev/null +++ b/beam/beam/scan/form.json @@ -0,0 +1,342 @@ +{ + "Handling Unit": { + "Delivery Note": [ + { + "action": "add_or_associate", + "doctype": "Delivery Note Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Delivery Note Item", + "field": "rate", + "target": "target.rate", + "context": "target" + } + ], + "Item Price": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Item Price", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Packing Slip": [ + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "conversion_factor", + "target": "target.conversion_factor", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "pulled_quantity", + "target": "target.qty", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "rate", + "target": "target.rate", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "stock_qty", + "target": "target.stock_qty", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "warehouse", + "target": "target.warehouse", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Packing Slip Item", + "field": "dn_detail", + "target": "target.dn_detail", + "context": "target" + } + ], + "Purchase Invoice": [ + { + "action": "add_or_associate", + "doctype": "Purchase Invoice Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target" + } + ], + "Putaway Rule": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Putaway Rule", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Quality Inspection": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Quality Inspection", + "field": "item_code", + "target": "target.item_code", + "context": "target" + }, + { + "action": "set_item_code_and_handling_unit", + "doctype": "Quality Inspection", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target" + } + ], + "Sales Invoice": [ + { + "action": "add_or_associate", + "doctype": "Sales Invoice Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target" + } + ], + "Stock Entry": [ + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "basic_rate", + "target": "target.valuation_rate", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "conversion_factor", + "target": "target.conversion_factor", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "s_warehouse", + "target": "target.warehouse", + "context": "target" + }, + { + "action": "add_or_associate", + "doctype": "Stock Entry Detail", + "field": "transfer_qty", + "target": "target.stock_qty", + "context": "target" + } + ], + "Stock Reconciliation": [ + { + "action": "add_or_associate", + "doctype": "Stock Reconciliation Item", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target" + } + ], + "Warranty Claim": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Warranty Claim", + "field": "item_code", + "target": "target.item_code", + "context": "target" + }, + { + "action": "set_item_code_and_handling_unit", + "doctype": "Warranty Claim", + "field": "handling_unit", + "target": "target.handling_unit", + "context": "target" + } + ] + }, + "Item": { + "Delivery Note": [ + { + "action": "add_or_increment", + "doctype": "Delivery Note Item", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Item Price": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Item Price", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Packing Slip": [ + { + "action": "add_or_increment", + "doctype": "Packing Slip Item", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Purchase Invoice": [ + { + "action": "add_or_increment", + "doctype": "Purchase Invoice Item", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Purchase Receipt": [ + { + "action": "add_or_increment", + "doctype": "Purchase Receipt Item", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Putaway Rule": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Putaway Rule", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Quality Inspection": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Quality Inspection", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Sales Invoice": [ + { + "action": "add_or_increment", + "doctype": "Sales Invoice Item", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Stock Entry": [ + { + "action": "add_or_increment", + "doctype": "Stock Entry Detail", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Stock Reconciliation": [ + { + "action": "add_or_increment", + "doctype": "Stock Reconciliation Item", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ], + "Warranty Claim": [ + { + "action": "set_item_code_and_handling_unit", + "doctype": "Warranty Claim", + "field": "item_code", + "target": "target.item_code", + "context": "target" + } + ] + }, + "Warehouse": { + "Delivery Note": [ + { + "action": "set_warehouse", + "doctype": "Delivery Note Item", + "field": "warehouse", + "target": "target.warehouse", + "context": "target" + } + ], + "Purchase Invoice": [ + { + "action": "set_warehouse", + "doctype": "Purchase Invoice Item", + "field": "warehouse", + "target": "target.warehouse", + "context": "target" + } + ], + "Purchase Receipt": [ + { + "action": "set_warehouse", + "doctype": "Purchase Receipt Item", + "field": "warehouse", + "target": "target.warehouse", + "context": "target" + } + ], + "Sales Invoice": [ + { + "action": "set_warehouse", + "doctype": "Sales Invoice Item", + "field": "warehouse", + "target": "target.warehouse", + "context": "target" + } + ], + "Stock Entry": [ + { + "action": "set_warehouse", + "doctype": "Stock Entry", + "field": "warehouse", + "target": "target.warehouse", + "context": "target" + } + ], + "Stock Reconciliation": [ + { + "action": "set_warehouse", + "doctype": "Stock Reconciliation Item", + "field": "warehouse", + "target": "target.warehouse", + "context": "target" + } + ] + } +} diff --git a/beam/beam/scan/list.json b/beam/beam/scan/list.json new file mode 100644 index 00000000..708e595c --- /dev/null +++ b/beam/beam/scan/list.json @@ -0,0 +1,126 @@ +{ + "Handling Unit": { + "Delivery Note": [{ "action": "filter", "doctype": "Delivery Note", "field": "name", "target": "target" }], + "Item": [{ "action": "route", "doctype": "Item", "field": "Item", "target": "target" }], + "Packing Slip": [{ "action": "filter", "doctype": "Packing Slip", "field": "name", "target": "target" }], + "Purchase Invoice": [ + { + "action": "filter", + "doctype": "Purchase Invoice", + "field": "name", + "target": "target" + } + ], + "Purchase Receipt": [ + { + "action": "route", + "doctype": "Purchase Receipt", + "field": "Purchase Receipt", + "target": "target" + } + ], + "Putaway Rule": [{ "action": "filter", "doctype": "Putaway Rule", "field": "item_code", "target": "target" }], + "Quality Inspection": [ + { + "action": "filter", + "doctype": "Quality Inspection", + "field": "handling_unit", + "target": "target" + } + ], + "Sales Invoice": [{ "action": "filter", "doctype": "Sales Invoice", "field": "name", "target": "target" }], + "Stock Entry": [{ "action": "filter", "doctype": "Stock Entry", "field": "name", "target": "target" }], + "Stock Reconciliation": [ + { + "action": "filter", + "doctype": "Stock Reconciliation", + "field": "name", + "target": "target" + } + ] + }, + "Item": { + "Delivery Note": [ + { "action": "filter", "doctype": "Delivery Note Item", "field": "item_code", "target": "target" } + ], + "Item": [{ "action": "route", "doctype": "Item", "field": "Item", "target": "target" }], + "Item Price": [{ "action": "filter", "doctype": "Item Price", "field": "item_code", "target": "target" }], + "Packing Slip": [{ "action": "filter", "doctype": "Packing Slip Item", "field": "item_code", "target": "target" }], + "Purchase Invoice": [ + { + "action": "filter", + "doctype": "Purchase Invoice Item", + "field": "item_code", + "target": "target" + } + ], + "Purchase Receipt": [ + { + "action": "filter", + "doctype": "Purchase Receipt Item", + "field": "item_code", + "target": "target" + } + ], + "Putaway Rule": [{ "action": "filter", "doctype": "Putaway Rule", "field": "item_code", "target": "target" }], + "Quality Inspection": [ + { "action": "filter", "doctype": "Quality Inspection", "field": "item_code", "target": "target" } + ], + "Sales Invoice": [ + { "action": "filter", "doctype": "Sales Invoice Item", "field": "item_code", "target": "target" } + ], + "Stock Entry": [{ "action": "filter", "doctype": "Stock Entry Detail", "field": "item_code", "target": "target" }], + "Stock Reconciliation": [ + { + "action": "filter", + "doctype": "Stock Reconciliation Item", + "field": "item_code", + "target": "target" + } + ], + "Warranty Claim": [{ "action": "filter", "doctype": "Warranty Claim", "field": "item_code", "target": "target" }] + }, + "Warehouse": { + "Delivery Note": [ + { "action": "filter", "doctype": "Delivery Note Item", "field": "warehouse", "target": "target" } + ], + "Item": [ + { + "action": "filter", + "doctype": "Item Default", + "field": "default_warehouse", + "target": "target" + } + ], + "Packing Slip": [{ "action": "filter", "doctype": "Packing Slip Item", "field": "warehouse", "target": "target" }], + "Purchase Invoice": [ + { + "action": "filter", + "doctype": "Purchase Invoice Item", + "field": "warehouse", + "target": "target" + } + ], + "Purchase Receipt": [ + { + "action": "filter", + "doctype": "Purchase Receipt Item", + "field": "warehouse", + "target": "target" + } + ], + "Sales Invoice": [ + { "action": "filter", "doctype": "Sales Invoice Item", "field": "warehouse", "target": "target" } + ], + "Stock Entry": [{ "action": "filter", "doctype": "Stock Entry Detail", "field": "warehouse", "target": "target" }], + "Stock Reconciliation": [ + { + "action": "filter", + "doctype": "Stock Reconciliation Item", + "field": "warehouse", + "target": "target" + } + ], + "Warehouse": [{ "action": "route", "doctype": "Warehouse", "field": "Warehouse", "target": "target" }] + } +} diff --git a/beam/beam/scan/user_login.py b/beam/beam/scan/user_login.py new file mode 100644 index 00000000..e8b6a2a1 --- /dev/null +++ b/beam/beam/scan/user_login.py @@ -0,0 +1,47 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +from erpnext import get_default_company +from frappe.core.doctype.user.user import get_restricted_ip_list + +from beam.beam.scan import get_barcode_context + + +@frappe.whitelist(allow_guest=True) +def scan_login(barcode): + client_ip = ( + frappe.local.request.headers.get("X-Forwarded-For") or frappe.local.request.remote_addr + ) + + user = get_barcode_context(barcode) + if not user: + frappe.throw("Wrong barcode", title="Login Error") + + if user["doc"].doctype != "User": + frappe.throw("Wrong barcode", title="Login Error") + + employee = frappe.get_doc("Employee", {"user_id": user["doc"].name}) + company = employee.company or get_default_company() + + BEAMSettings = frappe.get_doc("BEAM Settings", {"company": company}) + if BEAMSettings.enable_scan_to_login == "Not Allowed": + frappe.throw("Login scanning is not allowed", title="Scanner Login Disabled") + + ip_list = get_restricted_ip_list(BEAMSettings) + if ip_list and not any(client_ip.startswith(ip) for ip in ip_list): + frappe.throw("Network not available", title="Login Error") + + user_doc = frappe.get_doc("User", user["doc"].name) + roles = [role.role for role in user_doc.get("roles")] + if BEAMSettings.enable_scan_to_login == "Mobile Users Only" and not "BEAM Mobile User" in roles: + frappe.throw("Not Beam mobile user", title="Login Error") + + try: + frappe.local.login_manager = frappe.auth.LoginManager() + frappe.local.login_manager.user = user_doc.name + frappe.local.login_manager.post_login() + except Exception as e: + frappe.throw(f"Error logging in: {str(e)}", title="Login Error") + + return {"success": True, "message": f"User {user_doc.name} logged in successfully"} diff --git a/beam/beam/zpl_layout.py b/beam/beam/zpl_layout.py new file mode 100644 index 00000000..3276f376 --- /dev/null +++ b/beam/beam/zpl_layout.py @@ -0,0 +1,525 @@ +# Copyright (c) 2026, AgriTheory and contributors +# For license information, please see license.txt + +#!/usr/bin/env python3 +""" +ZPL Label Layout Tools - Extract coordinates from PDF labels and generate ZPL templates. + +Usage: + python zpl_layout.py /path/to/label.pdf --dpi 300 --width 6 --height 4 --output ./output/ + python zpl_layout.py /path/to/label.pdf --rotate # Portrait to landscape +""" +import argparse +import json +import sys +from pathlib import Path + +import pdfplumber + + +def analyze_pdf_label( + pdf_path, target_dpi=300, label_width_inches=6, label_height_inches=4, rotate_90=True +): + """ + Extract text blocks with coordinates from PDF and convert to ZPL coordinates. + + Args: + pdf_path: Path to PDF file + target_dpi: Target printer DPI (default 300) + label_width_inches: Label width in inches (landscape) + label_height_inches: Label height in inches (landscape) + rotate_90: If True, rotate portrait PDF to landscape ZPL + + Returns: + Dictionary with text blocks, barcodes, and coordinate mappings + """ + results = { + "label_dimensions": { + "width_dots": label_width_inches * target_dpi, + "height_dots": label_height_inches * target_dpi, + "dpi": target_dpi, + }, + "text_blocks": [], + "barcode_regions": [], + "lines": [], + } + + with pdfplumber.open(pdf_path) as pdf: + page = pdf.pages[0] # First page + + # Get PDF dimensions + pdf_width = page.width + pdf_height = page.height + + print(f"PDF dimensions: {pdf_width} x {pdf_height} points") + print( + f"Target ZPL: {results['label_dimensions']['width_dots']} x {results['label_dimensions']['height_dots']} dots" + ) + print(f"Rotation: {'90° CW (portrait → landscape)' if rotate_90 else 'None'}\n") + + # Extract text with coordinates + words = page.extract_words(x_tolerance=3, y_tolerance=3, keep_blank_chars=False) + + # Group words into text blocks (by proximity) + text_blocks = [] + current_block = [] + last_y = None + y_tolerance = 15 # Points tolerance for same line + + for word in words: + x0, y0, x1, y1 = word["x0"], word["top"], word["x1"], word["bottom"] + text = word["text"] + + # Convert PDF coordinates to ZPL with optional rotation + # PDF: origin bottom-left, Y increases upward + # ZPL: origin top-left, Y increases downward + + if rotate_90: + # Rotate 90° clockwise: portrait PDF (4"x6") → landscape ZPL (6"x4") + # New X = old Y (from top) + # New Y = pdf_width - old X + pdf_y_from_top = pdf_height - y1 # Convert to top-origin + zpl_x = int((pdf_y_from_top / pdf_height) * results["label_dimensions"]["width_dots"]) + zpl_y = int(((pdf_width - x0) / pdf_width) * results["label_dimensions"]["height_dots"]) + else: + # No rotation + zpl_x = int((x0 / pdf_width) * results["label_dimensions"]["width_dots"]) + zpl_y = int(((pdf_height - y1) / pdf_height) * results["label_dimensions"]["height_dots"]) + + # Detect potential barcode patterns + is_barcode = False + if text.startswith("(") and ")" in text: + # GS1 application identifier format like (420) + is_barcode = True + elif ( + text.replace(" ", "").replace("-", "").isdigit() + and len(text.replace(" ", "").replace("-", "")) > 10 + ): + # Long numeric string - likely tracking/serial number + is_barcode = True + + block_info = { + "text": text, + "pdf_coords": {"x": x0, "y": pdf_height - y1, "x1": x1, "y1": pdf_height - y0}, + "zpl_coords": {"x": zpl_x, "y": zpl_y}, + "width": int((x1 - x0) / pdf_width * results["label_dimensions"]["width_dots"]), + "height": int((y1 - y0) / pdf_height * results["label_dimensions"]["height_dots"]), + "is_potential_barcode": is_barcode, + } + + if is_barcode: + results["barcode_regions"].append(block_info) + + text_blocks.append(block_info) + + results["text_blocks"] = text_blocks + + # Detect horizontal lines (dividers) + lines = page.lines + for line in lines: + if rotate_90: + # Rotate the line coordinates + is_horizontal = abs(line["x0"] - line["x1"]) < 2 # Vertical in PDF becomes horizontal in ZPL + if is_horizontal: + pdf_y_from_top = pdf_height - line["y0"] + zpl_y = int( + ((pdf_width - line["x0"]) / pdf_width) * results["label_dimensions"]["height_dots"] + ) + zpl_x0 = int((pdf_y_from_top / pdf_height) * results["label_dimensions"]["width_dots"]) + zpl_x1 = int( + ((pdf_height - line["y1"]) / pdf_height) * results["label_dimensions"]["width_dots"] + ) + results["lines"].append( + { + "type": "horizontal", + "zpl_coords": {"x0": min(zpl_x0, zpl_x1), "y": zpl_y, "x1": max(zpl_x0, zpl_x1)}, + "length": abs(zpl_x1 - zpl_x0), + } + ) + else: + if abs(line["y0"] - line["y1"]) < 2: # Horizontal line + zpl_y = int( + ((pdf_height - line["y0"]) / pdf_height) * results["label_dimensions"]["height_dots"] + ) + zpl_x0 = int((line["x0"] / pdf_width) * results["label_dimensions"]["width_dots"]) + zpl_x1 = int((line["x1"] / pdf_width) * results["label_dimensions"]["width_dots"]) + results["lines"].append( + { + "type": "horizontal", + "zpl_coords": {"x0": zpl_x0, "y": zpl_y, "x1": zpl_x1}, + "length": zpl_x1 - zpl_x0, + } + ) + + return results + + +def smart_group_text(text_blocks, width, height): + """ + Intelligently group text blocks into logical sections based on layout. + """ + sections = {} + + # Sort blocks by Y position (top to bottom) + sorted_blocks = sorted(text_blocks, key=lambda b: (b["zpl_coords"]["y"], b["zpl_coords"]["x"])) + + # Define regions (for 6"x4" = 1800x1200) + regions = { + "top_bar": (0, 0, width, 150), # Top header bar + "main_addresses": (0, 150, width, 500), # Address blocks + "divider_1": (0, 500, width, 550), + "shipping_info": (0, 550, width, 800), # Postal/carrier info + "divider_2": (0, 800, width, 850), + "product_details": (0, 850, width, 1050), # PO/SKU/Description + "bottom_barcodes": (0, 1050, width, height), # Bottom barcode area + } + + for region_name, (x0, y0, x1, y1) in regions.items(): + sections[region_name] = [] + for block in sorted_blocks: + bx = block["zpl_coords"]["x"] + by = block["zpl_coords"]["y"] + if x0 <= bx < x1 and y0 <= by < y1: + sections[region_name].append(block) + + return sections + + +def generate_layout_map(sections, width, height): + """ + Generate a visual ASCII layout map. + """ + # Create a grid (scaled down) + grid_width = 90 # chars + grid_height = 24 # lines + scale_x = width / grid_width + scale_y = height / grid_height + + grid = [[" " for _ in range(grid_width)] for _ in range(grid_height)] + + # Draw borders + for x in range(grid_width): + grid[0][x] = "-" + grid[grid_height - 1][x] = "-" + for y in range(grid_height): + grid[y][0] = "|" + grid[y][grid_width - 1] = "|" + + # Place text blocks + for section_name, blocks in sections.items(): + for block in blocks: + x = int(block["zpl_coords"]["x"] / scale_x) + y = int(block["zpl_coords"]["y"] / scale_y) + if 1 < x < grid_width - 1 and 1 < y < grid_height - 1: + if block["is_potential_barcode"]: + grid[y][x] = "█" + else: + grid[y][x] = "·" + + return "\n".join("".join(row) for row in grid) + + +def print_analysis(analysis, sections): + """Print human-readable analysis.""" + print("=" * 80) + print("LABEL ANALYSIS - ZPL COORDINATE MAPPING") + print("=" * 80) + print( + f"\nLabel dimensions: {analysis['label_dimensions']['width_dots']} x {analysis['label_dimensions']['height_dots']} dots @ {analysis['label_dimensions']['dpi']} DPI" + ) + + print("\n" + "-" * 80) + print("SECTIONS") + print("-" * 80) + + for section_name, blocks in sections.items(): + if blocks: + print(f"\n### {section_name.upper().replace('_', ' ')}") + for block in blocks: + print(f" [{block['zpl_coords']['x']:4d}, {block['zpl_coords']['y']:4d}] \"{block['text']}\"") + + print("\n" + "-" * 80) + print("HORIZONTAL LINES (Dividers)") + print("-" * 80) + for line in analysis["lines"]: + print( + f" Y={line['zpl_coords']['y']:4d}, X=[{line['zpl_coords']['x0']:4d} to {line['zpl_coords']['x1']:4d}], Length={line['length']} dots" + ) + + print("\n" + "-" * 80) + print("BARCODE REGIONS") + print("-" * 80) + for barcode in analysis["barcode_regions"]: + print( + f" [{barcode['zpl_coords']['x']:4d}, {barcode['zpl_coords']['y']:4d}] \"{barcode['text']}\" (size: {barcode['width']}x{barcode['height']} dots)" + ) + + +def generate_zpl_template(analysis, sections): + """Generate a production-ready ZPL template with proper structure.""" + lines = [] + + # Header + dpi = analysis["label_dimensions"]["dpi"] + width_dots = analysis["label_dimensions"]["width_dots"] + height_dots = analysis["label_dimensions"]["height_dots"] + width_inches = width_dots / dpi + height_inches = height_dots / dpi + lines.append("{# Shipping Label - " + f'{width_inches}x{height_inches}" @ {dpi} DPI #}}') + lines.append( + "{% set label = zebra_zpl_label(width=" + + str(width_dots) + + ", length=" + + str(height_dots) + + ", dpi=" + + str(dpi) + + ") -%}" + ) + lines.append("") + lines.append("^XA {# Start Format #}") + lines.append(f"^PW{width_dots} " + "{# Print Width: " + str(width_dots) + " dots #}") + lines.append(f"^LL{height_dots} " + "{# Label Length: " + str(height_dots) + " dots #}") + lines.append("") + + # Top section - may contain store number or routing info + top_blocks = sections.get("top_bar", []) + if top_blocks: + lines.append("{# === TOP BAR SECTION === #}") + for block in sorted(top_blocks, key=lambda b: b["zpl_coords"]["x"]): + x, y = block["zpl_coords"]["x"], block["zpl_coords"]["y"] + text = block["text"] + lines.append(f"^FO{x},{y}^A0N,40,40^FD{text}^FS") + lines.append("") + + # Main address section + addr_blocks = sections.get("main_addresses", []) + if addr_blocks: + lines.append("{# === ADDRESS SECTION === #}") + lines.append("{# Ship From (Left Side) #}") + lines.append("^FO50,150^A0N,35,35^FDShip From:^FS") + lines.append("^FO50,200^A0N,28,28^FB700,5,0,L,0^FD{{ doc.ship_from_name }}^FS") + lines.append("^FO50,250^A0N,28,28^FB700,5,0,L,0^FD{{ doc.ship_from_address }}^FS") + lines.append("") + lines.append("{# Ship To (Right Side) #}") + mid_x = analysis["label_dimensions"]["width_dots"] / 2 + lines.append("^FO950,150^A0N,35,35^FDShip To:^FS") + lines.append("^FO950,200^A0N,28,28^FB800,5,0,L,0^FD{{ doc.ship_to_name }}^FS") + lines.append("^FO950,250^A0N,28,28^FB800,5,0,L,0^FD{{ doc.ship_to_address }}^FS") + lines.append("") + + # Horizontal divider + lines.append("{# === DIVIDER LINE === #}") + lines.append("^FO50,500^GB1700,3,3^FS") + lines.append("") + + # Shipping info section (postal code barcode + carrier info) + ship_blocks = sections.get("shipping_info", []) + if ship_blocks: + lines.append("{# === SHIPPING INFORMATION === #}") + lines.append("{# Postal Code Barcode (Left) #}") + lines.append("^FO50,520^A0N,25,25^FD(420) Ship to Postal Code^FS") + lines.append("^FO100,560^BY3^BCN,100,Y,N^FD(420){{ doc.ship_to_zip }}^FS") + lines.append("^FO120,680^A0N,30,30^FD(420) {{ doc.ship_to_zip }}^FS") + lines.append("") + lines.append("{# Carrier Information (Right) #}") + lines.append("^FO950,520^A0N,28,28^FDCarrier: {{ doc.carrier }}^FS") + lines.append("^FO950,560^A0N,28,28^FDPRO#: {{ doc.tracking_number }}^FS") + lines.append("^FO950,600^A0N,28,28^FDB/L#: {{ doc.bill_of_lading }}^FS") + lines.append( + "^FO950,640^A0N,28,28^FDNumber of Cartons: {{ doc.carton_number }} of {{ doc.total_cartons }}^FS" + ) + lines.append("") + + # Second divider + lines.append("{# === DIVIDER LINE === #}") + lines.append("^FO50,800^GB1700,3,3^FS") + lines.append("") + + # Product details section + prod_blocks = sections.get("product_details", []) + if prod_blocks: + lines.append("{# === PRODUCT DETAILS === #}") + lines.append("{# Left Column #}") + lines.append("^FO50,820^A0N,28,28^FDPO #: {{ doc.po_number }}^FS") + lines.append("^FO50,860^A0N,28,28^FDVendor Part #: {{ doc.vendor_part_number }}^FS") + lines.append("^FO50,900^A0N,28,28^FDUPC #: {{ doc.upc }}^FS") + lines.append("^FO50,940^A0N,28,28^FDCarton Qty: {{ doc.carton_qty }}^FS") + lines.append("") + lines.append("{# Right Column #}") + lines.append("^FO950,820^A0N,28,28^FDSKU #: {{ doc.sku }}^FS") + lines.append("^FO950,860^A0N,28,28^FDSize: {{ doc.size }}^FS") + lines.append("^FO950,900^A0N,28,28^FDColor: {{ doc.color }}^FS") + lines.append("^FO950,940^A0N,28,28^FDDescription: {{ doc.description }}^FS") + lines.append("") + + # Bottom barcode section (SSCC-18) + barcode_blocks = sections.get("bottom_barcodes", []) + if barcode_blocks: + lines.append("{# === BOTTOM SSCC BARCODE === #}") + lines.append("^FO200,1050^A0N,25,25^FDSSCC^FS") + lines.append("^FO150,1090^BY3^BCN,100,Y,N^FD{{ doc.sscc_barcode }}^FS") + lines.append("") + + # End format + lines.append("^XZ {# End Format #}") + + return "\n".join(lines) + + +def process_label(pdf_path, output_dir=None, dpi=300, width=6, height=4, rotate=True): + """ + Process a PDF label and generate ZPL template. + + Args: + pdf_path: Path to PDF file + output_dir: Directory to save outputs (default: creates 'output' next to PDF) + dpi: Target printer DPI + width: Label width in inches + height: Label height in inches + rotate: Whether to rotate 90 degrees + + Returns: + Dictionary with analysis results + """ + pdf_path = Path(pdf_path) + + if not pdf_path.exists(): + raise FileNotFoundError(f"PDF not found: {pdf_path}") + + # Determine output directory + if output_dir is None: + output_dir = pdf_path.parent / "output" + else: + output_dir = Path(output_dir) + + output_dir.mkdir(parents=True, exist_ok=True) + + print(f"\n{'='*80}") + print(f"Processing: {pdf_path.name}") + print(f"{'='*80}\n") + + # Analyze PDF + analysis = analyze_pdf_label( + str(pdf_path), + target_dpi=dpi, + label_width_inches=width, + label_height_inches=height, + rotate_90=rotate, + ) + + # Smart grouping + sections = smart_group_text( + analysis["text_blocks"], + analysis["label_dimensions"]["width_dots"], + analysis["label_dimensions"]["height_dots"], + ) + + # Print layout map + print("\nVISUAL LAYOUT MAP") + print("-" * 80) + layout_map = generate_layout_map( + sections, analysis["label_dimensions"]["width_dots"], analysis["label_dimensions"]["height_dots"] + ) + print(layout_map) + print() + + # Print analysis + print_analysis(analysis, sections) + + print("\n" + "=" * 80) + print("PRODUCTION-READY ZPL TEMPLATE") + print("=" * 80) + template = generate_zpl_template(analysis, sections) + print(template) + + # Save outputs + base_name = pdf_path.stem.lower().replace(" ", "_") + + # Save template + template_path = output_dir / f"{base_name}.zpl" + with open(template_path, "w") as f: + f.write(template) + print(f"\n✓ ZPL Template: {template_path}") + + # Save layout map + layout_path = output_dir / f"{base_name}_layout_map.txt" + with open(layout_path, "w") as f: + f.write(layout_map) + print(f"✓ Layout Map: {layout_path}") + + # Save detailed analysis + analysis_path = output_dir / f"{base_name}_analysis.json" + with open(analysis_path, "w") as f: + json.dump( + { + "label_dimensions": analysis["label_dimensions"], + "sections": { + k: [ + {"text": b["text"], "coords": b["zpl_coords"], "is_barcode": b["is_potential_barcode"]} + for b in v + ] + for k, v in sections.items() + if v + }, + "lines": analysis["lines"], + }, + f, + indent=2, + ) + print(f"✓ Analysis JSON: {analysis_path}") + + return analysis + + +def main(): + parser = argparse.ArgumentParser( + description="Extract coordinates from PDF labels and generate ZPL templates", + formatter_class=argparse.RawDescriptionHelpFormatter, + epilog=""" +Examples: + # Basic usage (assumes portrait PDF to landscape 6x4 @ 300 DPI) + python zpl_layout.py /path/to/label.pdf + + # Specify output directory + python zpl_layout.py /path/to/label.pdf --output ./my_output/ + + # Custom dimensions (no rotation) + python zpl_layout.py /path/to/label.pdf --width 4 --height 6 --dpi 203 --no-rotate + + # Process multiple PDFs + for pdf in label_spec/*/label.pdf; do + python zpl_layout.py "$pdf" + done + """, + ) + + parser.add_argument("pdf", help="Path to PDF label file") + parser.add_argument("--output", "-o", help="Output directory (default: ./output/ next to PDF)") + parser.add_argument("--dpi", type=int, default=300, help="Target printer DPI (default: 300)") + parser.add_argument("--width", type=float, default=6, help="Label width in inches (default: 6)") + parser.add_argument("--height", type=float, default=4, help="Label height in inches (default: 4)") + parser.add_argument( + "--no-rotate", action="store_true", help="Do not rotate portrait to landscape" + ) + + args = parser.parse_args() + + try: + process_label( + args.pdf, + output_dir=args.output, + dpi=args.dpi, + width=args.width, + height=args.height, + rotate=not args.no_rotate, + ) + print(f"\n{'='*80}") + print("Processing complete!") + print(f"{'='*80}\n") + except Exception as e: + print(f"\nError: {e}\n", file=sys.stderr) + sys.exit(1) + + +if __name__ == "__main__": + main() diff --git a/beam/customize.py b/beam/customize.py deleted file mode 100644 index 45b0a066..00000000 --- a/beam/customize.py +++ /dev/null @@ -1,45 +0,0 @@ -# Copyright (c) 2025, AgriTheory and contributors -# For license information, please see license.txt - -import json -from pathlib import Path - -import frappe - - -def load_customizations(): - customizations_directory = Path().cwd().parent / "apps" / "beam" / "beam" / "beam" / "custom" - files = list(customizations_directory.glob("**/*.json")) - for file in files: - customizations = json.loads(Path(file).read_text()) - for field in customizations.get("custom_fields"): - if field.get("module") != "BEAM": - continue - existing_field = frappe.get_value("Custom Field", field.get("name")) - custom_field = ( - frappe.get_doc("Custom Field", field.get("name")) - if existing_field - else frappe.new_doc("Custom Field") - ) - field.pop("modified") - {custom_field.set(key, value) for key, value in field.items()} - custom_field.flags.ignore_permissions = True - custom_field.flags.ignore_version = True - custom_field.save() - for prop in customizations.get("property_setters"): - if prop.get("module") != "BEAM": - continue - property_setter = frappe.get_doc( - { - "name": prop.get("name"), - "doctype": "Property Setter", - "doctype_or_field": prop.get("doctype_or_field"), - "doc_type": prop.get("doc_type"), - "field_name": prop.get("field_name"), - "property": prop.get("property"), - "value": prop.get("value"), - "property_type": prop.get("property_type"), - } - ) - property_setter.flags.ignore_permissions = True - property_setter.insert() diff --git a/beam/docs/assets/beam_settings.png b/beam/docs/assets/beam_settings.png index 1ea936a3..f0716ed9 100644 Binary files a/beam/docs/assets/beam_settings.png and b/beam/docs/assets/beam_settings.png differ diff --git a/beam/docs/demand.md b/beam/docs/demand.md new file mode 100644 index 00000000..b79a6bca --- /dev/null +++ b/beam/docs/demand.md @@ -0,0 +1,35 @@ + + +# Demand + +
+ Rohan Bansal, Myuddin Khatri, Tyler Matteson, and ViralKansodiya-Fosserp 2024-09-02 +
+ + +This feature computes the what Items are needed and where they are available. + +### Demand Map + +Demand increases based on the following factors: +- When a Sales Order is submitted +- When a Work Order is submitted + +Demand decreases based on the following factors: +- When a Sales Order is either: + - fulfilled (via a Sales Invoice or a Delivery Note) + - cancelled + - closed + - put on hold +- When a Work Order is either: + - completed (via a Stock Entry) + - cancelled + - closed + - stopped + + diff --git a/beam/docs/form.md b/beam/docs/form.md index 52ff652d..d6062ff6 100644 --- a/beam/docs/form.md +++ b/beam/docs/form.md @@ -3,6 +3,11 @@ For license information, please see license.txt--> # Form +
+ Rohan Bansal, Heather Kusmierz, and Tyler Matteson 2025-05-28 +
+ + The result of scanning a barcode in the form depends on several factors: - Is the barcode recognized? @@ -14,7 +19,6 @@ For example, when an Item is scanned while viewing a Delivery Note record, it wi |-----------------|-----------------------|--------|--------| |Item|Delivery Note|add_or_increment|item_code| -Beam uses a [decision matrix](./matrix.md) to decide what action to take based on what kind of doctype has been scanned. +BEAM uses a [decision matrix](./matrix.md) to decide what action to take based on what kind of doctype has been scanned. Custom actions and client side functions can be added by using [hooks](./hooks.md). - diff --git a/beam/docs/handling_unit.md b/beam/docs/handling_unit.md index d13521c3..8d572804 100644 --- a/beam/docs/handling_unit.md +++ b/beam/docs/handling_unit.md @@ -3,16 +3,21 @@ For license information, please see license.txt--> # Handling Unit +
+ Rohan Bansal, github-actions, Heather Kusmierz, Tyler Matteson, and Francisco Roldán 2025-05-28 +
+ + A Handling Unit is an abstraction for tracking quantities of items that are moved or stored together. It does not replace Batch or Serial numbers, the manufacture of an Item, or the functionality of the Product Bundle, but can supplement these as a way of conveniently grabbing information that would otherwise require a lot of keystrokes to enter. -By assigning a unique ID to the Handling Unit, it is possible to capture via scanner the item, net quantity, unit of measure and timestamp of the previous transaction, and then act upon that information in context, according to the [decision matrix](./matrix.md). Beam adds a new doctype, Handling Unit, to implement this functionality in ERPNext. +By assigning a unique ID to the Handling Unit, it is possible to capture via scanner the item, net quantity, unit of measure and timestamp of the previous transaction, and then act upon that information in context, according to the [decision matrix](./matrix.md). BEAM adds a new doctype, Handling Unit, to implement this functionality in ERPNext. ![Screen shot of the Handling Unit doctype listview. The list shows several new Handling Units that were created for items received via a Purchase Receipt.](./assets/handling_unit_list.png) ## Listviews Generally scanning a Handling Unit in a list view will filter to show all the transactions of the doctype with the appropriate Handling Unit. -## Purchase Receipt +## Purchase Receipt For Purchase Receipts, Handling Units are generated and cannot be supplied by the user. | Item | Warehouse | Handling Unit | Quantity | @@ -76,13 +81,29 @@ When material is transferred from one warehouse to another, it will generate a n | Cocoplum | Work In Progress | 456 | 20 Ea | -When cancelling a Stock Entry, the user will be given an option to re-combine or let handling units remain tracked separately. +#### Cancelling Material Transfer Entries + +When cancelling a Material Transfer Stock Entry (including Send to Subcontractor and Material Transfer for Manufacture), a dialog appears asking whether to recombine handling units or keep them tracked separately. ![Screen shot of the recombine dialog](./assets/recombine.png) +The dialog shows each source handling unit along with its corresponding target handling unit that was created during the transfer. By default, all rows are pre-selected for recombination (the recommended action). + +**Recombine (Default):** When rows are selected and "Cancel and Recombine" is clicked: +- The source and target handling units are merged back together +- The original handling unit retains its full quantity as if the transfer never happened +- The target handling unit is removed from inventory +- This is the typical choice when correcting errors or undoing temporary transfers + +**Keep Separate:** When rows are unchecked before clicking "Cancel and Recombine": +- Both handling units remain in the system with their respective quantities +- Stock ledger entries are created to restore the quantities in both warehouses +- The handling units continue to be tracked independently +- Useful when you want to maintain the split for future reference or traceability + ### Repack and Manufacture -In the case of a Repack, Material Issue or Material Consumption for Manufacture, a new Handling Unit is generated for the new quantities. +In the case of a Repack, Material Issue or Material Consumption for Manufacture, a new Handling Unit is generated for the new quantities. | Item | Warehouse | Handling Unit | Quantity | | ---------------- | ------------------ | -------------- | --------------:| @@ -98,6 +119,22 @@ In a case where less than the total quantity associated with a Handling Unit is | Cocoplum Puree | Work In Progress | 012 | 1 liter | | Cocoplum | Scrap | | 1 Ea | +#### Cancelling Repack and Manufacture Entries + +Similar to Material Transfer entries, when cancelling a Repack or Manufacture Stock Entry, a dialog appears to choose the recombine behavior. The dialog shows each consumed (source) handling unit paired with its corresponding produced (target) handling unit. All rows are pre-selected for recombination by default. + +**Recombine (Default):** When rows are selected: +- The consumed handling unit is restored to its original quantity +- The produced handling unit is removed from inventory +- The transformation is completely reversed +- Best for correcting data entry errors or voiding incorrect manufacturing entries + +**Keep Separate:** When rows are unchecked: +- The consumed handling unit receives its quantity back +- The produced handling unit also retains its quantity +- Both handling units coexist in inventory +- Useful for maintaining audit trails when a production run needs to be reversed but you want to preserve the separate handling unit records for compliance or tracking purposes + #### BOM Scrap Item In a Manufacturing or Repack Stock Entry, scrap items can be toggled to create a Handling Unit corresponding with their scrap quantity. This can be changed after a BOM is submitted. @@ -117,7 +154,7 @@ In both these cases, there is no offsetting movement or creation of items. | Cocoplum | Work In Progress | 123 | -20 Ea | ### Material Receipt -In the case of Material Receipt, a new Handling Unit is generated for each item. +In the case of Material Receipt, a new Handling Unit is generated for each item. | Item | Warehouse | Handling Unit | Quantity | | ---------------- | ------------------ | -------------- | --------------:| diff --git a/beam/docs/hooks.md b/beam/docs/hooks.md index 04fb9f54..66a2963e 100644 --- a/beam/docs/hooks.md +++ b/beam/docs/hooks.md @@ -1,9 +1,14 @@ -# Extending Beam With Custom Hooks +# Extending BEAM With Custom Hooks -Beam can be extended by adding configurations to your application's `hooks.py`. +
+ Rohan Bansal, Heather Kusmierz, and Tyler Matteson 2025-05-28 +
+ + +BEAM can be extended by adding configurations to your application's `hooks.py`. To make scanning available on a custom doctype, add a table field for "Item Barcode" directly in the doctype or via customize form. Then add a key that is a peer with "Item" in the example below. @@ -40,7 +45,7 @@ beam_frm = { } } ``` -To add a custom JavaScript function, add the following hook to your application's `hooks.py`. An example implementation is available in the source code. +To add a custom JavaScript function, add the following hook to your application's `hooks.py`. An example implementation is available in the source code. ```python # hooks.py @@ -49,4 +54,4 @@ beam_client = { "show_message": "custom_app.show_message" } -``` \ No newline at end of file +``` diff --git a/beam/docs/hu_traceability_report.md b/beam/docs/hu_traceability_report.md index 2752d6e5..12080b4e 100644 --- a/beam/docs/hu_traceability_report.md +++ b/beam/docs/hu_traceability_report.md @@ -3,6 +3,11 @@ For license information, please see license.txt--> # Handling Unit Traceability Report +
+ Rohan Bansal, Heather Kusmierz, and Tyler Matteson 2025-02-14 +
+ + The Handling Unit Traceability report provides a simple interface to track a Handling Unit over its life cycle through your company's processes. Filters for the Handling Unit ID, Delivery Note name, and Sales Invoice name allow for fine-tuning of the report's results. ![Screen shot of the Handling Unit Traceability report's filter fields, including Handling Unit, Delivery Note, and Sales Invoice](./assets/hu_trace_filters.png) diff --git a/beam/docs/index.md b/beam/docs/index.md index 89dbbc60..c2a7d6db 100644 --- a/beam/docs/index.md +++ b/beam/docs/index.md @@ -1,13 +1,17 @@ -# Beam +# BEAM -Beam is a general purpose 2D barcode scanning application for ERPNext. +
+ Rohan Bansal, Heather Kusmierz, Tyler Matteson, and Francisco Roldán 2025-05-28 +
+ +BEAM is a general purpose barcode scanning application for ERPNext. ## What does this application do? -Beam allows a user to scan a 2D barcode from either a listview or a form view, then helps enter data that would otherwise require numerous keystrokes. Unlike ERPNext's built-in barcode scanning, Beam expects the user to have a hardware barcode scanner connected to their device. +BEAM allows a user to scan a 2D or QR barcode from either a listview or a form view, then helps enter data that would otherwise require numerous keystrokes. Unlike ERPNext's built-in barcode scanning, BEAM expects the user to have a hardware barcode scanner connected to their device. For example, if the user scans a barcode associated with an Item in the Item listview, it will take them to that item's record. @@ -23,22 +27,42 @@ If the user scans an Item in a Delivery Note, it will populate everything it kno Read more about [how scanning in form views works](./form.md). -## Beam Settings +## BEAM Settings -Beam's version 15 introduced a new Beam Settings document to allow users to opt in or out of features in the app. Settings are unique on a per-company basis and are automatically generated (with default options) during certain related transactions if a Beam Settings document doesn't already exist for the company. Related transactions include submission of a Purchase Receipt, Purchase Invoice, or Stock Entry. +Version 15 introduced a new BEAM Settings document to allow users to opt in or out of features in the app. Settings are unique on a per-company basis and are automatically generated (with default options) during certain related transactions if a BEAM Settings document doesn't already exist for the company. Related transactions include submission of a Purchase Receipt, Purchase Invoice, or Stock Entry. -![Screen shot of the Beam Settings document with a field for company and a check box to enable handling units.](./assets/beam_settings.png) +![Screen shot of the Beam Settings document for the fictitious Ambrosia Pie Company with Barcode Font size of 12, Enable Handling Units checked, Ignore Drop Shipped Items in Demand unchecked, and fields for Receiving Workstation and Shipping Workstation.](./assets/beam_settings.png) Settings options include: - **Company:** the company in ERPNext to apply the given settings to. One Beam Settings document may exist for each company in the system +- **Barcode Font Size:** (default 12) the font size to use when printing barcodes - **Enable Handling Units:** (default checked) enables the generation of Handling Units (see What is a Handling Unit section for more information) +- **Ignore Drop Shipped Items in Demand:** (default unchecked) if checked, calculated demand from Sales Orders will ignore any items marked to be shipped by the supplier (drop shipped) + +### QR Code Settings + +- **QR Scale:** (default 8) the module size in pixels used when generating QR code images — larger values produce a bigger image +- **QR Border:** (default 4) the quiet zone border size in modules surrounding the QR code +- **QR Error Correct:** (default M) the error correction level encoded into QR codes; options are L (7%), M (15%), Q (25%), and H (30%) — higher levels allow the code to remain scannable even if partially damaged, at the cost of a denser image + +### Barcode Generation + +The Barcode Generation section controls which document types receive an automatically generated Code128 barcode when saved. Any document type that has a Barcodes table (using the Item Barcode child doctype) is listed here. Checked items have auto-generation **enabled**; unchecked items are shown with a strikethrough and will not have barcodes generated on save. +By default, **Item** and **Warehouse** are enabled. If a Code128 barcode already exists on a document, a new one will never be generated regardless of this setting. If you customize another doctype by adding a Item Barcode table, automatic generation can be configured here but still requires a `doc_event` hook to trigger, which can be configured in your app's `hooks.py` or in a Server Script. +```python +"Asset": { + "validate": [ + "beam.beam.barcodes.create_beam_barcode", + ] +}, +``` ## What is a Handling Unit? A Handling Unit is the combination of a container, any packaging material, and the items within or on it. This could be a pallet of raw materials used in a manufacturing process, a crate containing several other Handling Units, or a delivery vehicle transporting the crates and pallets. -Handling Units have unique, scannable identification numbers that are used in any stock transaction involving the items contained within the unit. The ID allows the user to reference everything about the stock transaction, saved from previous transactions. It also enables you to track the Handling Unit throughout its life cycle. The Beam application includes a [Handling Unit Traceability report](./hu_traceability_report.md) to summarize the transactions, related documents, quantities, and warehouses that involved a given Handling Unit. +Handling Units have unique, scannable identification numbers that are used in any stock transaction involving the items contained within the unit. The ID allows the user to reference everything about the stock transaction, saved from previous transactions. It also enables you to track the Handling Unit throughout its life cycle. The BEAM application includes a [Handling Unit Traceability report](./hu_traceability_report.md) to summarize the transactions, related documents, quantities, and warehouses that involved a given Handling Unit. A Handling Unit is generated when materials are received or created in the manufacturing process. @@ -46,7 +70,7 @@ Read more [about Handling Units here](./handling_unit.md). ## Installation and Customization -Beam comes packed with features, but can be extended with custom hooks both on the server side and in the client as needed. See the following pages for detailed instructions on installing and customizing the application: +BEAM comes packed with features, but can be extended with custom hooks both on the server side and in the client as needed. See the following pages for detailed instructions on installing and customizing the application: - [Installation](https://github.com/agritheory/beam) - [Customization](./hooks.md) @@ -59,7 +83,7 @@ Warehouses may also have unique barcodes associated with them. The user can navi ## Print Server Integration -Beam offers the ability to print to raw input printers like Zebra printers directly from the browser. Also included are several debugging and example print formats. For more details about configuring this, see the [print server section](./print_server.md). +BEAM offers the ability to print to raw input printers like Zebra printers directly from the browser. Also included are several debugging and example print formats. For more details about configuring this, see the [print server section](./print_server.md). ### Zebra Printing diff --git a/beam/docs/listview.md b/beam/docs/listview.md index b3fe3a31..1dc92136 100644 --- a/beam/docs/listview.md +++ b/beam/docs/listview.md @@ -3,6 +3,11 @@ For license information, please see license.txt--> # Listview +
+ Rohan Bansal, Heather Kusmierz, and Tyler Matteson 2025-05-28 +
+ + The result of scanning a barcode in the listview depends on several factors: - Is the barcode recognized? @@ -22,7 +27,6 @@ Another example: If an Item is scanned while viewing the Purchase Receipt list, |Item|Purchase Receipt|filter|item_code| -Beam uses a [decision matrix](./matrix.md) to decide what action to take based on what kind of doctype has been scanned. +BEAM uses a [decision matrix](./matrix.md) to decide what action to take based on what kind of doctype has been scanned. Custom actions and client side functions can be added by using [hooks](./hooks.md) - diff --git a/beam/docs/matrix.md b/beam/docs/matrix.md index 6133f078..5fe860e2 100644 --- a/beam/docs/matrix.md +++ b/beam/docs/matrix.md @@ -2,6 +2,11 @@ For license information, please see license.txt--> # Listview Actions + +
+ Rohan Bansal and Tyler Matteson 2025-05-28 +
+ | Scanned Doctype | Listview | Action | Target | |-----------------|-----------------------|--------|--------| |Handling Unit|Delivery Note|route|Delivery Note| @@ -36,7 +41,7 @@ For license information, please see license.txt--> |Warehouse|Stock Reconciliation|filter|warehouse| |Warehouse|Warehouse|route|Warehouse| - --- + --- # Form Actions | Scanned Doctype | Form | Action | Target | diff --git a/beam/docs/print_server.md b/beam/docs/print_server.md index 28bf6fed..542bae0c 100644 --- a/beam/docs/print_server.md +++ b/beam/docs/print_server.md @@ -3,6 +3,11 @@ For license information, please see license.txt--> # Print Server +
+ Rohan Bansal, Heather Kusmierz, and Tyler Matteson 2025-02-14 +
+ + There are several steps to get a print server connected in ERPNext. 1. First, the `pycups` dependency needs to be installed on the system, which in turn depends on the CUPS project's `libcups` library. See the following links for installation instructions: @@ -15,6 +20,8 @@ There are several steps to get a print server connected in ERPNext. ![Screen shot of the Network Printer Settings document fields, including Name, Printer Name, Server IP, and Port.](./assets/network_printer_settings.png) +The **Printer Name** field is an autocomplete that queries the configured CUPS server and displays available printers by their CUPS identifier, with the make/model and location shown as secondary text. Selecting a printer automatically fills in the **Printer Location** field from CUPS. The location can be edited freely — saving the record pushes the updated value back to CUPS, keeping the two in sync. The **Printer Type** field (`General Purpose` or `Label / RAW`) can be used to distinguish IPP or PDF printers from ZPL/raw label printers. + --- A convenient Print Handling Unit button on relevant doctypes enables the user to print new Handling Unit labels directly from the ERPNext user interface. diff --git a/beam/docs/testing.md b/beam/docs/testing.md index 04d877a2..9bf47c80 100644 --- a/beam/docs/testing.md +++ b/beam/docs/testing.md @@ -3,6 +3,11 @@ For license information, please see license.txt--> # Testing +
+ Rohan Bansal, Heather Kusmierz, and Tyler Matteson 2025-02-14 +
+ + ## Simulating a Scanner Open the browser console. This assumes a barcode of `'9968934975826708157'` which must be sent as a string. diff --git a/beam/docs/zebra_printing.md b/beam/docs/zebra_printing.md index 9334a993..0bb7d4e9 100644 --- a/beam/docs/zebra_printing.md +++ b/beam/docs/zebra_printing.md @@ -3,22 +3,27 @@ For license information, please see license.txt--> # Zebra Printing +
+ Rohan Bansal and Tyler Matteson 2025-02-14 +
+ + To create a Zebra print format, you need the following documents: - A ZPL Print Format made against Doctype that may contain barcodes (Item, Warehouse, Handling Units, etc.) that uses the available Jinja utility functions to generate ZPL code. - A document Print Format that uses the free Labelary API to convert the above ZPL code and generate a preview of the print output for the linked document. ### ZPL Code Generation -Currently, only three types of printable ZPL data can be generated with utilities within Beam: +Currently, only three types of printable ZPL data can be generated with utilities within BEAM: - `Text` - `Barcode` - `Label` -Beam uses the [py-zebra-zpl](https://github.com/mtking2/py-zebra-zpl) library to generate the above types, as it provides a basic interface to create ZPL code using Python objects. Please refer to the library's documentation for more information on how to use it. +BEAM uses the [py-zebra-zpl](https://github.com/mtking2/py-zebra-zpl) library to generate the above types, as it provides a basic interface to create ZPL code using Python objects. Please refer to the library's documentation for more information on how to use it. **Note:** Additional ZPL elements (like graphic fields) and commands (text mirroring, character encoding, etc.) can be developed separately and added as text directly to the ZPL Print Format. For more information, visit the [official documentation page](https://supportcommunity.zebra.com/s/article/ZPL-Command-Information-and-DetailsV2?language=en_US) or the [Labelary ZPL Programming Guide](https://labelary.com/zpl.html). -In addition, Beam exposes the following Jinja functions to be used within a Print Format: +In addition, BEAM exposes the following Jinja functions to be used within a Print Format: --- @@ -135,21 +140,34 @@ Additional arguments can be passed to the function to customize the text. Please #### `labelary_api` -Generate an encoded Zebra printing label via the free Labelary API. It takes the following arguments: +Generate an encoded Zebra printing label preview via the free Labelary API. Converts ZPL code to a PNG image for preview purposes. It takes the following arguments: - `doc`: The document to be printed. Required. - `print_format`: The ZPL Print Format to be used for generating the label. Required. - `settings`: Additional settings to be passed to the Labelary API. Allows setting up the following parameters: - - `dpmm`: The desired print density, in dots per millimeter. Defaults to 8. + - `dpmm`: The desired print density, in dots per millimeter. Defaults to 8 (≈203 DPI). Use 12 for 300 DPI printers. - `width`: The desired label width, in inches. Defaults to 6. - `height`: The desired label height, in inches. Defaults to 4. - `index`: The label index (base 0). Some ZPL code will generate multiple labels, and this parameter can be used to access these different labels. Defaults to 0. -##### Example +**Important:** The `width` and `height` settings **MUST match the label dimensions used in your ZPL format**, otherwise the image will appear stretched or compressed. The `dpmm` setting should also match your printer's DPI. + +##### Example: 6x4" label at 203 DPI ```jinja - + ``` +##### Example: 4x6" label at 300 DPI +```jinja + +``` + +##### DPI Reference +| Printer Type | DPI | DPMM | +|---|---|---| +| Standard | 203 | 8 | +| High Resolution | 300 | 12 | + --- #### `get_handling_unit` @@ -182,3 +200,187 @@ Add text, barcodes, and other printable elements to a ZPL label. It takes the fo {% add_to_label(label, barcode) %} {{ label.dump_contents() }} ``` + +--- + +## ZPL Label Layout Tools + +The ZPL Layout Tools are designed to accelerate the process of creating ZPL label templates by automatically extracting text coordinates from PDF shipping label samples and generating production-ready ZPL templates with correct coordinates. + +### Overview + +Instead of manually measuring and calculating ZPL dot coordinates for every label element, you can: + +1. Run the layout analysis tool against a sample PDF label +2. Get an automatically generated ZPL template with all coordinates mapped +3. Customize as needed for your specific document fields +4. Integrate into BEAM print formats + +### Command Line Tool + +The layout analysis tool is available as a standalone command-line utility at `beam/beam/zpl_layout.py`. + +#### Usage + +```bash +# Activate the virtual environment +source /path/to/env/bin/activate +cd /path/to/beam + +# Basic usage (assumes portrait PDF, 6x4" landscape output @ 300 DPI) +python beam/beam/zpl_layout.py /path/to/label.pdf + +# Specify custom label dimensions +python beam/beam/zpl_layout.py /path/to/label.pdf --width 4 --height 6 --dpi 203 + +# Disable rotation (for already-landscape PDFs) +python beam/beam/zpl_layout.py /path/to/label.pdf --no-rotate + +# Custom output directory +python beam/beam/zpl_layout.py /path/to/label.pdf --output ./my_templates/ +``` + +#### Options + +- `pdf`: Path to the PDF file to analyze (required) +- `--output, -o`: Output directory (default: creates `output/` directory next to PDF) +- `--dpi`: Target printer DPI - 203 or 300 (default: 300) +- `--width`: Label width in inches (default: 6) +- `--height`: Label height in inches (default: 4) +- `--no-rotate`: Do not rotate portrait PDF to landscape + +### Output Files + +For each PDF processed, the tool generates three files in the output directory: + +#### 1. `{label_name}.zpl` - Production ZPL Template + +A Jinja2-compatible ZPL template with: +- All text coordinates automatically mapped +- Sections organized (addresses, shipping info, product details, barcodes) +- Variable placeholders (e.g., `{{ doc.ship_to_name }}`) ready for customization +- Comments indicating each section and coordinate values + +Example: +```jinja +{# Shipping Label - 6.0x4.0" @ 300 DPI #} +{% set label = zebra_zpl_label(width=1800.0, length=1200.0, dpi=300) -%} + +^XA {# Start Format #} +^PW1800.0 {# Print Width: 1800.0 dots #} +^LL1200.0 {# Label Length: 1200.0 dots #} + +{# === ADDRESS SECTION === #} +{# Ship From (Left Side) #} +^FO50,150^A0N,35,35^FDShip From:^FS +^FO50,200^A0N,28,28^FB700,5,0,L,0^FD{{ doc.ship_from_name }}^FS +^FO50,250^A0N,28,28^FB700,5,0,L,0^FD{{ doc.ship_from_address }}^FS + +... + +^XZ {# End Format #} +``` + +#### 2. `{label_name}_analysis.json` - Coordinate Data + +JSON file containing detailed extraction results: +- Label dimensions in dots and DPI +- Text blocks grouped by section (main_addresses, shipping_info, product_details, etc.) +- Each block includes: + - Text content + - ZPL X,Y coordinates (in dots) + - Barcode detection flag + +Use this for reference or further customization. + +#### 3. `{label_name}_layout_map.txt` - ASCII Visual Map + +ASCII art representation of the label layout showing: +- `·` for regular text blocks +- `█` for detected barcodes +- Borders indicating label dimensions + +Useful for visually verifying that coordinates were extracted correctly. + +### Integration into BEAM Print Formats + +Once you have a generated ZPL template: + +1. **Copy the template** into a new BEAM Print Format (create via Settings > Print Format) +2. **Replace variable placeholders** with actual document field references: + - `{{ doc.ship_from_name }}` → `{{ doc.supplier_name }}` (or your actual field) + - `{{ doc.po_number }}` → `{{ doc.purchase_order_number }}` + - etc. +3. **Test in Labelary viewer** at https://labelary.com/viewer.html + - Copy the ZPL code (with variables replaced by test data) + - Set label size to match your printer + - Verify layout and positioning +4. **Adjust coordinates as needed** based on actual print results + +### Key Features + +- **Automatic Barcode Detection**: Identifies GS1 Application Identifiers (e.g., `(420)`) and long numeric sequences +- **Rotation Support**: Automatically converts portrait PDFs (4"×6") to landscape (6"×4") +- **Multi-DPI Support**: Works with 203 DPI and 300 DPI printers +- **Section Grouping**: Intelligently organizes extracted text into logical regions +- **Visual Feedback**: ASCII layout map shows element positions for verification + +### Coordinate System + +The tool converts between different coordinate systems: + +| System | Origin | Y-Axis | Units | Example | +|--------|--------|--------|-------|---------| +| PDF | Bottom-left | Increases upward | Points | (x0, y0) in pdfplumber | +| ZPL | Top-left | Increases downward | Dots | ^FO{x},{y} in ZPL | + +Conversion formula: `zpl_dots = pdf_points × (target_dpi / 72)` + +### DPI/DPMM Reference + +When using the `labelary_api` helper or generating ZPL templates, ensure label dimensions match across all components: + +| DPI | DPMM | Printer Type | Example | +|-----|------|--------------|---------| +| 203 | 8 | Standard Zebra | Most common thermal printers | +| 300 | 12 | High Resolution | Better quality labels | + +**Critical:** Always pass the correct `dpmm` value to `labelary_api` to avoid image stretching. If your ZPL template is 6x4" at 300 DPI but you pass `dpmm: 8`, the preview will appear stretched horizontally. + +Example configurations: +- 6x4" label at 203 DPI: `labelary_api(doc, 'Format Name', {'width': 6, 'height': 4, 'dpmm': 8})` +- 4x6" label at 300 DPI: `labelary_api(doc, 'Format Name', {'width': 4, 'height': 6, 'dpmm': 12})` + +### Troubleshooting + +**Coordinates seem incorrect:** +- Verify the PDF orientation (portrait vs. landscape) +- Try with `--no-rotate` flag if PDF is already landscape +- Check that DPI matches your printer specification + +**Text not grouped correctly:** +- The section boundaries may need adjustment for non-standard label layouts +- Use the JSON analysis file to see exactly where text was detected +- Consider manually adjusting section coordinates in the generated template + +**Missing elements:** +- Some PDF elements (images, lines) may not be extracted +- pdfplumber extracts text only; complex graphics may need manual addition +- Review the layout map to identify missing elements + +### Example: Processing Trading Partner Labels + +The `label_spec/` folder contains sample PDFs from multiple trading partners. To generate templates for all: + +```bash +cd /path/to/beam +source /path/to/env/bin/activate + +# Pure Hockey (6x4 with rotation) +python beam/beam/zpl_layout.py label_spec/Pure\ Hockey\ -\ ASN\ label/*.pdf + +# Mindware (4x6 already landscape) +python beam/beam/zpl_layout.py "label_spec/Mindware - Oriental Trading Co - Carton label/*.pdf" --width 4 --height 6 --no-rotate +``` + +Templates are automatically saved to `label_spec/{partner}/output/` for easy access. diff --git a/beam/hooks.py b/beam/hooks.py index 53d939be..b00d7d1e 100644 --- a/beam/hooks.py +++ b/beam/hooks.py @@ -8,6 +8,7 @@ app_description = "Barcode Scanning for ERPNext" app_email = "support@agritheory.dev" app_license = "MIT" +required_apps = ["erpnext"] # Includes in # ------------------ @@ -18,7 +19,7 @@ # include js, css files in header of web template # web_include_css = "/assets/beam/css/beam.css" -# web_include_js = "/assets/beam/js/beam.js" +web_include_js = ["beam-web.bundle.js"] # include custom scss in every website theme (without file extension ".scss") # website_theme_scss = "beam/public/scss/website" @@ -31,7 +32,10 @@ # page_js = {"page" : "public/js/file.js"} # include js in doctype views -doctype_js = {"Stock Entry": "public/js/stock_entry_custom.js"} +doctype_js = { + "Network Printer Settings": "public/js/network_printer_settings_custom.js", + "Stock Entry": "public/js/stock_entry_custom.js", +} # doctype_list_js = {"doctype" : "public/js/doctype_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} @@ -44,7 +48,7 @@ # website user home page (by Role) # role_home_page = { -# "Role": "home_page" +# "BEAM Mobile User": "/beam/" # } # Generators @@ -60,6 +64,7 @@ "methods": [ "beam.beam.barcodes.add_to_label", "beam.beam.barcodes.barcode128", + "beam.beam.barcodes.get_qr_code", "beam.beam.barcodes.formatted_zpl_barcode", "beam.beam.barcodes.formatted_zpl_label", "beam.beam.barcodes.formatted_zpl_text", @@ -68,6 +73,7 @@ "beam.beam.barcodes.zebra_zpl_text", "beam.beam.printing.labelary_api", "beam.beam.scan.get_handling_unit", + "beam.beam.scan.get_serial_no", ], } @@ -110,8 +116,11 @@ # --------------- # Override standard doctype classes override_doctype_class = { + "Sales Order": "beam.beam.overrides.sales_order.BEAMSalesOrder", + "Network Printer Settings": "beam.beam.overrides.network_printer_settings.BEAMNetworkPrinterSettings", "Stock Entry": "beam.beam.overrides.stock_entry.BEAMStockEntry", "Subcontracting Receipt": "beam.beam.overrides.subcontracting_receipt.BEAMSubcontractingReceipt", + "Work Order": "beam.beam.overrides.work_order.BEAMWorkOrder", } @@ -120,55 +129,68 @@ # Hook on document methods and events doc_events = { - "Item": { - "validate": [ - "beam.beam.barcodes.create_beam_barcode", - ] - }, - "Warehouse": { - "validate": [ - "beam.beam.barcodes.create_beam_barcode", - ] - }, - "Purchase Receipt": { - "before_submit": [ - "beam.beam.handling_unit.generate_handling_units", + "Inventory Dimension": { + "after_insert": [ + "beam.beam.overrides.inventory_dimension.reset_demand_map", + "beam.beam.overrides.inventory_dimension.reset_receiving_map", ], - "validate": [ - # "beam.beam.handling_unit.validate_handling_unit_overconsumption", + "on_trash": [ + "beam.beam.overrides.inventory_dimension.reset_demand_map", + "beam.beam.overrides.inventory_dimension.reset_receiving_map", ], }, - "Purchase Invoice": { - "before_submit": [ - "beam.beam.handling_unit.generate_handling_units", - ], + ("Item", "Warehouse", "User"): { + "validate": ["beam.beam.barcodes.create_beam_barcode"], + }, + # ( + # "Purchase Receipt", + # "Stock Entry", + # "Sales Invoice", + # "Delivery Note", + # ): {"validate": ["beam.beam.handling_unit.validate_handling_unit_overconsumption"]}, + ("Delivery Note", "Purchase Receipt", "Sales Invoice", "Stock Entry", "Stock Reconciliation",): { + "on_submit": ["beam.beam.demand.demand.modify_allocations"], + "on_cancel": ["beam.beam.demand.demand.modify_allocations"], + }, + ("Purchase Receipt", "Purchase Invoice", "Subcontracting Receipt"): { + "before_submit": ["beam.beam.handling_unit.generate_handling_units"], }, "Stock Entry": { - "validate": [ - # "beam.beam.handling_unit.validate_handling_unit_overconsumption", - ], "before_submit": [ "beam.beam.handling_unit.generate_handling_units", "beam.beam.overrides.stock_entry.validate_items_with_handling_unit", ], }, - "Sales Invoice": { - "validate": [ - # "beam.beam.handling_unit.validate_handling_unit_overconsumption", - ], + ("Sales Order", "Work Order"): { + "on_submit": ["beam.beam.demand.demand.modify_demand"], + "on_cancel": ["beam.beam.demand.demand.modify_demand"], }, - "Delivery Note": { - "validate": [ - # "beam.beam.handling_unit.validate_handling_unit_overconsumption", + "Purchase Order": { + "on_submit": ["beam.beam.demand.receiving.modify_receiving"], + "on_cancel": ["beam.beam.demand.receiving.modify_receiving"], + }, + "Purchase Invoice": { + "on_submit": [ + "beam.beam.demand.receiving.modify_receiving", + "beam.beam.demand.demand.modify_allocations", + ], + "on_cancel": [ + "beam.beam.demand.receiving.modify_receiving", + "beam.beam.demand.demand.modify_allocations", ], }, - "Subcontracting Receipt": { - "before_submit": [ - "beam.beam.handling_unit.generate_handling_units", + "Company": { + "after_insert": [ + "beam.beam.overrides.company.create_company_beam_settings", ], }, } +# Types +# --------------- + +export_python_type_annotations = True + # Scheduled Tasks # --------------- @@ -197,11 +219,10 @@ # Overriding Methods # ------------------------------ -# -# override_whitelisted_methods = { -# "frappe.desk.doctype.event.event.get_events": "beam.event.get_events" -# } -# + +# override_whitelisted_methods = {"demand": "beam.beam..graphql_server"} + + # each overriding function accepts a `data` argument; # generated from the base implementation of the doctype dashboard, # along with any modifications made in other Frappe apps @@ -255,6 +276,388 @@ # Authentication and authorization # -------------------------------- -# auth_hooks = [ -# "beam.auth.validate" -# ] +auth_hooks = ["beam.beam.boot.redirect_to_beam"] + +demand = { + "Delivery Note": { + "on_submit": [ + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "decrease", + "allocation_effect": "decrease", + } + ], + "on_cancel": [ + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "increase", + "allocation_effect": "increase", + } + ], + }, + "Purchase Invoice": { + "on_submit": [ + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "increase", + "allocation_effect": "decrease", + "conditions": {"update_stock": True, "is_return": False}, + }, + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "decrease", + "allocation_effect": "increase", + "conditions": {"update_stock": True, "is_return": True}, + }, + ], + "on_cancel": [ + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "decrease", + "allocation_effect": "increase", + "conditions": {"update_stock": True, "is_return": False}, + }, + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "increase", + "allocation_effect": "decrease", + "conditions": {"update_stock": True, "is_return": True}, + }, + ], + }, + "Purchase Receipt": { + "on_submit": [ + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "decrease", + "allocation_effect": "increase", + } + ], + "on_cancel": [ + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "increase", + "allocation_effect": "decrease", + } + ], + }, + "Sales Invoice": { + "on_submit": [ + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "decrease", + "allocation_effect": "increase", + "conditions": {"update_stock": True, "is_return": False}, + }, + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "increase", + "allocation_effect": "decrease", + "conditions": {"update_stock": True, "is_return": True}, + }, + ], + "on_cancel": [ + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "increase", + "allocation_effect": "decrease", + "conditions": {"update_stock": True, "is_return": False}, + }, + { + "warehouse_field": "warehouse", + "quantity_field": "stock_qty", + "demand_effect": "decrease", + "allocation_effect": "increase", + "conditions": {"update_stock": True, "is_return": True}, + }, + ], + }, + "Stock Entry": { + "on_submit": [ + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Material Transfer for Manufacture"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Material Transfer for Manufacture"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Material Issue"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Material Receipt"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Material Transfer"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Material Transfer"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Manufacture"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Manufacture"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Repack"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Repack"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Send to Subcontractor"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Send to Subcontractor"}, + }, + ], + "on_cancel": [ + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Material Transfer for Manufacture"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Material Transfer for Manufacture"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Material Issue"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Material Receipt"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Material Transfer"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Material Transfer"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Manufacture"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Manufacture"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Repack"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Repack"}, + }, + { + "warehouse_field": "s_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "increase", + "conditions": {"purpose": "Send to Subcontractor"}, + }, + { + "warehouse_field": "t_warehouse", + "quantity_field": "transfer_qty", + "allocation_effect": "decrease", + "conditions": {"purpose": "Send to Subcontractor"}, + }, + ], + }, + "Stock Reconciliation": { + "on_submit": [ + { + "warehouse_field": "warehouse", + "quantity_field": "qty", + "allocation_effect": "adjustment", + } + ], + "on_cancel": [ + { + "warehouse_field": "warehouse", + "quantity_field": "qty", + "allocation_effect": "adjustment", + } + ], + }, +} + + +beam_mobile = { + "components": { + "DeliveryNote": "./beam/beam/www/beam/pages/DeliveryNote.vue", + "Demand": "./beam/beam/www/beam/pages/Demand.vue", + "Home": "./beam/beam/www/beam/pages/Home.vue", + "JobCard": "./beam/beam/www/beam/pages/JobCard.vue", + "Manufacture": "./beam/beam/www/beam/pages/Manufacture.vue", + "Move": "./beam/beam/www/beam/pages/Move.vue", + "Operation": "./beam/beam/www/beam/pages/Operation.vue", + "PurchaseReceipt": "./beam/beam/www/beam/pages/PurchaseReceipt.vue", + "Receive": "./beam/beam/www/beam/pages/Receive.vue", + "Repack": "./beam/beam/www/beam/pages/Repack.vue", + "Ship": "./beam/beam/www/beam/pages/Ship.vue", + "WorkOrder": "./beam/beam/www/beam/pages/WorkOrder.vue", + "Workstation": "./beam/beam/www/beam/pages/Workstation.vue", + "404": "./beam/beam/www/beam/pages/404.vue", + }, + "routes": [ + { + "path": "/", + "name": "home", + "component": "Home", + "meta": {"requiresAuth": True, "doctype": None, "view": "list"}, + }, + { + "path": "/workstation", + "name": "workstation", + "component": "Workstation", + "meta": {"requiresAuth": True, "doctype": "Workstation", "view": "list"}, + }, + { + "path": "/work_order/:id/", + "name": "work_order", + "component": "WorkOrder", + "meta": {"requiresAuth": True, "doctype": "Work Order", "view": "form"}, + }, + { + "path": "/job_card/:id/", + "name": "job_card", + "component": "JobCard", + "meta": {"requiresAuth": True, "doctype": "Work Order", "view": "form"}, + }, + { + "path": "/work_order/:id/operation/:operationId", + "name": "operation", + "component": "Operation", + "meta": {"requiresAuth": True, "doctype": "Work Order", "view": "form"}, + }, + { + "path": "/receive", + "name": "receive", + "component": "Receive", + "meta": {"requiresAuth": True, "doctype": "Purchase Receipt", "view": "list"}, + }, + { + "path": "/purchase-receipt", + "name": "purchase-receipt", + "component": "PurchaseReceipt", + "meta": {"requiresAuth": True, "doctype": "Purchase Receipt", "view": "form"}, + }, + { + "path": "/purchase-receipt/:id", + "name": "purchase-receipt", + "component": "PurchaseReceipt", + "meta": {"requiresAuth": True, "doctype": "Purchase Receipt", "view": "form"}, + }, + { + "path": "/ship", + "name": "ship", + "component": "Ship", + "meta": {"requiresAuth": True, "doctype": "Delivery Note", "view": "list"}, + }, + { + "path": "/delivery-note", + "name": "delivery-note", + "component": "DeliveryNote", + "meta": {"requiresAuth": True, "doctype": "Delivery Note", "view": "form"}, + }, + { + "path": "/demand", + "name": "demand", + "component": "Demand", + "meta": {"requiresAuth": True, "doctype": "Stock Entry", "view": "list"}, + }, + { + "path": "/move", + "name": "move", + "component": "Move", + "meta": {"requiresAuth": True, "doctype": "Stock Entry", "view": "form"}, + }, + { + "path": "/manufacture", + "name": "manufacture", + "component": "Manufacture", + "meta": {"requiresAuth": True, "doctype": "Work Order", "view": "list"}, + }, + { + "path": "/repack", + "name": "repack", + "component": "Repack", + "meta": {"requiresAuth": True, "doctype": "Stock Entry", "view": "form"}, + }, + { + "path": "/:catchAll(.*)*", + "name": "404", + "component": "404", + }, + ], +} diff --git a/beam/install.py b/beam/install.py index 7ee4788b..e76a11ee 100644 --- a/beam/install.py +++ b/beam/install.py @@ -1,21 +1,33 @@ # Copyright (c) 2025, AgriTheory and contributors # For license information, please see license.txt +import pathlib + import frappe +from frappe.utils import get_site_path +from beam.beam.demand.demand import build_demand_allocation_map +from beam.beam.demand.receiving import reset_build_receiving_map from beam.beam.scan.config import get_scan_doctypes -from beam.customize import load_customizations +from beam.patches.v15.setup_beam_mobile_settings import execute + + +def create_beam_mobile_user_role(): + if not frappe.db.exists("Role", "BEAM Mobile User"): + role = frappe.get_doc( + {"doctype": "Role", "role_name": "BEAM Mobile User", "desk_access": 0, "home_page": "/beam"} + ) + role.insert(ignore_permissions=True) def after_install(): - load_customizations() print("Setting up Handling Unit Inventory Dimension") if frappe.db.exists("Inventory Dimension", "Handling Unit"): return huid = frappe.new_doc("Inventory Dimension") huid.dimension_name = "Handling Unit" huid.reference_document = "Handling Unit" - huid.apply_to_all_doctypes = 1 + huid.apply_to_all_doctypes = True huid.save() # re-label @@ -29,17 +41,24 @@ def after_install(): if custom_field.dt == "Purchase Invoice Item": frappe.set_value("Custom Field", custom_field, "label", "Handling Unit") else: - frappe.set_value("Custom Field", custom_field, "read_only", 1) - frappe.set_value("Custom Field", custom_field["name"], "no_copy", 1) + frappe.set_value("Custom Field", custom_field, "read_only", True) + frappe.set_value("Custom Field", custom_field["name"], "no_copy", True) frm_doctypes = get_scan_doctypes()["frm"] for custom_field in frappe.get_all("Custom Field", {"label": "Handling Unit"}, ["name", "dt"]): - frappe.set_value("Custom Field", custom_field["name"], "no_copy", 1) + frappe.set_value("Custom Field", custom_field["name"], "no_copy", True) if ( custom_field["dt"] not in frm_doctypes and custom_field["dt"].replace(" Item", "").replace(" Detail", "") not in frm_doctypes ): - frappe.set_value("Custom Field", custom_field["name"], "read_only", 1) - frappe.set_value("Custom Field", custom_field["name"], "no_copy", 1) + frappe.set_value("Custom Field", custom_field["name"], "read_only", True) + frappe.set_value("Custom Field", custom_field["name"], "no_copy", True) + + print("Setting up demand database") + pathlib.Path(f"{get_site_path()}/demand.db").unlink(missing_ok=True) + build_demand_allocation_map() + reset_build_receiving_map() + create_beam_mobile_user_role() + execute() diff --git a/beam/patches.txt b/beam/patches.txt index e69de29b..ceaf3949 100644 --- a/beam/patches.txt +++ b/beam/patches.txt @@ -0,0 +1,2 @@ +beam.patches.v15.create_beam_mobile_user_role # 11/01/24 Francisco Roldan +beam.patches.v15.setup_beam_mobile_settings # 11/01/24 Tyler Matteson \ No newline at end of file diff --git a/beam/patches/.gitkeep b/beam/patches/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/beam/patches/v15/create_beam_mobile_user_role.py b/beam/patches/v15/create_beam_mobile_user_role.py new file mode 100644 index 00000000..59b7c148 --- /dev/null +++ b/beam/patches/v15/create_beam_mobile_user_role.py @@ -0,0 +1,8 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +from beam.install import create_beam_mobile_user_role + + +def execute(): + create_beam_mobile_user_role() diff --git a/beam/patches/v15/setup_beam_mobile_settings.py b/beam/patches/v15/setup_beam_mobile_settings.py new file mode 100644 index 00000000..86267b0e --- /dev/null +++ b/beam/patches/v15/setup_beam_mobile_settings.py @@ -0,0 +1,39 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +from erpnext import get_default_company + +from beam.beam.doctype.beam_settings.beam_settings import create_beam_settings + + +def execute(company=None): + frappe.reload_doc("beam", "doctype", "beam_settings") + + default_config = [ + { + "label": "Manufacture", + "route": "#/manufacture", + "dt": "Stock Entry", + "component": "Manufacture", + }, + {"label": "Demand", "route": "#/demand", "dt": "Stock Entry", "component": "Demand"}, + {"label": "Move", "route": "#/move", "dt": "Stock Entry", "component": "Demand"}, + {"label": "Receive", "route": "#/receive", "dt": "Purchase Receipt", "component": "Receive"}, + {"label": "Ship", "route": "#/ship", "dt": "Delivery Note", "component": "Ship"}, + {"label": "Repack", "route": "#/repack", "dt": "Stock Entry", "component": "Repack"}, + ] + + beam_configs = frappe.get_all("BEAM Settings", pluck="name") + if not beam_configs: + company = get_default_company() or company + if not company: + return + beam_configs = [create_beam_settings(company)] + for company in beam_configs: + doc = frappe.get_doc("BEAM Settings", company) + if len(doc.routes) > 0: + return + for row in default_config: + doc.append("routes", row) + doc.save() diff --git a/beam/public/js/beam-web.bundle.js b/beam/public/js/beam-web.bundle.js new file mode 100644 index 00000000..0004b12b --- /dev/null +++ b/beam/public/js/beam-web.bundle.js @@ -0,0 +1,25 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import './scan/scan.js' + +// specifically remove Frappe website theming for the Beam page +if (window.location.pathname === '/beam') { + const stylesheets = document.querySelectorAll('link[rel=stylesheet]') + for (const stylesheet of stylesheets) { + if (stylesheet.href.includes('assets/frappe/dist/css/website.bundle')) { + stylesheet.parentNode.removeChild(stylesheet) + } + } +} + +// remove redirect-to query parameter on login page for mobile users +document.addEventListener('DOMContentLoaded', function () { + if (window.location.pathname === '/login') { + const url = new URL(window.location.href) + if (url.searchParams.has('redirect-to')) { + url.searchParams.delete('redirect-to') + window.history.replaceState({}, document.title, url.toString()) + } + } +}) diff --git a/beam/public/js/network_printer_settings_custom.js b/beam/public/js/network_printer_settings_custom.js new file mode 100644 index 00000000..e98acf69 --- /dev/null +++ b/beam/public/js/network_printer_settings_custom.js @@ -0,0 +1,52 @@ +// Copyright (c) 2025, AgriTheory and contributors +// For license information, please see license.txt + +let printers_cache = [] +let pending_printer_name = null + +function set_location_from_cache(frm, printer_name) { + const match = printers_cache.find(p => p.value === printer_name) + frm.set_value('printer_location', match ? match.location || '' : '') +} + +frappe.ui.form.on('Network Printer Settings', { + after_save(frm) { + // Refresh cache from CUPS so subsequent printer_name changes + // reflect the just-saved location, not stale pre-load data. + printers_cache = [] + frm.trigger('connect_print_server') + }, + connect_print_server(frm) { + if (frm.doc.server_ip && frm.doc.port) { + frappe.call({ + doc: frm.doc, + method: 'get_printers_list', + args: { + ip: frm.doc.server_ip, + port: frm.doc.port, + }, + callback(data) { + printers_cache = data.message || [] + frm.fields_dict.printer_name.set_data(printers_cache) + // Resolve any pending printer_name lookup that fired before cache was ready + if (pending_printer_name) { + set_location_from_cache(frm, pending_printer_name) + pending_printer_name = null + } + }, + }) + } + }, + printer_name(frm) { + if (!frm.doc.printer_name) { + return + } + if (!printers_cache.length) { + // Cache not populated yet — queue the lookup and trigger a fetch + pending_printer_name = frm.doc.printer_name + frm.trigger('connect_print_server') + return + } + set_location_from_cache(frm, frm.doc.printer_name) + }, +}) diff --git a/beam/public/js/print/print.js b/beam/public/js/print/print.js index 70a54a49..238ffb54 100644 --- a/beam/public/js/print/print.js +++ b/beam/public/js/print/print.js @@ -40,47 +40,49 @@ function custom_print_button(frm) { if (frm.doc.docstatus != 1) { return } - frappe.db.get_value('BEAM Settings', { company: frm.doc.company }, 'enable_handling_units', r => { - if (r && r.enable_handling_units) { - frm.add_custom_button(__(' Print Handling Unit'), () => { - let d = new frappe.ui.Dialog({ - title: __('Select Printer Setting'), - fields: [ - { - label: __('Printer Setting'), - fieldname: 'printer_setting', - fieldtype: 'Link', - options: 'Network Printer Settings', - }, - { - label: __('Printer Format'), - fieldname: 'print_format', - fieldtype: 'Link', - options: 'Print Format', - get_query: function () { - return { - filters: { doc_type: 'Handling Unit' }, - } - }, - }, - ], - primary_action_label: 'Select', - primary_action(selection) { - d.hide() - frappe.call({ - method: 'beam.beam.printing.print_handling_units', - args: { - doctype: frm.doc.doctype, - name: frm.doc.name, - printer_setting: selection.printer_setting, - print_format: selection.print_format, - doc: frm.doc, - }, - }) + const beam_settings = frappe.boot.beam?.settings?.[frm.doc.company] + if (!beam_settings?.enable_handling_units) { + return + } + frm.add_custom_button(__(' Print Handling Unit'), () => { + let d = new frappe.ui.Dialog({ + title: __('Select Printer Setting'), + fields: [ + { + label: __('Printer Setting'), + fieldname: 'printer_setting', + fieldtype: 'Link', + options: 'Network Printer Settings', + default: frappe.defaults.get_user_default('Network Printer Settings'), + }, + { + label: __('Print Format'), + fieldname: 'print_format', + fieldtype: 'Link', + options: 'Print Format', + default: frappe.boot.beam?.default_hu_print_format, + get_query: function () { + return { + filters: { doc_type: 'Handling Unit' }, + } + }, + }, + ], + primary_action_label: 'Select', + primary_action(selection) { + d.hide() + frappe.call({ + method: 'beam.beam.printing.print_handling_units', + args: { + doctype: frm.doc.doctype, + name: frm.doc.name, + printer_setting: selection.printer_setting, + print_format: selection.print_format, + doc: frm.doc, }, }) - d.show() - }) - } + }, + }) + d.show() }) } diff --git a/beam/public/js/scan/scan.js b/beam/public/js/scan/scan.js index c987ed43..3d33c7f8 100644 --- a/beam/public/js/scan/scan.js +++ b/beam/public/js/scan/scan.js @@ -3,17 +3,23 @@ import onScan from 'onscan.js' +const isLoginPath = window.location.pathname === '/login' + function waitForElement(selector) { return new Promise(resolve => { - if (document.querySelector(selector)) { - return resolve(document.querySelector(selector)) - } - const observer = new MutationObserver(mutations => { - if (document.querySelector(selector)) { - resolve(document.querySelector(selector)) + if (isLoginPath) return resolve(document.body) + + const element = document.querySelector(selector) + if (element) return resolve(element) + + const observer = new MutationObserver(() => { + const element = document.querySelector(selector) + if (element) { + resolve(element) observer.disconnect() } }) + observer.observe(document.body, { childList: true, subtree: true, @@ -21,10 +27,18 @@ function waitForElement(selector) { }) } +function initScanHandler() { + if (typeof ScanHandler === 'undefined') return + new ScanHandler() +} + waitForElement('[data-route]').then(element => { - let observer = new MutationObserver(() => { - new ScanHandler() + initScanHandler() + + const observer = new MutationObserver(() => { + initScanHandler() }) + const config = { attributes: true, childList: false, characterData: true } observer.observe(element, config) }) @@ -66,18 +80,24 @@ class ScanHandler { } async get_scanned_context(sCode, iQty) { return new Promise(resolve => { - const context = this.reduceContext() - frappe.xcall('beam.beam.scan.scan', { barcode: sCode, context: context, current_qty: iQty }).then(r => { - if (r && r.length) { - if (Object.keys(frappe.boot.beam.client).includes(r[0].action)) { - let path = frappe.boot.beam.client[r[0].action][0] - resolve(path.split('.').reduce((o, i) => o[i], window)(r)) // calls (first) custom built callback registered in hooks - } else { - resolve(this[String(r[0].action)](r)) // TODO: this only calls the first function + if (isLoginPath) { + frappe.xcall('beam.beam.scan.user_login.scan_login', { barcode: sCode }).then(r => { + if (r.success) window.location.href = '/beam' + }) + } else { + const context = this.reduceContext() + frappe.xcall('beam.beam.scan.scan', { barcode: sCode, context: context, current_qty: iQty }).then(r => { + if (r && r.length) { + if (Object.keys(frappe.boot.beam.client).includes(r[0].action)) { + let path = frappe.boot.beam.client[r[0].action][0] + resolve(path.split('.').reduce((o, i) => o[i], window)(r)) // calls (first) custom built callback registered in hooks + } else { + resolve(this[String(r[0].action)](r)) // TODO: this only calls the first function + } } - } - // TODO: else error - }) + // TODO: else error + }) + } }) } route(barcode_context) { @@ -188,6 +208,36 @@ class ScanHandler { frappe.model.set_value(row.doctype, row.name, 's_warehouse', barcode_context.target) frappe.model.set_value(row.doctype, row.name, 't_warehouse', barcode_context.target) } + } else if ( + barcode_context.doctype == 'Stock Reconciliation Item' || + barcode_context.doctype == 'Stock Reconciliation' + ) { + cur_frm.set_value('set_warehouse', barcode_context.target) + cur_frm.set_value('purpose', 'Stock Reconciliation') + frappe.call({ + method: 'erpnext.stock.doctype.stock_reconciliation.stock_reconciliation.get_items', + args: { + warehouse: barcode_context.target, + posting_date: cur_frm.doc.posting_date, + posting_time: cur_frm.doc.posting_time, + company: cur_frm.doc.company, + }, + callback: function (r) { + if (r.exc || !r.message || !r.message.length) return + + cur_frm.clear_table('items') + + r.message.forEach(row => { + let item = cur_frm.add_child('items') + $.extend(item, row) + + item.qty = item.qty || 0 + item.valuation_rate = item.valuation_rate || 0 + item.use_serial_batch_fields = cint(frappe.user_defaults?.use_serial_batch_fields) + }) + cur_frm.refresh_field('items') + }, + }) } } add_or_increment(barcode_context) { diff --git a/beam/public/js/stock_entry_custom.js b/beam/public/js/stock_entry_custom.js index 8b6d6531..09f68642 100644 --- a/beam/public/js/stock_entry_custom.js +++ b/beam/public/js/stock_entry_custom.js @@ -23,8 +23,8 @@ frappe.ui.form.on('Stock Entry', { async function show_handling_unit_recombine_dialog(frm) { const data = await get_handling_units(frm) - if (!data) { - return resolve({}) + if (!data || !data.length) { + return [] } let fields = [ { @@ -35,6 +35,14 @@ async function show_handling_unit_recombine_dialog(frm) { disabled: 0, hidden: 1, }, + { + fieldtype: 'Data', + fieldname: 'target_row_name', + in_list_view: 0, + read_only: 1, + disabled: 0, + hidden: 1, + }, { fieldtype: 'Link', fieldname: 'item_code', @@ -43,6 +51,7 @@ async function show_handling_unit_recombine_dialog(frm) { read_only: 1, disabled: 0, label: __('Item Code'), + columns: 2, }, { fieldtype: 'Data', @@ -57,6 +66,7 @@ async function show_handling_unit_recombine_dialog(frm) { label: __('Handling Unit'), in_list_view: 1, read_only: 1, + columns: 2, }, { fieldtype: 'Float', @@ -64,6 +74,7 @@ async function show_handling_unit_recombine_dialog(frm) { label: __('Remaining Qty'), in_list_view: 1, read_only: 1, + columns: 1, }, { fieldtype: 'Data', @@ -71,6 +82,7 @@ async function show_handling_unit_recombine_dialog(frm) { label: __('Handling Unit to recombine'), in_list_view: 1, read_only: 1, + columns: 2, }, { fieldtype: 'Float', @@ -78,6 +90,7 @@ async function show_handling_unit_recombine_dialog(frm) { label: __('Transferred Qty'), in_list_view: 1, read_only: 1, + columns: 1, }, ] @@ -88,10 +101,8 @@ async function show_handling_unit_recombine_dialog(frm) { { fieldname: 'handling_units', fieldtype: 'Table', - in_place_edit: false, - editable_grid: false, cannot_add_rows: true, - cannot_delete_rows: true, + cannot_delete_rows: false, reqd: 1, data: data, get_data: () => { @@ -104,9 +115,14 @@ async function show_handling_unit_recombine_dialog(frm) { }, ], primary_action: () => { - let to_recombine = dialog.fields_dict.handling_units.grid.get_selected_children().map(row => { - return row.row_name - }) + let selected = dialog.fields_dict.handling_units.grid.get_selected_children() + let to_recombine = [] + for (let row of selected) { + to_recombine.push(row.row_name) + if (row.target_row_name) { + to_recombine.push(row.target_row_name) + } + } dialog.hide() return resolve(to_recombine) }, @@ -114,14 +130,39 @@ async function show_handling_unit_recombine_dialog(frm) { size: 'extra-large', }) dialog.show() + // Pre-check all rows so recombine is the default behavior + setTimeout(() => { + const grid = dialog.fields_dict.handling_units.grid + // Enable and check all rows + if (grid.wrapper) { + grid.wrapper.find('.grid-row-check').prop('disabled', false).prop('checked', true) + // Hide the Delete button + grid.wrapper.find('.grid-remove-rows').hide() + } + grid.grid_rows?.forEach(row => { + if (row.doc) { + row.doc.__checked = 1 + if (row.row) { + row.row.find('.grid-row-check').prop('disabled', false).prop('checked', true) + } + } + }) + grid.refresh() + }, 200) dialog.get_close_btn() }) } async function get_handling_units(frm) { let handling_units = [] + const transfer_types = ['Material Transfer', 'Send to Subcontractor', 'Material Transfer for Manufacture'] + for (const row of frm.doc.items) { - if (row.handling_unit && row.to_handling_unit) { + if (!row.handling_unit) continue + + if (transfer_types.includes(frm.doc.purpose)) { + // Material Transfer types: source and destination HU are on the same row + if (!row.to_handling_unit) continue let remaining_qty = await get_handling_unit_stock_qty(frm.doc.name, row.handling_unit, row.s_warehouse) handling_units.push({ row_name: row.name, @@ -132,8 +173,25 @@ async function get_handling_units(frm) { remaining_qty: remaining_qty, transferred_qty: row.qty, }) + } else { + // Repack/Manufacture/etc: source and target HUs are on separate rows + // Only show source rows (those with s_warehouse); pair with matching target row + if (!row.s_warehouse) continue + let target_row = frm.doc.items.find(r => r.t_warehouse && r.handling_unit && r.item_code === row.item_code) + let remaining_qty = await get_handling_unit_stock_qty(frm.doc.name, row.handling_unit, row.s_warehouse) + handling_units.push({ + row_name: row.name, + target_row_name: target_row?.name || '', + item_code: row.item_code, + item_name: row.item_name, + handling_unit: row.handling_unit, + to_handling_unit: target_row?.handling_unit || '', + remaining_qty: remaining_qty, + transferred_qty: row.transfer_qty || row.qty, + }) } } + return handling_units } async function get_handling_unit_stock_qty(name, handling_unit, s_warehouse) { @@ -147,6 +205,10 @@ async function get_handling_unit_stock_qty(name, handling_unit, s_warehouse) { //re combine async function set_recombine_handling_units(frm) { + // const beam_settings = frappe.boot.beam?.settings?.[frm.doc.company] + // if (!beam_settings?.enable_handling_units) { + // return + // } let to_recombine = await show_handling_unit_recombine_dialog(frm) await frappe.xcall('beam.beam.overrides.stock_entry.set_rows_to_recombine', { docname: frm.doc.name, diff --git a/beam/tests/conftest.py b/beam/tests/conftest.py index f3f7866a..c9fd82f1 100644 --- a/beam/tests/conftest.py +++ b/beam/tests/conftest.py @@ -3,12 +3,14 @@ import json from pathlib import Path -from unittest.mock import MagicMock import frappe import pytest from frappe.utils import get_bench_path +from beam.beam.demand.demand import build_demand_allocation_map +from beam.beam.demand.receiving import reset_build_receiving_map + def _get_logger(*args, **kwargs): from frappe.utils.logger import get_logger @@ -39,7 +41,9 @@ def db_instance(): if (sites / "common_site_config.json").is_file(): currentsite = json.loads((sites / "common_site_config.json").read_text()).get("default_site") - frappe.init(site=currentsite, sites_path=sites) + frappe.init(site=currentsite, sites_path=sites, force=True) frappe.connect() - frappe.db.commit = MagicMock() + + build_demand_allocation_map() + reset_build_receiving_map() yield frappe.db diff --git a/beam/tests/fixtures.py b/beam/tests/fixtures.py index 12e924ea..2a8f7a96 100644 --- a/beam/tests/fixtures.py +++ b/beam/tests/fixtures.py @@ -63,6 +63,8 @@ ("Refrigerator Station", "200"), ("Oven Station", "20"), ("Mixer Station", "10"), + ("Receiving", "100"), + ("Shipping", "100"), ] operations = [ @@ -368,6 +370,19 @@ "default_warehouse": "Kitchen - APC", "supplier": "Freedom Provisions", }, + { + "item_code": "Whipped Cream Canister", + "uom": "Nos", + "item_group": "Bakery Supplies", + "default_warehouse": "Storeroom - APC", + "description": "Pressurized whipped cream canister for serving pies; also sold retail.", + "item_price": 2.75, + "supplier": "Unity Bakery Supply", + "is_sales_item": 1, + "is_purchase_item": 1, + "has_serial_no": 1, + "serial_no_series": "WCC-.#####", + }, ] boms = [ @@ -697,3 +712,159 @@ "TransAmerica Bank Cafeteria", "Whole Harvest Grocery Group", ] + + +employees = [ + { + "name": "Tristan Hawkins", + "gender": "Male", + "date_of_birth": "2002-12-09", + "date_of_joining": "2018-01-01", + "address": { + "address_line1": "1156 Mountview Canyon", + "city": "Lakewood", + "state": "ME", + "postal_code": "02311", + }, + "phone": "(704) 885-0542", + "roles": ["Stock Manager", "Item Manager"], + # "department": "Operations", + "designation": "Bakery Manager", + }, + { + "name": "Deane Solomon", + "gender": "Female", + "date_of_birth": "1987-10-08", + "date_of_joining": "2018-01-01", + "address": { + "address_line1": "590 Avenue Of The Palms Hills", + "city": "San Jacinto", + "state": "MA", + "postal_code": "28260", + }, + "phone": "(658) 583-5499", + "roles": ["Stock User", "BEAM Mobile User"], + "reports_to": "Tristan Hawkins", + # "department": "Operations", + "designation": "Baker", + }, + { + "name": "Scott Larson", + "gender": "Male", + "date_of_birth": "1993-01-23", + "date_of_joining": "2018-01-01", + "address": { + "address_line1": "135 Locksley Route", + "city": "Sikeston", + "state": "CT", + "postal_code": "89972", + }, + "phone": "(962) 762-5895", + "roles": ["Stock User", "BEAM Mobile User"], + "reports_to": "Tristan Hawkins", + # "department": "Operations", + "designation": "Baker", + }, + { + "name": "Almeta Nolan", + "gender": "Female", + "date_of_birth": "1995-08-28", + "date_of_joining": "2018-01-01", + "address": { + "address_line1": "78 Payson Terrace", + "city": "Bedford", + "state": "MA", + "postal_code": "10796", + }, + "phone": "(366) 357-8223", + "roles": ["Stock User", "BEAM Mobile User"], + "reports_to": "Tristan Hawkins", + # "department": "Operations", + "designation": "Bakery Manager", + }, + { + "name": "Denise Wilkins", + "gender": "Female", + "date_of_birth": "1973-11-28", + "date_of_joining": "2018-01-01", + "address": { + "address_line1": "721 Mason Court", + "city": "Colonial Heights", + "state": "ME", + "postal_code": "53756", + }, + "phone": "(930) 920-4520", + "roles": ["Stock User", "BEAM Mobile User"], + "reports_to": "Tristan Hawkins", + # "department": "Operations", + "designation": "Baker", + }, + { + "name": "Neta Estrada", + "gender": "Female", + "date_of_birth": "1982-11-09", + "date_of_joining": "2020-01-15", + "address": { + "address_line1": "665 Gorgas Alley", + "city": "Whittier", + "state": "NH", + "postal_code": "85689", + }, + "phone": "(054) 893-8970", + "roles": ["Stock User", "BEAM Mobile User"], + "reports_to": "Tristan Hawkins", + # "department": "Operations", + "designation": "Baker", + }, + { + "name": "Issac Benson", + "gender": "Male", + "date_of_birth": "1975-09-19", + "date_of_joining": "2023-08-08", + "address": { + "address_line1": "78 Martha Street", + "city": "McKinney", + "state": "NH", + "postal_code": "47856", + }, + "phone": "(814) 677-9322", + "roles": ["Stock User", "BEAM Mobile User"], + "reports_to": "Tristan Hawkins", + # "department": "Operations", + "designation": "Baker", + }, + { + "name": "Tracey Faulkner", + "gender": "Female", + "date_of_birth": "1993-01-09", + "date_of_joining": "2022-12-14", + "address": { + "address_line1": "1079 Woodside Pine", + "city": "Belle Glade", + "state": "NH", + "postal_code": "97865", + }, + "phone": "(133) 195-7828", + "roles": ["Stock User", "BEAM Mobile User"], + "reports_to": "Tristan Hawkins", + # "department": "Operations", + "designation": "Baker", + }, + { + "name": "Phoebe Hickman", + "gender": "Female", + "date_of_birth": "1999-12-06", + "date_of_joining": "2022-01-16", + "address": { + "address_line1": "188 Dorcas Cove", + "city": "Royal Palm Beach", + "state": "NH", + "postal_code": "71202", + }, + "phone": "(041) 000-2569", + "roles": ["Stock User", "BEAM Mobile User"], + "reports_to": "Tristan Hawkins", + # "department": "Operations", + "designation": "Baker", + }, +] diff --git a/beam/tests/mobile/conftest.py b/beam/tests/mobile/conftest.py new file mode 100644 index 00000000..20ef8000 --- /dev/null +++ b/beam/tests/mobile/conftest.py @@ -0,0 +1,54 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +import pytest + + +@pytest.fixture(scope="session") +def browser_context_args(browser_context_args): + # emulate an Android barcode scanner + return { + **browser_context_args, + "viewport": { + "width": 400, + "height": 900, + }, + } + + +@pytest.fixture(autouse=True) +def setup(page): + # delete all existing draft Purchase Receipts + delete_draft_records(["Purchase Receipt", "Stock Entry"]) + + page.set_default_timeout(5000) + + base_url = frappe.utils.get_url() + page.goto(base_url) + + # visiting the home page redirects to login page + page.get_by_role("textbox", name="Email").fill("support@agritheory.dev") + page.get_by_role("textbox", name="Password").fill("admin") + page.get_by_role("button", name="Login").click() # this will redirect to `/beam` + yield + + # delete all Purchase Receipts created during the test + receipts = frappe.get_all( + "Purchase Receipt", filters={"docstatus": ["in", [1, 2]]}, fields=["name", "docstatus"] + ) + for receipt in receipts: + receipt_doc = frappe.get_doc("Purchase Receipt", receipt.name) + if receipt.docstatus == 1: + receipt_doc.cancel() + # only delete if the document is in draft state, since cancelled documents are + # linked to SLEs, which can't be deleted + elif receipt.docstatus == 0: + receipt_doc.delete() + + +def delete_draft_records(doctypes: list[str]): + for doctype in doctypes: + records = frappe.get_all(doctype, filters={"docstatus": 0}, pluck="name") + for record in records: + frappe.delete_doc(doctype, record, force=True) diff --git a/beam/tests/mobile/test_manufacture.py b/beam/tests/mobile/test_manufacture.py new file mode 100644 index 00000000..12b1d171 --- /dev/null +++ b/beam/tests/mobile/test_manufacture.py @@ -0,0 +1,112 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +# To test locally: +# active the virtual environment +# bench start, and then run: +# pytest ./beam/tests/mobile/test_manufacture.py --browser firefox --headed --disable-warnings + +import re + +import frappe +import pytest +from playwright.sync_api import expect + +from beam.tests.test_utils import use_current_db_transaction + + +@pytest.mark.order(1) +def test_complete_partial_stock_entry(page): + """ + This test needs to disable handling units on Beam Settings and + populate the item in Stock Entry, otherwise we will obtain the error: + + 'frappe.exceptions.ValidationError: Row #1: Handling Unit is missing for item Butter' + or + 'erpnext.stock.stock_ledger.NegativeStockError: 1.0 units of + Item Butter needed in Warehouse Refrigerator - APC to complete this transaction.' + """ + + frappe.db.set_value("BEAM Settings", "Ambrosia Pie Company", "enable_handling_units", 0) + frappe.db.commit() + + butter = frappe.new_doc("Stock Entry") + butter.stock_entry_type = butter.purpose = "Material Receipt" + butter.append( + "items", + { + "item_code": "Butter", + "qty": 5, # intentionally to help with demand tests + "t_warehouse": "Refrigerator - APC", + "uom": "Pound", + "basic_rate": 4.50, + "expense_account": "5119 - Stock Adjustment - APC", + }, + ) + + butter.save() + butter.submit() + frappe.db.commit() + + # navigate in the following order: Home -> Manufacture -> Work Order + page.get_by_text("Manufacture").click() + page.locator("css=.beam_list-item").first.click() + + # get the selected Work Order + order_id = page.url.split("/")[-1] + assert order_id + + # ensure there are no existing Stock Entries against this Work Order + entry = frappe.db.exists( + "Stock Entry", + {"docstatus": 0, "work_order": order_id}, + ) + assert not entry + + # find the first item in the list + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + item_count = page.locator("css=.box .beam_item-count").first + expect(item_count).to_have_text(re.compile("0/")) + + assert item_code == "Butter" + + # ensure that the item has barcodes + barcodes = frappe.get_all( + "Item Barcode", filters={"parenttype": "Item", "parent": item_code}, pluck="barcode" + ) + assert len(barcodes) > 0 + + # scan barcode and expect increment by 1 + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) + expect(item_count).to_have_text(re.compile("1/")) + + # check that a draft Stock Entry is created + page.get_by_text("SAVE", exact=True).click() + page.wait_for_timeout(1000) + with use_current_db_transaction(): + entries = frappe.get_all( + "Stock Entry", + filters={"work_order": order_id}, + fields=["docstatus"], + ) + assert len(entries) >= 1 + assert entries[0]["docstatus"] == 0 + + # check that the draft Purchase Receipt is submitted + page.get_by_text("TRANSFER", exact=True).click() + page.wait_for_timeout(1000) + with use_current_db_transaction(): + receipts = frappe.get_all( + "Stock Entry", + filters={"work_order": order_id}, + fields=["docstatus"], + ) + assert len(receipts) >= 1 + assert receipts[0]["docstatus"] == 1 + + frappe.db.set_value("BEAM Settings", "Ambrosia Pie Company", "enable_handling_units", 1) + frappe.db.commit() diff --git a/beam/tests/mobile/test_mobile.py b/beam/tests/mobile/test_mobile.py new file mode 100644 index 00000000..de57efa8 --- /dev/null +++ b/beam/tests/mobile/test_mobile.py @@ -0,0 +1,43 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +# To test locally: +# active the virtual environment +# bench start, and then run: +# pytest ./beam/tests/mobile/test_mobile.py --browser firefox --headed --disable-warnings + +import re + +import frappe +import pytest +from playwright.sync_api import expect + +# NOTE: any navigation tests should be done using `expect(page).to_have_url` since +# `page.expect_navigation()` since the latter won't work with Beam's hash-based routes + + +@pytest.mark.order(6) +@pytest.mark.parametrize("route", ["Ship"]) +def test_scan_item_barcode(page, route): + # navigate in the following order: Home -> List -> Form + page.get_by_text(route).click() + page.locator("css=.beam_list-item").first.click() + + # find the first item in the list + item = page.locator("css=.box .beam_list-item").first + item_name, *others = item.inner_text().split("\n") + item_count = page.locator("css=.box .beam_item-count").first + expect(item_count).to_have_text(re.compile("0/")) + + # ensure that the item has barcodes + barcodes = frappe.get_all( + "Item Barcode", filters={"parenttype": "Item", "parent": item_name}, pluck="barcode" + ) + assert len(barcodes) > 0 + + # scan barcode and expect increment by 1 + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) + expect(item_count).to_have_text(re.compile("1/")) diff --git a/beam/tests/mobile/test_receive.py b/beam/tests/mobile/test_receive.py new file mode 100644 index 00000000..fac92732 --- /dev/null +++ b/beam/tests/mobile/test_receive.py @@ -0,0 +1,266 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +# To test locally: +# active the virtual environment +# bench start, and then run: +# pytest ./beam/tests/mobile/test_receive.py --browser firefox --headed --disable-warnings + +import re +from urllib.parse import urlparse + +import frappe +import pytest +from playwright.sync_api import expect + +from beam.tests.test_utils import use_current_db_transaction + +# NOTE: any navigation tests should be done using `expect(page).to_have_url` since +# `page.expect_navigation()` won't work with Beam's hash-based routes + + +@pytest.mark.order(2) +def test_scan_invalid_barcode(page): + page.get_by_text("Receive").click() + page.locator("css=.beam_list-item").first.click() + + # get the selected Purchase Order + parsed_url = urlparse(page.url.replace("#", "")) + path_parts = [p for p in parsed_url.path.split("/") if p] + order_id = path_parts[-1] if path_parts else None + assert order_id + + # find all items in the list + all_item_counts = page.locator("css=.box .beam_item-count") + + # get all item counts before scanning invalid barcode + initial_counts = [] + for i in range(all_item_counts.count()): + count_text = all_item_counts.nth(i).inner_text() + initial_counts.append(count_text) + + # ensure all items start with 0 count + for count in initial_counts: + assert count.startswith("0/"), f"Expected item to start with 0/, but got: {count}" + + # verify there are no existing Purchase Receipts created by test user + with use_current_db_transaction(): + existing_receipts = frappe.get_all( + "Purchase Receipt", filters={"owner": "support@agritheory.dev", "docstatus": 0}, fields=["name"] + ) + assert ( + len(existing_receipts) == 0 + ), f"Found existing draft Purchase Receipts created by test user: {existing_receipts}" + + # scan an invalid barcode that doesn't exist + invalid_barcode = "INVALID_BARCODE_12345" + page.evaluate("barcode => scanner.simulate(window, barcode)", invalid_barcode) + + page.wait_for_timeout(500) + + # verify ALL item counts remain unchanged (all should still start with "0/") + initial_counts = [] + for i in range(all_item_counts.count()): + count_text = all_item_counts.nth(i).inner_text() + initial_counts.append(count_text) + + # ensure all items start with 0 count + for count in initial_counts: + assert count.startswith("0/"), f"Expected item to start with 0/, but got: {count}" + + # verify no draft Purchase Receipt was created by test user + with use_current_db_transaction(): + new_receipts = frappe.get_all( + "Purchase Receipt", filters={"owner": "support@agritheory.dev", "docstatus": 0}, fields=["name"] + ) + assert ( + len(new_receipts) == 0 + ), f"Invalid barcode scan should not create any Purchase Receipts, but found: {new_receipts}" + + +@pytest.mark.order(3) +def test_receive_without_scanning(page): + """Test trying to receive without scanning any items""" + # navigate to a Purchase Order + page.get_by_text("Receive").click() + page.locator("css=.beam_list-item").first.click() + + # get the selected Purchase Order + parsed_url = urlparse(page.url.replace("#", "")) + path_parts = [p for p in parsed_url.path.split("/") if p] + order_id = path_parts[-1] if path_parts else None + assert order_id + + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + + # find all items in the list + all_item_counts = page.locator("css=.box .beam_item-count") + initial_counts = [] + for i in range(all_item_counts.count()): + count_text = all_item_counts.nth(i).inner_text() + initial_counts.append(count_text) + + # ensure all items start with 0 count + for count in initial_counts: + assert count.startswith("0/"), f"Expected item to start with 0/, but got: {count}" + + # count existing Purchase Receipts before attempting to save + with use_current_db_transaction(): + existing_receipts = frappe.get_all( + "Purchase Receipt Item", + filters={"purchase_order": order_id, "item_code": item_code, "owner": "support@agritheory.dev"}, + fields=["docstatus", "received_qty"], + ) + initial_count = len(existing_receipts) + + # try to click SAVE without scanning anything + save_button = page.get_by_text("SAVE", exact=True) + save_button.click() + page.wait_for_timeout(1000) + + # verify no new draft Purchase Receipt was created + with use_current_db_transaction(): + new_receipts = frappe.get_all( + "Purchase Receipt Item", + filters={"purchase_order": order_id, "item_code": item_code, "owner": "support@agritheory.dev"}, + fields=["docstatus", "received_qty"], + ) + final_count = len(new_receipts) + assert ( + final_count == initial_count + ), f"Expected no new receipts, but count changed from {initial_count} to {final_count}" + + +@pytest.mark.order(4) +def test_complete_partial_receipt(page): + # navigate in the following order: Home -> Receive -> Purchase Order + page.get_by_text("Receive").click() + page.locator("css=.beam_list-item").first.click() + + # get the selected Purchase Order + # NOTE: URL format changed: the id lives in the path after the hash (e.g. #/purchase-receipt/PUR-ORD-...) + # this PR changed the URL format: + # https://github.com/agritheory/beam/pull/274 + parsed_url = urlparse(page.url.replace("#", "")) + path_parts = [p for p in parsed_url.path.split("/") if p] + order_id = path_parts[-1] if path_parts else None + + assert order_id + + # find the first item in the list + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + item_count = page.locator("css=.box .beam_item-count").first + expect(item_count).to_have_text(re.compile("0/")) + + assert item_code == "Cloudberry" + + # Refresh transaction to see setup data + with use_current_db_transaction(): + # ensure that the item has barcodes + barcodes = frappe.get_all( + "Item Barcode", filters={"parenttype": "Item", "parent": item_code}, pluck="barcode" + ) + assert len(barcodes) > 0 + + # scan barcode and expect increment by 1 + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) + expect(item_count).to_have_text(re.compile("1/")) + + # ensure there are no existing Purchase Receipts against this Purchase Order + receipt = frappe.db.exists( + "Purchase Receipt Item", + { + "docstatus": 0, + "purchase_order": order_id, + "item_code": item_code, + "owner": "support@agritheory.dev", + }, + ) + assert not receipt + + # check that a draft Purchase Receipt is created + page.get_by_text("SAVE", exact=True).click() + page.wait_for_timeout(1000) + with use_current_db_transaction(): + receipts = frappe.get_all( + "Purchase Receipt Item", + filters={"purchase_order": order_id, "item_code": item_code}, + fields=["docstatus", "received_qty", "creation", "parent"], + order_by="creation desc", + ) + assert len(receipts) >= 1 + assert receipts[0]["docstatus"] == 0 + assert receipts[0]["received_qty"] == 1 + + # check that the draft Purchase Receipt is submitted + page.get_by_text("RECEIVE", exact=True).click() + page.wait_for_timeout(1500) + with use_current_db_transaction(): + receipts = frappe.get_all( + "Purchase Receipt Item", + filters={"purchase_order": order_id, "item_code": item_code}, + fields=["docstatus", "received_qty", "creation"], + order_by="creation desc", + limit=1, + ) + assert len(receipts) == 1 + assert receipts[0]["docstatus"] == 1 + assert receipts[0]["received_qty"] == 1 + + +@pytest.mark.order(5) +def test_rapid_barcode_scanning(page): + """Test scanning multiple barcodes quickly""" + # navigate to a Purchase Order + page.get_by_text("Receive").click() + page.locator("css=.beam_list-item").first.click() + + # get the selected Purchase Order + parsed_url = urlparse(page.url.replace("#", "")) + path_parts = [p for p in parsed_url.path.split("/") if p] + order_id = path_parts[-1] if path_parts else None + assert order_id + + # find the first item in the list + item = page.locator("css=.box .beam_list-item").first + item_code, *others = item.inner_text().split("\n") + item_count = page.locator("css=.box .beam_item-count").first + expect(item_count).to_have_text(re.compile("0/")) + + # get barcode for the item + with use_current_db_transaction(): + barcodes = frappe.get_all( + "Item Barcode", filters={"parenttype": "Item", "parent": item_code}, pluck="barcode" + ) + assert len(barcodes) > 0 + + # scan the same barcode multiple times quickly + scan_count = 10 + for _ in range(scan_count): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcodes[0]) + # very short delay between scans to simulate rapid scanning + page.wait_for_timeout(100) + + page.wait_for_timeout(1000) + + expect(item_count).to_have_text(re.compile(f"{scan_count}/")) + + page.get_by_text("SAVE", exact=True).click() + page.wait_for_timeout(1000) + + with use_current_db_transaction(): + receipts = frappe.get_all( + "Purchase Receipt Item", + filters={"purchase_order": order_id, "item_code": item_code}, + fields=["docstatus", "received_qty"], + order_by="creation desc", + limit=1, + ) + assert len(receipts) == 1 + assert receipts[0]["docstatus"] == 0 + assert receipts[0]["received_qty"] == scan_count diff --git a/beam/tests/mobile/test_repack.py b/beam/tests/mobile/test_repack.py new file mode 100644 index 00000000..25f66033 --- /dev/null +++ b/beam/tests/mobile/test_repack.py @@ -0,0 +1,303 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import re + +import frappe +import pytest +from playwright.sync_api import expect + +from beam.tests.test_utils import use_current_db_transaction + + +def fill_warehouse_dropdown(page, label: str, value: str): + wrapper = page.locator(".input-wrapper", has=page.locator("label", has_text=label)) + inp = wrapper.locator("input") + inp.click() + inp.fill(value) + page.wait_for_timeout(300) + result = wrapper.locator("li.autocomplete-result", has_text=value).first + result.wait_for(state="visible") + result.click() + + +@pytest.fixture(autouse=True, scope="session") +def disable_handling_unit_for_tests(): + """Disable handling unit validation for all items during tests.""" + items = frappe.get_all("Item", filters={"enable_handling_unit": 1}, pluck="name") + for item in items: + frappe.db.set_value("Item", item, "enable_handling_unit", 0) + frappe.db.commit() + yield + for item in items: + frappe.db.set_value("Item", item, "enable_handling_unit", 1) + frappe.db.commit() + + +@pytest.mark.order(8) +def test_repack_items_manually(page): + page.get_by_text("Repack").click() + expect(page).to_have_url(re.compile(r"#/repack"), timeout=15000) + + with use_current_db_transaction(): + source_barcode = frappe.get_all( + "Item Barcode", + filters={"parent": "Butter"}, + pluck="barcode", + limit=1, + ) + assert source_barcode, "Butter must have a barcode" + + finished_barcode = frappe.get_all( + "Item Barcode", + filters={"parent": "Ambrosia Pie"}, + pluck="barcode", + limit=1, + ) + assert finished_barcode, "Ambrosia Pie must have a barcode" + + source_wh = "Refrigerator - APC" + target_wh = "Baked Goods - APC" + + qty_input = page.locator("input.aform_input-field[type='number']") + expect(qty_input).to_have_value("0") + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", source_barcode[0]) + + page.wait_for_timeout(800) + + expect(qty_input).to_have_value("1") + + page.get_by_role("button", name="+").click() + + expect(qty_input).to_have_value("2") + + page.get_by_role("button", name="-").click() + + expect(qty_input).to_have_value("1") + + fill_warehouse_dropdown(page, "Source Warehouse", source_wh) + page.get_by_role("button", name="ADD", exact=True).click() + + expect(page.locator("css=.beam_list-item").first).to_be_visible() + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", finished_barcode[0]) + + page.wait_for_timeout(800) + + page.get_by_role("button", name="+").click() + fill_warehouse_dropdown(page, "Target Warehouse", target_wh) + page.get_by_role("button", name="ADD", exact=True).click() + + expect(page.locator("css=.beam_list-item").nth(1)).to_be_visible() + + page.get_by_role("button", name="SAVE", exact=True).click() + page.wait_for_timeout(1500) + + with use_current_db_transaction(): + entries = frappe.get_all( + "Stock Entry", + filters={"stock_entry_type": "Repack", "docstatus": 0}, + fields=["name"], + order_by="creation desc", + limit=1, + ) + assert entries, "Expected draft Stock Entry to be created" + stock_entry_name = entries[0]["name"] + + page.get_by_role("button", name="REPACK", exact=True).click() + page.wait_for_timeout(1500) + + with use_current_db_transaction(): + submitted = frappe.get_all( + "Stock Entry", + filters={"name": stock_entry_name, "docstatus": 1}, + fields=["name"], + ) + + assert submitted, f"Expected Stock Entry {stock_entry_name} to be submitted" + + +@pytest.mark.order(9) +def test_repack_using_bom(page): + page.get_by_text("Repack").click() + page.wait_for_load_state("networkidle") + assert "/repack" in page.url + + bom_name = "BOM-Gooseberry Pie Filling-001" + target_wh = "Refrigerator - APC" + + with use_current_db_transaction(): + finished_barcode = frappe.get_all( + "Item Barcode", + filters={"parent": "Gooseberry Pie"}, + pluck="barcode", + limit=1, + ) + assert finished_barcode, "Gooseberry Pie must have a barcode" + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", finished_barcode[0]) + + page.wait_for_timeout(800) + + page.get_by_role("button", name="+").click() + page.wait_for_timeout(300) + + fill_warehouse_dropdown(page, "BOM (Optional)", bom_name) + page.wait_for_timeout(1000) + + fill_warehouse_dropdown(page, "Target Warehouse", target_wh) + + page.get_by_role("button", name="ADD", exact=True).click() + page.wait_for_timeout(1000) + + page.get_by_role("button", name="SAVE", exact=True).click() + page.wait_for_timeout(1500) + + with use_current_db_transaction(): + entries = frappe.get_all( + "Stock Entry", + filters={"stock_entry_type": "Repack", "docstatus": 0}, + fields=["name"], + order_by="creation desc", + limit=1, + ) + assert entries, "Expected a draft Stock Entry to be created from BOM repack" + + +@pytest.mark.order(10) +def test_scan_item_for_repack(page): + page.get_by_text("Repack").click() + page.wait_for_load_state("networkidle") + assert "/repack" in page.url + + with use_current_db_transaction(): + item_barcodes = frappe.get_all( + "Item Barcode", + filters={"parenttype": "Item"}, + fields=["parent", "barcode"], + limit=1, + ) + assert item_barcodes, "No Item barcodes found in test data" + item_code = item_barcodes[0]["parent"] + barcode = item_barcodes[0]["barcode"] + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcode) + page.wait_for_timeout(800) + + item_wrapper = page.locator( + ".input-wrapper", has=page.locator("label", has_text="Item to Repack") + ) + expect(item_wrapper.locator("input")).to_have_value(item_code) + + qty_input = page.locator("input.aform_input-field[type='number']") + expect(qty_input).to_have_value("1") + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcode) + page.wait_for_timeout(800) + + expect(qty_input).to_have_value("2") + + +@pytest.mark.order(11) +def test_clear_repack_form(page): + page.get_by_text("Repack").click() + page.wait_for_load_state("networkidle") + assert "/repack" in page.url + + with use_current_db_transaction(): + item_barcodes = frappe.get_all( + "Item Barcode", + filters={"parenttype": "Item"}, + fields=["parent", "barcode"], + limit=1, + ) + assert item_barcodes, "No Item barcodes found in test data" + barcode = item_barcodes[0]["barcode"] + + warehouse = frappe.get_all( + "Warehouse", + filters={"is_group": 0, "company": "Ambrosia Pie Company"}, + pluck="name", + limit=1, + )[0] + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcode) + page.wait_for_timeout(800) + + page.get_by_role("button", name="+").click() + fill_warehouse_dropdown(page, "Source Warehouse", warehouse) + + page.get_by_role("button", name="ADD", exact=True).click() + page.wait_for_timeout(500) + + expect(page.locator("css=.beam_list-item").first).to_be_visible() + expect(page.get_by_role("button", name="CLEAN", exact=True)).to_be_visible() + + page.get_by_role("button", name="CLEAN", exact=True).click() + page.wait_for_timeout(500) + + expect(page.get_by_text("Scan Items, Select Warehouses, and Set Qty to Begin")).to_be_visible() + + expect(page.get_by_role("button", name="CLEAN", exact=True)).to_be_hidden() + + +@pytest.mark.order(12) +def test_repack_validation_single_warehouse_direction(page): + page.get_by_text("Repack").click() + page.wait_for_load_state("networkidle") + assert "/repack" in page.url + + with use_current_db_transaction(): + item_barcodes = frappe.get_all( + "Item Barcode", + filters={"parenttype": "Item"}, + fields=["parent", "barcode"], + limit=1, + ) + assert item_barcodes, "No Item barcodes found in test data" + barcode = item_barcodes[0]["barcode"] + + warehouses = frappe.get_all( + "Warehouse", + filters={"is_group": 0, "company": "Ambrosia Pie Company"}, + pluck="name", + limit=2, + ) + assert len(warehouses) >= 2, "Need at least 2 warehouses for this test" + + with page.expect_request( + lambda request: request.headers.get("x-frappe-cmd") == "beam.beam.scan.scan" + ): + page.evaluate("barcode => scanner.simulate(window, barcode)", barcode) + page.wait_for_timeout(800) + + page.get_by_role("button", name="+").click() + + fill_warehouse_dropdown(page, "Source Warehouse", warehouses[0]) + fill_warehouse_dropdown(page, "Target Warehouse", warehouses[1]) + + page.get_by_role("button", name="ADD", exact=True).click() + page.wait_for_timeout(500) + + expect(page.get_by_text("Please select only source or target warehouse")).to_be_visible() + + expect(page.get_by_text("Scan Items, Select Warehouses, and Set Qty to Begin")).to_be_visible() diff --git a/beam/tests/setup.py b/beam/tests/setup.py index 547b0105..c17f8369 100644 --- a/beam/tests/setup.py +++ b/beam/tests/setup.py @@ -5,14 +5,27 @@ from itertools import groupby import frappe +from erpnext.buying.doctype.purchase_order.purchase_order import ( + make_purchase_invoice, + make_purchase_receipt, +) from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, ) +from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse from erpnext.setup.utils import enable_all_roles_and_domains, set_defaults_for_tests from erpnext.stock.get_item_details import get_item_details from frappe.desk.page.setup_wizard.setup_wizard import setup_complete -from beam.tests.fixtures import boms, customers, items, operations, suppliers, workstations +from beam.tests.fixtures import ( + boms, + customers, + employees, + items, + operations, + suppliers, + workstations, +) def before_test(): @@ -40,11 +53,10 @@ def before_test(): enable_all_roles_and_domains() set_defaults_for_tests() frappe.db.commit() - create_test_data() - for modu in frappe.get_all("Module Onboarding"): - frappe.db.set_value("Module Onboarding", modu, "is_complete", 1) + for module in frappe.get_all("Module Onboarding"): + frappe.db.set_value("Module Onboarding", module, "is_complete", True) frappe.set_value("Website Settings", "Website Settings", "home_page", "login") - frappe.db.commit() + create_test_data() def create_test_data(): @@ -71,17 +83,19 @@ def create_test_data(): company_address.city = "Chelsea" company_address.state = "MA" company_address.pincode = "89077" - company_address.is_your_company_address = 1 + company_address.is_your_company_address = True company_address.append("links", {"link_doctype": "Company", "link_name": settings.company}) company_address.save() frappe.set_value("Company", settings.company, "tax_id", "04-1871930") create_warehouses(settings) setup_manufacturing_settings(settings) create_workstations() + setup_beam_settings(settings) create_operations() create_item_groups(settings) create_suppliers(settings) create_customers(settings) + create_employees(settings) create_items(settings) create_boms(settings) prod_plan_from_doc = "Sales Order" @@ -143,11 +157,11 @@ def create_customers(settings): def setup_manufacturing_settings(settings): mfg_settings = frappe.get_doc("Manufacturing Settings", "Manufacturing Settings") - mfg_settings.material_consumption = 1 + mfg_settings.material_consumption = True mfg_settings.default_wip_warehouse = "Kitchen - APC" mfg_settings.default_fg_warehouse = "Baked Goods - APC" mfg_settings.overproduction_percentage_for_work_order = 5.00 - mfg_settings.job_Card_excess_transfer = 1 + mfg_settings.job_card_excess_transfer = True mfg_settings.save() if frappe.db.exists("Account", {"account_name": "Work In Progress", "company": settings.company}): @@ -177,6 +191,36 @@ def setup_manufacturing_settings(settings): frappe.set_value("Warehouse", "Kitchen - APC", "account", wip.name) +def setup_beam_settings(settings): + if frappe.db.exists("BEAM Settings", settings.company): + beams = frappe.get_doc("BEAM Settings", settings.company) + else: + beams = frappe.new_doc("BEAM Settings") + beams.company = settings.company + beams.enable_demand = True + beams.enable_handling_units = True + beams.receiving_workstation = "Receiving" + beams.shipping_workstation = "Shipping" + beams.set("warehouse_types", [{"warehouse_type": "Quarantine"}]) + beams.set( + "routes", + [ + { + "label": "Manufacture", + "route": "#/manufacture", + "dt": "Stock Entry", + "component": "Manufacture", + }, + {"label": "Demand", "route": "#/demand", "dt": "Stock Entry", "component": "Demand"}, + {"label": "Move", "route": "#/move", "dt": "Stock Entry", "component": "Demand"}, + {"label": "Receive", "route": "#/receive", "dt": "Purchase Receipt", "component": "Receive"}, + {"label": "Ship", "route": "#/ship", "dt": "Delivery Note", "component": "Ship"}, + {"label": "Repack", "route": "#/repack", "dt": "Stock Entry", "component": "Repack"}, + ], + ) + beams.save() + + def create_workstations(): for ws in workstations: if frappe.db.exists("Workstation", ws[0]): @@ -219,21 +263,21 @@ def create_items(settings): if not frappe.db.exists("Price List", "Bakery Buying"): pl = frappe.new_doc("Price List") pl.price_list_name = "Bakery Buying" - pl.buying = 1 + pl.buying = True pl.append("countries", {"country": "United States"}) pl.save() if not frappe.db.exists("Price List", "Bakery Wholesale"): pl = frappe.new_doc("Price List") pl.price_list_name = "Bakery Wholesale" - pl.selling = 1 + pl.selling = True pl.append("countries", {"country": "United States"}) pl.save() if not frappe.db.exists("Pricing Rule", "Bakery Retail"): pr = frappe.new_doc("Pricing Rule") pr.title = "Bakery Retail" - pr.selling = 1 + pr.selling = True pr.apply_on = "Item Group" pr.company = settings.company pr.margin_type = "Percentage" @@ -251,16 +295,23 @@ def create_items(settings): i.item_group = item.get("item_group") i.stock_uom = item.get("uom") i.description = item.get("description") - i.maintain_stock = 1 - i.enable_handling_unit = 0 if i.item_code in ("Water", "Ice Water") else 1 - i.include_item_in_manufacturing = 1 + i.maintain_stock = True + i.enable_handling_unit = i.item_code not in ("Water", "Ice Water") + i.include_item_in_manufacturing = True i.default_warehouse = settings.get("warehouse") i.default_material_request_type = ( "Purchase" if item.get("item_group") in ("Bakery Supplies", "Ingredients") else "Manufacture" ) i.valuation_method = "FIFO" - i.is_purchase_item = 1 if item.get("item_group") in ("Bakery Supplies", "Ingredients") else 0 - i.is_sales_item = 1 if item.get("item_group") == "Baked Goods" else 0 + i.is_purchase_item = ( + 1 + if item.get("item_group") in ("Bakery Supplies", "Ingredients") + or item.get("is_purchase_item", 0) + else 0 + ) + i.is_sales_item = ( + 1 if item.get("item_group") == "Baked Goods" or item.get("is_sales_item", 0) else 0 + ) i.append( "item_defaults", {"company": settings.company, "default_warehouse": item.get("default_warehouse")}, @@ -270,13 +321,20 @@ def create_items(settings): if i.item_code == "Parchment Paper": i.append("uoms", {"uom": "Box", "conversion_factor": 100}) i.purchase_uom = "Box" + if i.item_code in ("Water", "Ice Water"): + i.append("uoms", {"uom": "Gallon Liquid (US)", "conversion_factor": 15.142}) + i.purchase_uom = "Gallon Liquid (US)" + i.valuation_rate = 0.01 if i.item_code == "Water" else 0.02 + + i.has_serial_no = item.get("has_serial_no", 0) or 0 + i.serial_no_series = item.get("serial_no_series", "") or "" i.save() if item.get("item_price"): ip = frappe.new_doc("Item Price") ip.item_code = i.item_code ip.uom = i.stock_uom ip.price_list = "Bakery Wholesale" if i.is_sales_item else "Bakery Buying" - ip.buying = 1 + ip.buying = True ip.valid_from = "2018-1-1" ip.price_list_rate = item.get("item_price") ip.save() @@ -287,20 +345,22 @@ def create_items(settings): "items", { "item_code": "Water", - "qty": 10000000, + "qty": 9, # intentionally to help with demand tests "t_warehouse": "Refrigerator - APC", - "basic_rate": 0.0, - "allow_zero_valuation_rate": 1, + "uom": "Cup", + "basic_rate": 0.15, + "expense_account": "5111 - Cost of Goods Sold - APC", }, ) water.append( "items", { "item_code": "Ice Water", - "qty": 10000000, + "qty": 11, # intentionally to help with demand tests + "uom": "Cup", "t_warehouse": "Refrigerator - APC", - "basic_rate": 0.0, - "allow_zero_valuation_rate": 1, + "basic_rate": 0.30, + "expense_account": "5111 - Cost of Goods Sold - APC", }, ) water.save() @@ -323,6 +383,90 @@ def create_warehouses(settings): wh.parent_warehouse = root_wh wh.company = settings.company wh.save() + create_quarantine_warehouse(settings, parent_wh=root_wh) + + +# TODO: replace with test utils functionality +def create_quarantine_warehouse( + settings, + wh_name="Quarantined, Scrap and Rejected Items", + account_name=None, + parent_account=None, + account_number="1430", + parent_wh=None, + is_default_scrap_wh=True, +): + if not account_name: + if not parent_account: + # If one possible parent account in system, use it, if zero or 2+, account is standalone + parent_accts = frappe.get_all( + "Account", + { + "company": settings.company, + "root_type": "Asset", + "account_type": "Stock", + "is_group": 1, + }, + "name", + pluck="name", + ) + parent_account = parent_accts[0] if len(parent_accts) == 1 else "" + + if not frappe.db.exists( + "Account", + { + "name": wh_name, + "company": settings.company, + "root_type": "Asset", + "account_type": "Stock", + }, + ): + a = frappe.new_doc("Account") + a.name = a.account_name = wh_name + a.account_number = account_number + a.is_group = 0 + a.company = settings.company + a.root_type = "Asset" + a.report_type = "Balance Sheet" + a.account_currency = frappe.get_value("Company", settings.company, "default_currency") + a.parent_account = parent_account + a.account_type = "Stock" + a.save() + account_name = a.name + + if not parent_wh: + parent_wh = frappe.get_value("Warehouse", {"company": settings.company, "is_group": 1}) + + wh_type = "Quarantine" + if not frappe.db.exists("Warehouse Type", wh_type): + wht = frappe.new_doc("Warehouse Type") + wht.name = wh_type + wht.save() + + if not frappe.db.exists( + "Warehouse", + { + "warehouse_name": wh_name, + "company": settings.company, + "is_rejected_warehouse": 1, + "account": account_name, + }, + ): + wh = frappe.new_doc("Warehouse") + wh.warehouse_name = wh_name + wh.company = settings.company + wh.is_group = 0 + wh.parent_warehouse = parent_wh + wh.is_rejected_warehouse = 1 + wh.account = account_name + wh.warehouse_type = wh_type + wh.save() + wh_name = wh.name + + if is_default_scrap_wh: + ms = frappe.get_doc("Manufacturing Settings") + ms.default_scrap_warehouse = wh_name + ms.save() def create_boms(settings): @@ -337,7 +481,7 @@ def create_boms(settings): b.rm_cost_as_per = "Price List" b.buying_price_list = "Bakery Buying" b.currency = "USD" - b.with_operations = 1 + b.with_operations = True for item in bom.get("items"): b.append("items", {**item, "stock_uom": item.get("uom")}) b.items[-1].bom_no = frappe.get_value("BOM", {"item": item.get("item_code")}) @@ -370,7 +514,7 @@ def create_sales_order(settings): "items", { "item_code": "Double Plum Pie", - "delivery_date": so.transaction_date, + "delivery_date": so.transaction_date + datetime.timedelta(days=1), "qty": 40, "warehouse": "Baked Goods - APC", }, @@ -379,7 +523,7 @@ def create_sales_order(settings): "items", { "item_code": "Gooseberry Pie", - "delivery_date": so.transaction_date, + "delivery_date": so.transaction_date + datetime.timedelta(days=2), "qty": 10, "warehouse": "Baked Goods - APC", }, @@ -388,7 +532,7 @@ def create_sales_order(settings): "items", { "item_code": "Kaduka Key Lime Pie", - "delivery_date": so.transaction_date, + "delivery_date": so.transaction_date + datetime.timedelta(days=3), "qty": 10, "warehouse": "Baked Goods - APC", }, @@ -415,7 +559,7 @@ def create_material_request(settings): "items", { "item_code": "Double Plum Pie", - "schedule_date": mr.schedule_date, + "schedule_date": mr.schedule_date + datetime.timedelta(days=1), "qty": 40, "warehouse": "Baked Goods - APC", }, @@ -424,7 +568,7 @@ def create_material_request(settings): "items", { "item_code": "Gooseberry Pie", - "schedule_date": mr.schedule_date, + "schedule_date": mr.schedule_date + datetime.timedelta(days=2), "qty": 10, "warehouse": "Baked Goods - APC", }, @@ -433,7 +577,7 @@ def create_material_request(settings): "items", { "item_code": "Kaduka Key Lime Pie", - "schedule_date": mr.schedule_date, + "schedule_date": mr.schedule_date + datetime.timedelta(days=3), "qty": 10, "warehouse": "Baked Goods - APC", }, @@ -446,7 +590,7 @@ def create_production_plan(settings, prod_plan_from_doc): pp = frappe.new_doc("Production Plan") pp.posting_date = settings.day pp.company = settings.company - pp.combine_sub_items = 1 + pp.combine_sub_items = True if prod_plan_from_doc == "Sales Order": pp.get_items_from = "Sales Order" pp.append( @@ -465,12 +609,20 @@ def create_production_plan(settings, prod_plan_from_doc): }, ) pp.get_mr_items() - for item in pp.po_items: - item.planned_start_date = settings.day + + pp.po_items = sorted(pp.po_items, key=lambda x: x.get("item_code")) + + for idx, item in enumerate(pp.po_items): + item.planned_start_date = settings.day + datetime.timedelta(days=idx) + pp.get_sub_assembly_items() + start_time = datetime.datetime(settings.day.year, settings.day.month, settings.day.day, 0, 0) for item in pp.sub_assembly_items: - item.schedule_date = settings.day + item.schedule_date = start_time + time = frappe.get_value("BOM Operation", {"parent": item.bom_no}, "sum(time_in_mins) AS time") + start_time += datetime.timedelta(minutes=time + 2) pp.for_warehouse = "Storeroom - APC" + pp.sub_assembly_items = sorted(pp.sub_assembly_items, key=lambda x: x.get("production_item")) raw_materials = get_items_for_material_requests( pp.as_dict(), warehouses=None, get_parent_warehouse_data=None ) @@ -501,54 +653,83 @@ def create_production_plan(settings, prod_plan_from_doc): sorted((m for m in mr.items if m.supplier), key=lambda d: d.supplier), lambda x: x.get("supplier"), ): - items = list(_items) if supplier == "No Supplier": - # make a stock entry here? continue - if supplier == "Freedom Provisions": - pr = frappe.new_doc("Purchase Invoice") - pr.update_stock = 1 - else: - pr = frappe.new_doc("Purchase Receipt") - pr.company = settings.company - pr.supplier = supplier - pr.posting_date = settings.day - pr.set_posting_time = 1 - pr.buying_price_list = "Bakery Buying" + items = list(_items) + po = frappe.new_doc("Purchase Order") + po.company = settings.company + po.supplier = supplier + po.transaction_date = po.schedule_date = settings.day + po.buying_price_list = "Bakery Buying" for item in items: item_details = get_item_details( { "item_code": item.item_code, "qty": item.qty, - "supplier": pr.supplier, - "company": pr.company, - "doctype": pr.doctype, - "currency": pr.currency, - "buying_price_list": pr.buying_price_list, + "supplier": po.supplier, + "company": po.company, + "doctype": po.doctype, + "currency": po.currency, + "buying_price_list": po.buying_price_list, } ) - pr.append("items", {**item_details}) + po.append("items", {**item_details}) + po.save() + po.submit() + + if supplier == "Freedom Provisions": + pr = make_purchase_invoice(po.name) + pr.update_stock = True + else: + pr = make_purchase_receipt(po.name) + + pr.set_posting_time = True + pr.posting_date = settings.day pr.save() # pr.submit() # don't submit - needed to test handling unit generation - pp.make_work_order() - wos = frappe.get_all("Work Order", {"production_plan": pp.name}) + wo_list, po_list = [], [] + subcontracted_po = {} + default_warehouses = get_default_warehouse() + pp.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses) + pp.make_work_order_for_finished_goods(wo_list, default_warehouses) + wos = frappe.get_all("Work Order", {"production_plan": pp.name}, order_by="name ASC") + start_time = datetime.datetime(settings.day.year, settings.day.month, settings.day.day, 0, 0) for wo in wos: wo = frappe.get_doc("Work Order", wo) wo.wip_warehouse = "Kitchen - APC" + wo.actual_start_date = wo.planned_start_date = start_time + wo.required_items = sorted(wo.required_items, key=lambda x: x.get("item_code")) + for idx, w in enumerate(wo.required_items, start=1): + w.idx = idx wo.save() wo.submit() - job_cards = frappe.get_all("Job Card", {"work_order": wo.name}) - for job_card in job_cards: - job_card = frappe.get_doc("Job Card", job_card) + frappe.db.set_value("Work Order", wo.name, "creation", start_time) + # Get job cards and sort by sequence_id to process in order + job_cards = frappe.get_all( + "Job Card", {"work_order": wo.name}, ["name", "sequence_id"], order_by="sequence_id asc" + ) + for jc in job_cards: + job_card = frappe.get_doc("Job Card", jc.name) + batch_size, total_operation_time = frappe.get_value( + "Operation", job_card.operation, ["batch_size", "total_operation_time"] + ) + time_in_mins = (total_operation_time / batch_size) * wo.qty job_card.append( "time_logs", { - "completed_qty": wo.qty, + # "completed_qty": wo.qty, + "from_time": start_time, + "to_time": start_time + datetime.timedelta(minutes=time_in_mins), + "time_in_mins": time_in_mins, + "remaining_time_in_mins": time_in_mins, }, ) + # Complete the job card + job_card.total_completed_qty = wo.qty job_card.save() - job_card.submit() + start_time = job_card.time_logs[0].to_time + datetime.timedelta(minutes=2) + # job_card.submit() # TODO: don't submit for demand tests def create_purchase_receipt_for_received_qty_test(settings): @@ -556,7 +737,7 @@ def create_purchase_receipt_for_received_qty_test(settings): pr.company = settings.company pr.supplier = "Freedom Provisions" pr.posting_date = settings.day - pr.set_posting_time = 1 + pr.set_posting_time = True pr.buying_price_list = "Bakery Buying" item = frappe.get_doc("Item", "Gooseberry") pr.append( @@ -592,3 +773,44 @@ def create_network_printer_settings(settings): nps.port = ps["port"] nps.printer_name = ps["name"] nps.save() + + +def create_employees(settings, only_create=None): + for employee in employees: + if only_create and employee.get("employee_name") not in only_create: + continue + + if frappe.db.exists("Employee", {"employee_name": employee.get("employee_name")}): + continue + + if not frappe.db.exists("Designation", employee.get("designation")): + desg = frappe.new_doc("Designation") + desg.designation_name = employee.get("designation") + desg.save() + + empl = frappe.new_doc("Employee") + name = employee.pop("name") + empl.first_name = name.split(" ")[0] + empl.last_name = name.split(" ")[1] + empl.update(employee) + empl.reports_to = None + if settings.company: + empl.company = settings.company + empl.save() + + user = frappe.new_doc("User") + user.email = f"{empl.first_name[0].lower()}{empl.last_name.lower()}@cfc.co" + user.first_name = empl.first_name + user.last_name = empl.last_name + user.send_welcome_email = 0 + user.enabled = 1 + user.language = settings.language + user.time_zone = settings.time_zone + for r in employee.get("roles", []): + user.append("roles", {"role": r}) + + user.save() + empl.user_id = user.email + if employee.get("reports_to"): + empl.reports_to = frappe.get_value("Employee", {"employee_name": employee.get("reports_to")}) + empl.save() diff --git a/beam/tests/test_barcode_auto_generate.py b/beam/tests/test_barcode_auto_generate.py new file mode 100644 index 00000000..1115a780 --- /dev/null +++ b/beam/tests/test_barcode_auto_generate.py @@ -0,0 +1,74 @@ +# Copyright (c) 2025, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +import pytest + +from beam.beam.barcodes import create_beam_barcode +from beam.beam.doctype.beam_settings.beam_settings import get_doctypes_with_item_barcodes + + +def test_get_doctypes_with_item_barcodes(): + doctypes = get_doctypes_with_item_barcodes() + assert isinstance(doctypes, list) + assert "Item" in doctypes + assert "Warehouse" in doctypes + # all returned values must be real doctypes + for dt in doctypes: + assert frappe.db.exists("DocType", dt), f"Stale DocField reference: '{dt}' does not exist" + + +def _make_item(item_code): + if frappe.db.exists("Item", item_code): + item = frappe.get_doc("Item", item_code) + item.barcodes = [] + return item + item = frappe.new_doc("Item") + item.item_code = item_code + item.item_name = item_code + item.item_group = "All Item Groups" + item.stock_uom = "Nos" + item.is_stock_item = 1 + return item + + +@pytest.fixture() +def beam_settings(): + company = frappe.defaults.get_defaults().get("company") + settings = frappe.get_doc("BEAM Settings", {"company": company}) + original = settings.auto_barcode_doctypes + yield settings + settings.auto_barcode_doctypes = original + settings.save() + + +def test_barcode_generated_when_doctype_allowed(beam_settings): + beam_settings.auto_barcode_doctypes = '["Item", "Warehouse"]' + beam_settings.save() + + item = _make_item("_Test Barcode Allow Item") + create_beam_barcode(item) + + assert any(b.barcode_type == "Code128" for b in item.barcodes) + + +def test_barcode_not_generated_when_doctype_not_allowed(beam_settings): + beam_settings.auto_barcode_doctypes = '["Warehouse"]' + beam_settings.save() + + item = _make_item("_Test Barcode Disallow Item") + create_beam_barcode(item) + + assert not any(b.barcode_type == "Code128" for b in item.barcodes) + + +def test_barcode_not_duplicated_when_code128_exists(beam_settings): + beam_settings.auto_barcode_doctypes = '["Item", "Warehouse"]' + beam_settings.save() + + item = _make_item("_Test Barcode Dedup Item") + item.append("barcodes", {"barcode": "12345678901234567890", "barcode_type": "Code128"}) + create_beam_barcode(item) + + code128_barcodes = [b for b in item.barcodes if b.barcode_type == "Code128"] + assert len(code128_barcodes) == 1 diff --git a/beam/tests/test_demand.py b/beam/tests/test_demand.py new file mode 100644 index 00000000..10aed730 --- /dev/null +++ b/beam/tests/test_demand.py @@ -0,0 +1,427 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import random +from datetime import datetime +from pathlib import Path + +import frappe +import pytest +from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note +from frappe.utils import add_days, get_site_path, today + +from beam.beam.demand.demand import ( + build_demand_allocation_map, + get_demand, + get_manufacturing_demand, + get_sales_demand, +) +from beam.tests.fixtures import customers + +# TODO: +# configure rejected warehouse and make sure its under test for demand +# debug allocation issues when +# add filters to Demand Map: manufactured items, purchased items, finished goods + +current_year = datetime.now().year + + +@pytest.mark.order(1) +def test_opening_demand(): + # destroy and reset + demand_db_path = Path(f"{get_site_path()}/demand.db").resolve() + if demand_db_path.exists(): + demand_db_path.unlink(missing_ok=True) + + sales_demand = get_sales_demand() + assert sales_demand[0].item_code == "Ambrosia Pie" + assert sales_demand[1].item_code == "Double Plum Pie" + assert sales_demand[2].item_code == "Gooseberry Pie" + assert sales_demand[3].item_code == "Kaduka Key Lime Pie" + + wos = frappe.get_all("Work Order", ["production_item"], order_by="name ASC") + + assert wos[0].get("production_item") == "Ambrosia Pie Filling" + assert wos[1].get("production_item") == "Double Plum Pie Filling" + assert wos[2].get("production_item") == "Gooseberry Pie Filling" + assert wos[3].get("production_item") == "Kaduka Key Lime Pie Filling" + assert wos[4].get("production_item") == "Pie Crust" + assert wos[5].get("production_item") == "Ambrosia Pie" + assert wos[6].get("production_item") == "Double Plum Pie" + assert wos[7].get("production_item") == "Gooseberry Pie" + assert wos[8].get("production_item") == "Kaduka Key Lime Pie" + + # [print(f"assert wos[{idx}].get('production_item') == '{m.get('production_item')}'") for idx, m in enumerate(wos)] + + manufacturing_demand = get_manufacturing_demand() + # [ + # print( + # f"assert manufacturing_demand[{idx}].get('parent') == '{m.get('parent')}'" + '\n' + + # f"assert manufacturing_demand[{idx}].get('item_code') == '{m.get('item_code')}'" + # ) + # for idx, m in enumerate(manufacturing_demand) + # ] + + # assert frappe.get_value('Work Order', manufacturing_demand[0].get('parent'), 'production_item') == 'Kaduka Key Lime Pie Filling' + assert manufacturing_demand[0].get("parent") == f"MFG-WO-{current_year}-00001" + assert manufacturing_demand[0].get("item_code") == "Butter" + assert manufacturing_demand[1].get("parent") == f"MFG-WO-{current_year}-00001" + assert manufacturing_demand[1].get("item_code") == "Cloudberry" + assert manufacturing_demand[2].get("parent") == f"MFG-WO-{current_year}-00001" + assert manufacturing_demand[2].get("item_code") == "Cornstarch" + assert manufacturing_demand[3].get("parent") == f"MFG-WO-{current_year}-00001" + assert manufacturing_demand[3].get("item_code") == "Hairless Rambutan" + assert manufacturing_demand[4].get("parent") == f"MFG-WO-{current_year}-00001" + assert manufacturing_demand[4].get("item_code") == "Sugar" + assert manufacturing_demand[5].get("parent") == f"MFG-WO-{current_year}-00001" + assert manufacturing_demand[5].get("item_code") == "Tayberry" + assert manufacturing_demand[6].get("parent") == f"MFG-WO-{current_year}-00001" + assert manufacturing_demand[6].get("item_code") == "Water" + assert manufacturing_demand[7].get("parent") == f"MFG-WO-{current_year}-00002" + assert manufacturing_demand[7].get("item_code") == "Butter" + assert manufacturing_demand[8].get("parent") == f"MFG-WO-{current_year}-00002" + assert manufacturing_demand[8].get("item_code") == "Cocoplum" + assert manufacturing_demand[9].get("parent") == f"MFG-WO-{current_year}-00002" + assert manufacturing_demand[9].get("item_code") == "Cornstarch" + assert manufacturing_demand[10].get("parent") == f"MFG-WO-{current_year}-00002" + assert manufacturing_demand[10].get("item_code") == "Damson Plum" + assert manufacturing_demand[11].get("parent") == f"MFG-WO-{current_year}-00002" + assert manufacturing_demand[11].get("item_code") == "Sugar" + assert manufacturing_demand[12].get("parent") == f"MFG-WO-{current_year}-00002" + assert manufacturing_demand[12].get("item_code") == "Water" + assert manufacturing_demand[13].get("parent") == f"MFG-WO-{current_year}-00003" + assert manufacturing_demand[13].get("item_code") == "Butter" + assert manufacturing_demand[14].get("parent") == f"MFG-WO-{current_year}-00003" + assert manufacturing_demand[14].get("item_code") == "Cornstarch" + assert manufacturing_demand[15].get("parent") == f"MFG-WO-{current_year}-00003" + assert manufacturing_demand[15].get("item_code") == "Gooseberry" + assert manufacturing_demand[16].get("parent") == f"MFG-WO-{current_year}-00003" + assert manufacturing_demand[16].get("item_code") == "Sugar" + assert manufacturing_demand[17].get("parent") == f"MFG-WO-{current_year}-00003" + assert manufacturing_demand[17].get("item_code") == "Water" + assert manufacturing_demand[18].get("parent") == f"MFG-WO-{current_year}-00004" + assert manufacturing_demand[18].get("item_code") == "Butter" + assert manufacturing_demand[19].get("parent") == f"MFG-WO-{current_year}-00004" + assert manufacturing_demand[19].get("item_code") == "Cornstarch" + assert manufacturing_demand[20].get("parent") == f"MFG-WO-{current_year}-00004" + assert manufacturing_demand[20].get("item_code") == "Kaduka Lime" + assert manufacturing_demand[21].get("parent") == f"MFG-WO-{current_year}-00004" + assert manufacturing_demand[21].get("item_code") == "Limequat" + assert manufacturing_demand[22].get("parent") == f"MFG-WO-{current_year}-00004" + assert manufacturing_demand[22].get("item_code") == "Sugar" + assert manufacturing_demand[23].get("parent") == f"MFG-WO-{current_year}-00004" + assert manufacturing_demand[23].get("item_code") == "Water" + assert manufacturing_demand[24].get("parent") == f"MFG-WO-{current_year}-00005" + assert manufacturing_demand[24].get("item_code") == "Butter" + assert manufacturing_demand[25].get("parent") == f"MFG-WO-{current_year}-00005" + assert manufacturing_demand[25].get("item_code") == "Flour" + assert manufacturing_demand[26].get("parent") == f"MFG-WO-{current_year}-00005" + assert manufacturing_demand[26].get("item_code") == "Ice Water" + assert manufacturing_demand[27].get("parent") == f"MFG-WO-{current_year}-00005" + assert manufacturing_demand[27].get("item_code") == "Parchment Paper" + assert manufacturing_demand[28].get("parent") == f"MFG-WO-{current_year}-00005" + assert manufacturing_demand[28].get("item_code") == "Pie Tin" + assert manufacturing_demand[29].get("parent") == f"MFG-WO-{current_year}-00005" + assert manufacturing_demand[29].get("item_code") == "Salt" + assert manufacturing_demand[30].get("parent") == f"MFG-WO-{current_year}-00006" + assert manufacturing_demand[30].get("item_code") == "Ambrosia Pie Filling" + assert manufacturing_demand[31].get("parent") == f"MFG-WO-{current_year}-00006" + assert manufacturing_demand[31].get("item_code") == "Pie Box" + assert manufacturing_demand[32].get("parent") == f"MFG-WO-{current_year}-00006" + assert manufacturing_demand[32].get("item_code") == "Pie Crust" + assert manufacturing_demand[33].get("parent") == f"MFG-WO-{current_year}-00007" + assert manufacturing_demand[33].get("item_code") == "Double Plum Pie Filling" + assert manufacturing_demand[34].get("parent") == f"MFG-WO-{current_year}-00007" + assert manufacturing_demand[34].get("item_code") == "Pie Box" + assert manufacturing_demand[35].get("parent") == f"MFG-WO-{current_year}-00007" + assert manufacturing_demand[35].get("item_code") == "Pie Crust" + assert manufacturing_demand[36].get("parent") == f"MFG-WO-{current_year}-00008" + assert manufacturing_demand[36].get("item_code") == "Gooseberry Pie Filling" + assert manufacturing_demand[37].get("parent") == f"MFG-WO-{current_year}-00008" + assert manufacturing_demand[37].get("item_code") == "Pie Box" + assert manufacturing_demand[38].get("parent") == f"MFG-WO-{current_year}-00008" + assert manufacturing_demand[38].get("item_code") == "Pie Crust" + assert manufacturing_demand[39].get("parent") == f"MFG-WO-{current_year}-00009" + assert manufacturing_demand[39].get("item_code") == "Kaduka Key Lime Pie Filling" + assert manufacturing_demand[40].get("parent") == f"MFG-WO-{current_year}-00009" + assert manufacturing_demand[40].get("item_code") == "Pie Box" + assert manufacturing_demand[41].get("parent") == f"MFG-WO-{current_year}-00009" + assert manufacturing_demand[41].get("item_code") == "Pie Crust" + + build_demand_allocation_map() + + # get demand assert that correct quantities and allocations exist + water = get_demand(filters={"item_code": "Water"}) + assert len(water) == 4 + + assert water[0].parent == f"MFG-WO-{current_year}-00001" + assert water[0].total_required_qty == 10.0 + assert water[0].net_required_qty == 1.0 + assert water[0].allocated_qty == 9.0 + assert water[0].warehouse == "Refrigerator - APC" + + assert water[1].parent == f"MFG-WO-{current_year}-00002" + assert water[1].total_required_qty == 10.0 + assert water[1].net_required_qty == 10.0 + assert water[1].allocated_qty == 0.0 + assert water[1].warehouse == "Kitchen - APC" + + assert water[2].parent == f"MFG-WO-{current_year}-00003" + assert water[2].total_required_qty == 2.5 + assert water[2].net_required_qty == 2.5 + assert water[2].allocated_qty == 0.0 + assert water[2].warehouse == "Kitchen - APC" + + assert water[3].parent == f"MFG-WO-{current_year}-00004" + assert water[3].total_required_qty == 2.5 + assert water[3].net_required_qty == 2.5 + assert water[3].allocated_qty == 0.0 + assert water[3].warehouse == "Kitchen - APC" + + ice_water = get_demand(filters={"item_code": "Ice Water"}) + assert len(ice_water) == 1 + + assert ice_water[0].parent == f"MFG-WO-{current_year}-00005" + assert ice_water[0].total_required_qty == 50 + assert ice_water[0].net_required_qty == 39.0 + assert ice_water[0].allocated_qty == 11.0 + assert ice_water[0].warehouse == "Refrigerator - APC" + + +@pytest.mark.order(2) +def test_insufficient_total_demand_scenario(): + # test multiple allocations + se = frappe.new_doc("Stock Entry") + se.stock_entry_type = se.purpose = "Material Receipt" + se.append( + "items", + { + "item_code": "Water", + "qty": 7, + "t_warehouse": "Refrigerator - APC", + "uom": "Cup", + "basic_rate": 0.15, + "expense_account": "5111 - Cost of Goods Sold - APC", + }, + ) + se.append( + "items", + { + "item_code": "Ice Water", + "qty": 100, + "uom": "Cup", + "t_warehouse": "Refrigerator - APC", + "basic_rate": 0.30, + "expense_account": "5111 - Cost of Goods Sold - APC", + }, + ) + se.save() + se.submit() + water = get_demand(filters={"item_code": "Water"}) + assert len(water) == 4 + + assert water[0].parent == f"MFG-WO-{current_year}-00001" + assert water[0].total_required_qty == 10.0 + assert water[0].net_required_qty == 0.0 + assert water[0].allocated_qty == 10.0 + assert water[0].warehouse == "Refrigerator - APC" + + assert water[1].parent == f"MFG-WO-{current_year}-00002" + assert water[1].total_required_qty == 10.0 + assert water[1].net_required_qty == 10.0 + assert water[1].allocated_qty == 0.0 + assert water[1].warehouse == "Kitchen - APC" + + assert water[2].parent == f"MFG-WO-{current_year}-00003" + assert water[2].total_required_qty == 2.5 + assert water[2].net_required_qty == 2.5 + assert water[2].allocated_qty == 0.0 + assert water[2].warehouse == "Kitchen - APC" + + assert water[3].parent == f"MFG-WO-{current_year}-00004" + assert water[3].total_required_qty == 2.5 + assert water[3].net_required_qty == 2.5 + assert water[3].allocated_qty == 0.0 + assert water[3].warehouse == "Kitchen - APC" + + # assert partial allocations + ice_water = get_demand(filters={"item_code": "Ice Water"}) + assert len(ice_water) == 1 + + assert ice_water[0].total_required_qty == 50 + assert ice_water[0].net_required_qty == 0 + assert ice_water[0].allocated_qty == 50 + assert ice_water[0].warehouse == "Refrigerator - APC" + assert ice_water[0].parent == f"MFG-WO-{current_year}-00005" + + +@pytest.mark.order(31) # run after other tests +def test_demand_removal_on_order_cancel(): + # Force rebuild demand allocation map to ensure it's up to date + build_demand_allocation_map() + + pie = get_demand(filters={"item_code": "Ambrosia Pie"}) + assert len(pie) == 1 + + so = frappe.new_doc("Sales Order") + so.customer = random.choice(customers) + so.selling_price_list = "Bakery Wholesale" + so.append( + "items", + { + "item_code": "Ambrosia Pie", + "delivery_date": add_days(today(), 7), + "qty": 10, + "warehouse": "Baked Goods - APC", + }, + ) + so.save() + so.submit() + + pie = get_demand(filters={"item_code": "Ambrosia Pie"}) + assert len(pie) == 2 + + so.cancel() + so.delete() + + pie = get_demand(filters={"item_code": "Ambrosia Pie"}) + assert len(pie) == 1 + + +@pytest.mark.order(32) # run after other tests +def test_allocation_creation_on_delivery(): + se = frappe.new_doc("Stock Entry") + se.stock_entry_type = se.purpose = "Material Receipt" + se.append( + "items", + { + "item_code": "Ambrosia Pie", + "qty": 40, + "t_warehouse": "Baked Goods - APC", + "basic_rate": frappe.get_value("Item Price", {"item_code": "Ambrosia Pie"}, "price_list_rate"), + }, + ) + se.save() + se.submit() + + # assert partial allocations + pie = get_demand(filters={"item_code": "Ambrosia Pie"}) + assert len(pie) == 1 + + assert pie[0].total_required_qty == 40 + assert pie[0].net_required_qty == 0 + assert pie[0].allocated_qty == 40 + assert pie[0].warehouse == "Baked Goods - APC" + assert pie[0].parent == f"SAL-ORD-{current_year}-00001" + + dn = make_delivery_note(f"SAL-ORD-{current_year}-00001") + for item in dn.items[:]: + if item.item_code == "Ambrosia Pie": + item.qty = 5 + else: + dn.remove(item) + dn.save() + dn.submit() + + # assert partial allocations + pie = get_demand(filters={"item_code": "Ambrosia Pie"}) + assert len(pie) == 1 + + assert pie[0].total_required_qty == 35 + assert pie[0].net_required_qty == 0 + assert pie[0].allocated_qty == 35 + assert pie[0].warehouse == "Baked Goods - APC" + assert pie[0].parent == f"SAL-ORD-{current_year}-00001" + + +@pytest.mark.order(33) # run after other tests +def test_allocation_reversal_on_delivery_cancel(): + dn = frappe.get_doc("Delivery Note", f"MAT-DN-{current_year}-00001") + dn.cancel() + + pie = get_demand(filters={"item_code": "Ambrosia Pie"}) + assert len(pie) == 1 + + # demand + allocation from stock entry + assert pie[0].total_required_qty == 40 + assert pie[0].net_required_qty == 0 + assert pie[0].allocated_qty == 40 + assert pie[0].warehouse == "Baked Goods - APC" + assert pie[0].parent == f"SAL-ORD-{current_year}-00001" + + +@pytest.mark.order(13) +def test_allocation_from_purchasing(): + # Rebuild demand allocation map to ensure purchase receipts with handling units are reflected + build_demand_allocation_map() + + receipts = frappe.get_all( + "Purchase Receipt", ["name", "'Purchase Receipt' AS doctype"] + ) + frappe.get_all("Purchase Invoice", ["name", "'Purchase Invoice' AS doctype"]) + + for pr in receipts: + doc = frappe.get_doc(pr.doctype, pr.name) + for item in doc.items: + if item.handling_unit: # flag for inventoriable item + # TODO: this should be improved with greater specificity, but detecting that + # creating inventory leads to modification of the demand db is OK for now + d = get_demand(filters={"item_code": item.item_code}) + assert len(d) > 0 + + +@pytest.mark.order(35) # run after other tests +def test_ignore_drop_shipped_items_in_demand(): + # Create temporary drop shipped item + supplier = "Freedom Provisions" + i = frappe.new_doc("Item") + i.item_code = i.item_name = "Pie Servingware" + i.item_group = "Products" + i.is_stock_item = 1 + i.stock_uom = "Nos" + i.is_purchase_item = 1 + i.is_sales_item = 1 + i.delivered_by_supplier = 1 + i.append("supplier_items", {"supplier": supplier}) + i.save() + + d = get_sales_demand(item_code=i.item_code) + assert len(d) == 0 + + # create sales order with drop shipped item + so = frappe.new_doc("Sales Order") + so.customer = random.choice(customers) + so.selling_price_list = "Standard Selling" + so.append( + "items", + { + "item_code": i.item_code, + "delivery_date": add_days(today(), 7), + "qty": 10, + "rate": 2, + "delivered_by_supplier": 1, + "supplier": supplier, + }, + ) + so.save() + so.submit() + + bs = frappe.get_doc("BEAM Settings", {"company": so.company}) + assert bs.ignore_drop_shipped_items == 0 + + d = get_sales_demand(item_code=i.item_code) + assert len(d) == 1 + assert d[0].get("total_required_qty") == 10 + + # update settings to ignore drop shipped items + bs.ignore_drop_shipped_items = 1 + bs.save() + + d = get_sales_demand(item_code=i.item_code) + assert len(d) == 0 + + # reset settings and cancel/delete Sales Order + bs.ignore_drop_shipped_items = 0 + bs.save() + + so.cancel() + so.delete() diff --git a/beam/tests/test_handling_unit.py b/beam/tests/test_handling_unit.py index 0b27ebef..c02386f9 100644 --- a/beam/tests/test_handling_unit.py +++ b/beam/tests/test_handling_unit.py @@ -22,6 +22,85 @@ def submit_all_purchase_receipts(): pr.submit() +@pytest.mark.order(10) +def test_enable_handling_units_setting(): + """Test that enable_handling_units setting controls whether handling units are assigned to SLEs""" + company = frappe.defaults.get_defaults().get("company") + + # Test with enable_handling_units = False (default) + beam_settings = frappe.get_doc("BEAM Settings", {"company": company}) + original_value = beam_settings.enable_handling_units + beam_settings.enable_handling_units = 0 + beam_settings.save() + + try: + se_disabled = frappe.new_doc("Stock Entry") + se_disabled.stock_entry_type = se_disabled.purpose = "Material Receipt" + se_disabled.company = company + se_disabled.append( + "items", + { + "item_code": "Ambrosia Pie", + "qty": 10, + "t_warehouse": "Baked Goods - APC", + "basic_rate": frappe.get_value("Item Price", {"item_code": "Ambrosia Pie"}, "price_list_rate"), + }, + ) + se_disabled.save() + se_disabled.submit() + + # When disabled, handling_unit should NOT be generated + item_row = se_disabled.items[0] + assert ( + not item_row.handling_unit + ), f"Item row should not have handling_unit when setting is disabled, but got: {item_row.handling_unit}" + + # Check SLE - handling_unit should also NOT be set + sle_disabled = frappe.get_doc("Stock Ledger Entry", {"voucher_detail_no": item_row.name}) + assert ( + not sle_disabled.handling_unit or sle_disabled.handling_unit == "" + ), f"SLE should not have handling_unit when enable_handling_units is disabled, but got: {sle_disabled.handling_unit}" + + # Now test with enable_handling_units = True + beam_settings.enable_handling_units = 1 + beam_settings.save() + + se_enabled = frappe.new_doc("Stock Entry") + se_enabled.stock_entry_type = se_enabled.purpose = "Material Receipt" + se_enabled.company = company + se_enabled.append( + "items", + { + "item_code": "Ambrosia Pie", + "qty": 10, + "t_warehouse": "Baked Goods - APC", + "basic_rate": frappe.get_value("Item Price", {"item_code": "Ambrosia Pie"}, "price_list_rate"), + }, + ) + se_enabled.save() + se_enabled.submit() + + # When enabled, handling_unit should be generated on item row + item_row_enabled = se_enabled.items[0] + assert ( + item_row_enabled.handling_unit + ), "Item row should have handling_unit when setting is enabled" + + # Check SLE - handling_unit SHOULD be set when enabled + sle_enabled = frappe.get_doc("Stock Ledger Entry", {"voucher_detail_no": item_row_enabled.name}) + assert ( + sle_enabled.handling_unit + ), "SLE should have handling_unit when enable_handling_units is enabled" + assert ( + sle_enabled.handling_unit == item_row_enabled.handling_unit + ), f"SLE handling_unit should match item row: {sle_enabled.handling_unit} != {item_row_enabled.handling_unit}" + + finally: + # Restore original setting + beam_settings.enable_handling_units = original_value + beam_settings.save() + + @pytest.mark.order(1) def test_purchase_receipt_handling_unit_generation(): for pr in frappe.get_all("Purchase Receipt"): @@ -33,11 +112,11 @@ def test_purchase_receipt_handling_unit_generation(): assert isinstance(row.handling_unit, str) if row.rejected_qty: assert row.rejected_qty + row.qty == row.received_qty - hu = get_handling_unit(row.handling_unit) - assert hu.stock_qty == row.stock_qty + hu = get_handling_unit(row.handling_unit) + assert hu.stock_qty == row.stock_qty -@pytest.mark.order(2) +@pytest.mark.order(11) def test_purchase_invoice(): for pi in frappe.get_all("Purchase Invoice"): pi = frappe.get_doc("Purchase Invoice", pi) @@ -54,7 +133,7 @@ def test_purchase_invoice(): assert row.handling_unit == None -@pytest.mark.order(3) +@pytest.mark.order(13) def test_stock_entry_material_receipt(): submit_all_purchase_receipts() se = frappe.new_doc("Stock Entry") @@ -72,7 +151,7 @@ def test_stock_entry_material_receipt(): "items", { "item_code": "Ice Water", - "qty": 1000000000, + "qty": 100, "t_warehouse": "Refrigerator - APC", "basic_rate": 0, "allow_zero_valuation_rate": 1, @@ -93,7 +172,7 @@ def test_stock_entry_material_receipt(): assert row.handling_unit == sle.handling_unit -@pytest.mark.order(4) +@pytest.mark.order(14) def test_stock_entry_repack(): submit_all_purchase_receipts() pr_hu = frappe.get_value( @@ -120,6 +199,18 @@ def test_stock_entry_repack(): "handling_unit": pr_hu["handling_unit"], }, ) + scan = frappe.call( + "beam.beam.scan.scan", + **{ + "barcode": pr_hu.handling_unit, + "context": {"frm": "Stock Entry", "doc": se.as_dict()}, + "current_qty": 100, + }, + ) + assert scan[0]["action"] == "add_or_associate" + se.items[0].handling_unit = scan[0]["context"].get( + "handling_unit" + ) # simulates the effect of 'associate' se.append( "items", { @@ -149,7 +240,7 @@ def test_stock_entry_repack(): assert hu.stock_qty == 100 -@pytest.mark.order(4) +@pytest.mark.order(15) def test_stock_entry_material_transfer_for_manufacture(): submit_all_purchase_receipts() wo = frappe.get_value("Work Order", {"production_item": "Kaduka Key Lime Pie Filling"}) @@ -200,13 +291,23 @@ def test_stock_entry_material_transfer_for_manufacture(): assert row.handling_unit != row.to_handling_unit -@pytest.mark.order(6) +@pytest.mark.order(16) def test_stock_entry_for_manufacture(): submit_all_purchase_receipts() wo = frappe.get_value("Work Order", {"production_item": "Kaduka Key Lime Pie Filling"}) se_tfm = frappe.get_value( "Stock Entry", {"work_order": wo, "purpose": "Material Transfer for Manufacture"} ) + job_cards = frappe.get_all( + "Job Card", {"work_order": wo}, ["name", "sequence_id"], order_by="sequence_id asc" + ) + for jc in job_cards: + job_card = frappe.get_doc("Job Card", jc.name) + # Complete the job card by setting completed qty equal to qty to manufacture + for time_log in job_card.time_logs: + time_log.completed_qty = job_card.for_quantity + job_card.submit() + se = make_stock_entry(wo, "Manufacture", 40) # simulate scanning for row in se.get("items"): @@ -263,7 +364,7 @@ def test_stock_entry_for_manufacture(): assert row.t_warehouse == sle.warehouse # target warehouse -@pytest.mark.order(7) +@pytest.mark.order(17) def test_delivery_note(): se = frappe.new_doc("Stock Entry") se.stock_entry_type = se.purpose = "Material Receipt" @@ -306,7 +407,7 @@ def test_delivery_note(): assert hu.item_code == dn.items[0].item_code -@pytest.mark.order(8) +@pytest.mark.order(18) def test_sales_invoice(): se = frappe.new_doc("Stock Entry") se.stock_entry_type = se.purpose = "Material Receipt" @@ -350,7 +451,7 @@ def test_sales_invoice(): assert hu.item_code == si.items[0].item_code -@pytest.mark.order(9) +@pytest.mark.order(19) def test_packing_slip(): se = frappe.new_doc("Stock Entry") se.stock_entry_type = se.purpose = "Material Receipt" @@ -407,7 +508,7 @@ def test_packing_slip(): assert hu.stock_qty == 0 -@pytest.mark.order(10) +@pytest.mark.order(20) def test_stock_entry_material_transfer(): # create clean material receipt to avoid conflicts with Repack test semr = frappe.new_doc("Stock Entry") @@ -463,7 +564,11 @@ def test_stock_entry_material_transfer(): "Item", row.item_code, "enable_handling_unit" ): continue - sle = frappe.get_doc("Stock Ledger Entry", {"handling_unit": row.handling_unit}) + # For Material Transfer, there are two SLEs - one for source (negative) and one for target (positive) + # Get the source warehouse SLE (the one consuming from the handling unit) + sle = frappe.get_doc( + "Stock Ledger Entry", {"handling_unit": row.handling_unit, "warehouse": row.s_warehouse} + ) hu = get_handling_unit(str(row.handling_unit)) assert row.transfer_qty == abs(sle.actual_qty) assert hu.stock_qty == 95 # net qty @@ -496,7 +601,7 @@ def test_stock_entry_material_transfer(): assert row.t_warehouse == tsle.warehouse # target warehouse -@pytest.mark.order(11) +@pytest.mark.order(21) def test_stock_entry_for_send_to_subcontractor(): submit_all_purchase_receipts() se = frappe.new_doc("Stock Entry") @@ -557,7 +662,7 @@ def test_stock_entry_for_send_to_subcontractor(): assert hu.qty > 0 -@pytest.mark.order(12) +@pytest.mark.order(22) def test_subcontracting_receipt(): for row in frappe.get_all("Subcontracting Order", pluck="name"): if not frappe.db.exists( @@ -579,7 +684,7 @@ def test_subcontracting_receipt(): assert hu.stock_qty == row.returned_qty -@pytest.mark.order(13) +@pytest.mark.order(23) @pytest.mark.skip() # Remove when validate_handling_unit_overconsumption is uncommented in hooks.py doc_events def test_handling_units_overconsumption_in_material_transfer_stock_entry(): # Tests validate_handling_unit_overconsumption Stock Entry incoming code block @@ -635,7 +740,7 @@ def test_handling_units_overconsumption_in_material_transfer_stock_entry(): ) -@pytest.mark.order(14) +@pytest.mark.order(24) @pytest.mark.skip() # Remove when validate_handling_unit_overconsumption is uncommented in hooks.py doc_events def test_handling_units_overconsumption_in_delivery_note(): # Tests validate_handling_unit_overconsumption Delivery Note code block @@ -681,3 +786,223 @@ def test_handling_units_overconsumption_in_delivery_note(): f"Row #1: Handling Unit for Ambrosia Pie cannot be more than {hu.stock_qty} {hu.stock_uom}. You have {row_qty:.1f} {row_stock_uom}" in exc_info.value.args[0] ) + + +@pytest.mark.order(15) +def test_repack_cancel_without_recombine(): + """Test cancelling a Repack Stock Entry without recombining handling units""" + # Create a material receipt with a known handling unit + se_receipt = frappe.new_doc("Stock Entry") + se_receipt.stock_entry_type = se_receipt.purpose = "Material Receipt" + se_receipt.append( + "items", + { + "item_code": "Parchment Paper", + "qty": 100, + "t_warehouse": "Storeroom - APC", + "basic_rate": frappe.get_value( + "Item Price", {"item_code": "Parchment Paper"}, "price_list_rate" + ), + }, + ) + se_receipt.save() + se_receipt.submit() + source_hu = se_receipt.items[0].handling_unit + + # Create a repack entry + se_repack = frappe.new_doc("Stock Entry") + se_repack.stock_entry_type = se_repack.purpose = "Repack" + se_repack.append( + "items", + { + "item_code": "Parchment Paper", + "qty": 1, + "uom": "Box", + "conversion_factor": 100, + "stock_qty": 100, + "actual_qty": 100, + "transfer_qty": 100, + "s_warehouse": "Storeroom - APC", + "handling_unit": source_hu, + }, + ) + se_repack.append( + "items", + { + "item_code": "Parchment Paper", + "uom": "Nos", + "qty": 100, + "actual_qty": 100, + "transfer_qty": 100, + "t_warehouse": "Storeroom - APC", + }, + ) + se_repack.save() + se_repack.submit() + + source_row = se_repack.items[0] + target_row = se_repack.items[1] + target_hu = target_row.handling_unit + + # Verify initial state + source_hu_doc = get_handling_unit(source_hu) + target_hu_doc = get_handling_unit(target_hu) + assert source_hu_doc.stock_qty == 0 # consumed + assert target_hu_doc.stock_qty == 100 # created + + # Cancel WITHOUT recombine (don't set recombine_on_cancel) + se_repack.cancel() + + # After cancel without recombine: + # - Source HU should have qty 0 (consumed stays consumed) + # - Target HU should still exist with qty 100 (produced stays produced) + # This "keep separate" behavior maintains the split in cancelled state + source_hu_doc = get_handling_unit(source_hu) + target_hu_doc = get_handling_unit(target_hu) + assert source_hu_doc.stock_qty == 0 # consumed + assert target_hu_doc.stock_qty == 100 # produced + + +@pytest.mark.order(16) +def test_repack_cancel_with_recombine(): + """Test cancelling a Repack Stock Entry WITH recombining handling units""" + # Create a material receipt with a known handling unit + se_receipt = frappe.new_doc("Stock Entry") + se_receipt.stock_entry_type = se_receipt.purpose = "Material Receipt" + se_receipt.append( + "items", + { + "item_code": "Parchment Paper", + "qty": 100, + "t_warehouse": "Storeroom - APC", + "basic_rate": frappe.get_value( + "Item Price", {"item_code": "Parchment Paper"}, "price_list_rate" + ), + }, + ) + se_receipt.save() + se_receipt.submit() + source_hu = se_receipt.items[0].handling_unit + + # Create a repack entry + se_repack = frappe.new_doc("Stock Entry") + se_repack.stock_entry_type = se_repack.purpose = "Repack" + se_repack.append( + "items", + { + "item_code": "Parchment Paper", + "qty": 1, + "uom": "Box", + "conversion_factor": 100, + "stock_qty": 100, + "actual_qty": 100, + "transfer_qty": 100, + "s_warehouse": "Storeroom - APC", + "handling_unit": source_hu, + }, + ) + se_repack.append( + "items", + { + "item_code": "Parchment Paper", + "uom": "Nos", + "qty": 100, + "actual_qty": 100, + "transfer_qty": 100, + "t_warehouse": "Storeroom - APC", + }, + ) + se_repack.save() + se_repack.submit() + + source_row = se_repack.items[0] + target_row = se_repack.items[1] + target_hu = target_row.handling_unit + + # Set recombine_on_cancel on BOTH rows (as the frontend does) + source_row.db_set("recombine_on_cancel", True) + target_row.db_set("recombine_on_cancel", True) + + # Cancel WITH recombine + se_repack.reload() + se_repack.cancel() + + # After cancel with recombine: + # - Source HU should NOT get additional entries (recombine prevents split) + # - Target HU should NOT exist (was recombined back) + source_hu_doc = get_handling_unit(source_hu) + target_hu_doc = get_handling_unit(target_hu) + + # Source HU should have the original quantity (no split entries added) + assert source_hu_doc.stock_qty == 100 + # Target HU should be empty/zero (recombined back to source) + assert target_hu_doc is None or target_hu_doc.stock_qty == 0 + + +@pytest.mark.order(17) +def test_material_transfer_cancel_without_recombine(): + """Test cancelling a Material Transfer Stock Entry without recombining handling units""" + # Create a material receipt + se_receipt = frappe.new_doc("Stock Entry") + se_receipt.stock_entry_type = se_receipt.purpose = "Material Receipt" + se_receipt.append( + "items", + { + "item_code": "Parchment Paper", + "qty": 100, + "t_warehouse": "Storeroom - APC", + "basic_rate": frappe.get_value( + "Item Price", {"item_code": "Parchment Paper"}, "price_list_rate" + ), + }, + ) + se_receipt.save() + se_receipt.submit() + source_hu = se_receipt.items[0].handling_unit + + # Create a material transfer + se_transfer = frappe.new_doc("Stock Entry") + se_transfer.stock_entry_type = se_transfer.purpose = "Material Transfer" + se_transfer.company = frappe.defaults.get_defaults().get("company") + + scan = frappe.call( + "beam.beam.scan.scan", + **{ + "barcode": str(source_hu), + "context": {"frm": "Stock Entry", "doc": se_transfer.as_dict()}, + "current_qty": 1, + }, + ) + se_transfer.append( + "items", + { + **scan[0]["context"], + "qty": 50, + "actual_qty": 50, + "transfer_qty": 50, + "s_warehouse": "Storeroom - APC", + "t_warehouse": "Kitchen - APC", + }, + ) + se_transfer.save() + se_transfer.submit() + + transfer_row = se_transfer.items[0] + target_hu = transfer_row.to_handling_unit + + # Verify initial state + source_hu_doc = get_handling_unit(source_hu) + target_hu_doc = get_handling_unit(target_hu) + assert source_hu_doc.stock_qty == 50 # remaining in source + assert target_hu_doc.stock_qty == 50 # transferred to target + + # Cancel WITHOUT recombine + se_transfer.cancel() + + # After cancel without recombine: + # - Source HU should be restored + # - Target HU should also be restored (both persist separately) + source_hu_doc = get_handling_unit(source_hu) + target_hu_doc = get_handling_unit(target_hu) + assert source_hu_doc.stock_qty == 50 # restored in source warehouse + assert target_hu_doc.stock_qty == 50 # restored in target warehouse diff --git a/beam/tests/test_hooks_override.py b/beam/tests/test_hooks_override.py index 7149a91f..1fb76245 100644 --- a/beam/tests/test_hooks_override.py +++ b/beam/tests/test_hooks_override.py @@ -43,6 +43,7 @@ def patched_hooks(*args, **kwargs): monkeymodule.setattr("frappe.get_hooks", patched_hooks) +@pytest.mark.order(30) def test_beam_frm_hooks_override(patch_frappe_get_hooks): item_barcode = frappe.get_value("Item Barcode", {"parent": "Kaduka Key Lime Pie"}, "barcode") dn = frappe.new_doc("Delivery Note") diff --git a/beam/tests/test_item_barcode_print_format.py b/beam/tests/test_item_barcode_print_format.py new file mode 100644 index 00000000..46ce6a9e --- /dev/null +++ b/beam/tests/test_item_barcode_print_format.py @@ -0,0 +1,23 @@ +# Copyright (c) 2025, AgriTheory and contributors +# Test for barcode generation in Item Barcode print format + +import pytest + +from beam.beam.barcodes import barcode128 + + +@pytest.mark.parametrize("barcode_text", ["123456789012", "ITEM-00001", "987654321098"]) +def test_item_barcode_print_format(barcode_text): + # Generate barcode image in print format + img_html = barcode128(barcode_text) + assert img_html.startswith('') + # Optionally, check that the base64 string decodes to PNG + import base64 + import re + + match = re.search(r"data:image/png;base64,([A-Za-z0-9+/=]+)", img_html) + assert match, "No base64 PNG found in img tag" + png_bytes = base64.b64decode(match.group(1)) + assert png_bytes[:8] == b"\x89PNG\r\n\x1a\n", "Not a PNG file" diff --git a/beam/tests/test_printing.py b/beam/tests/test_printing.py new file mode 100644 index 00000000..e2f6f6a0 --- /dev/null +++ b/beam/tests/test_printing.py @@ -0,0 +1,85 @@ +# Copyright (c) 2025, AgriTheory and contributors +# For license information, please see license.txt + +from unittest.mock import Mock, patch + +import frappe +from frappe.exceptions import DoesNotExistError + +from beam.beam.printing import print_by_server + + +def test_print_by_server_empty_string_uses_standard(): + """Empty print_format should default to Standard""" + mock_cups = Mock() + mock_cups.IPPError = Exception + with patch("beam.beam.printing.cups", mock_cups): + try: + print_by_server( + doctype="Item", + name="Ambrosia Pie", + printer_setting="Kitchen Printer", + print_format="", + ) + except DoesNotExistError as e: + # Should fail trying to get "Standard" print format + assert "Standard" in str(e) + + +def test_print_by_server_none_uses_standard(): + """None print_format should default to Standard""" + mock_cups = Mock() + mock_cups.IPPError = Exception + with patch("beam.beam.printing.cups", mock_cups): + try: + print_by_server( + doctype="Item", + name="Ambrosia Pie", + printer_setting="Kitchen Printer", + print_format=None, + ) + except DoesNotExistError as e: + # Should fail trying to get "Standard" print format + assert "Standard" in str(e) + + +def test_print_by_server_explicit_format(): + """Explicit print_format should be used""" + from beam.beam.printing import print_by_server + + mock_cups = Mock() + mock_cups.IPPError = Exception + with patch("beam.beam.printing.cups", mock_cups): + try: + print_by_server( + doctype="Item", + name="Ambrosia Pie", + printer_setting="Kitchen Printer", + print_format="Item Barcode", + ) + except Exception as e: + # Should NOT fail on "Standard" - should use explicit format + assert "Standard" not in str(e), "Should use explicit format, not Standard" + + +def test_print_by_server_with_serialized_doc(): + """Serialized doc should be properly deserialized as full document instance""" + # Get a real item doc and serialize it like the frontend would + item = frappe.get_doc("Item", "Ambrosia Pie") + serialized_doc = frappe.as_json(item.as_dict()) + + mock_cups = Mock() + mock_cups.IPPError = Exception + with patch("beam.beam.printing.cups", mock_cups): + try: + print_by_server( + doctype="Item", + name="Ambrosia Pie", + printer_setting="Kitchen Printer", + print_format="Item Barcode", + doc=serialized_doc, # Pass as JSON string + ) + except Exception as e: + # Should not fail with AttributeError about 'in_print' + assert "in_print" not in str(e) + assert not isinstance(e, AttributeError) diff --git a/beam/tests/test_receiving_demand.py b/beam/tests/test_receiving_demand.py new file mode 100644 index 00000000..13d290e1 --- /dev/null +++ b/beam/tests/test_receiving_demand.py @@ -0,0 +1,89 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +# test update with purchase receipt submission +# test update with purchase receipt cancellation +# test demand from unfilled qty on blanket order +# test demand from purchase invoice without PO +# +# For license information, please see license.txt + + +from datetime import datetime + +import frappe +import pytest + +from beam.beam.demand.receiving import ( + _get_receiving_demand, + get_receiving_demand, + reset_build_receiving_map, +) + +current_year = datetime.now().year + + +@pytest.mark.order(2) +def test_opening_receiving(): + + receiving_demand = _get_receiving_demand() + assert receiving_demand[0].item_code == "Cloudberry" + assert receiving_demand[1].item_code == "Hairless Rambutan" + assert receiving_demand[2].item_code == "Tayberry" + assert receiving_demand[3].item_code == "Cocoplum" + assert receiving_demand[4].item_code == "Damson Plum" + assert receiving_demand[5].item_code == "Gooseberry" + assert receiving_demand[6].item_code == "Kaduka Lime" + assert receiving_demand[7].item_code == "Limequat" + assert receiving_demand[8].item_code == "Butter" + assert receiving_demand[9].item_code == "Cornstarch" + assert receiving_demand[10].item_code == "Flour" + assert receiving_demand[11].item_code == "Salt" + assert receiving_demand[12].item_code == "Sugar" + assert receiving_demand[13].item_code == "Water" + assert receiving_demand[14].item_code == "Parchment Paper" + assert receiving_demand[15].item_code == "Pie Box" + assert receiving_demand[16].item_code == "Pie Tin" + + reset_build_receiving_map() + + water = get_receiving_demand(filters={"item_code": "Water"}) + assert len(water) == 1 + + assert water[0].parent == f"PUR-ORD-{current_year}-00002" + assert water[0].stock_qty == 24.999442 + assert water[0].warehouse == "Kitchen - APC" + + +@pytest.mark.order(31) +def test_demand_from_purchase_invoice_without_po(): + + receiving_demand = get_receiving_demand(filters={"doctype": "Purchase Invoice"}) + assert len(receiving_demand) == 0 + + pi = frappe.new_doc("Purchase Invoice") + pi.supplier = "Freedom Provisions" + pi.set_posting_time = True + pi.buying_price_list = "Bakery Buying" + item = frappe.get_doc("Item", "Butter") + pi.append( + "items", + { + "item_code": item.item_code, + "warehouse": "Refrigerator - APC", + "rejected_warehouse": "Storeroom - APC", + "received_qty": 0, + "qty": 10, + "rate": 10, + }, + ) + pi.save() + pi.submit() + + receiving_demand = get_receiving_demand(filters={"doctype": "Purchase Invoice"}) + assert len(receiving_demand) == 1 + + pi.cancel() + + receiving_demand = get_receiving_demand(filters={"doctype": "Purchase Invoice"}) + assert len(receiving_demand) == 0 diff --git a/beam/tests/test_serial_number.py b/beam/tests/test_serial_number.py new file mode 100644 index 00000000..a304a8ba --- /dev/null +++ b/beam/tests/test_serial_number.py @@ -0,0 +1,134 @@ +# Copyright (c) 2025, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +import pytest +from frappe.utils import today + + +def _make_serials(series="WCC-.#####", qty=1): + from frappe.model.naming import make_autoname + + return [make_autoname(series) for _ in range(qty)] + + +@pytest.mark.order(20) +def test_serial_number_scan(): + warehouse = "Storeroom - APC" + supplier = "Unity Bakery Supply" + item_code = "Whipped Cream Canister" + serials = _make_serials(qty=3) + pr = frappe.get_doc( + { + "doctype": "Purchase Receipt", + "supplier": supplier, + "posting_date": today(), + "items": [ + { + "item_code": item_code, + "qty": 1, + "received_qty": 1, + "rate": 10, + "warehouse": warehouse, + "serial_no": serials[0], + "use_serial_batch_fields": 1, + } + ], + } + ) + pr.save() + pr.submit() + + # Serial No scanning disabled + company = frappe.defaults.get_defaults().get("company") + settings = frappe.get_doc("BEAM Settings", {"company": company}) + settings.scan_serial_no = 0 + settings.save() + assert settings.scan_serial_no == 0 + scan = frappe.call( + "beam.beam.scan.scan", + **{"barcode": str(serials[0]), "context": {"listview": "Purchase Receipt"}} + ) + assert scan is None + + # Serial No scanning enabled + settings.scan_serial_no = 1 + settings.save() + + assert settings.scan_serial_no == 1 + scan = frappe.call( + "beam.beam.scan.scan", + **{"barcode": str(serials[0]), "context": {"listview": "Purchase Receipt"}} + ) + assert scan[0]["action"] == "route" + assert scan[0]["doctype"] == "Purchase Receipt" + assert scan[0]["field"] == "Purchase Receipt" + assert scan[0]["target"] == pr.name + + pi = frappe.get_doc( + { + "doctype": "Purchase Invoice", + "supplier": supplier, + "posting_date": today(), + "update_stock": 1, + "items": [ + { + "item_code": item_code, + "qty": 1, + "received_qty": 1, + "rate": 10, + "warehouse": warehouse, + "serial_no": serials[1], + "use_serial_batch_fields": 1, + } + ], + } + ) + pi.save() + pi.submit() + + company = frappe.defaults.get_defaults().get("company") + settings = frappe.get_doc("BEAM Settings", {"company": company}) + settings.scan_serial_no = 1 + settings.save() + scan = frappe.call( + "beam.beam.scan.scan", + **{"barcode": str(serials[1]), "context": {"listview": "Purchase Invoice"}} + ) + assert scan[0]["action"] == "filter" + assert scan[0]["doctype"] == "Purchase Invoice" + assert scan[0]["field"] == "name" + assert scan[0]["target"] == pi.name + + dn = frappe.get_doc( + { + "doctype": "Delivery Note", + "customer": "Longwoods Sandwich Shop", + "posting_date": today(), + "items": [ + { + "item_code": item_code, + "qty": 1, + "received_qty": 1, + "rate": 10, + "warehouse": warehouse, + "serial_no": serials[1], + "use_serial_batch_fields": 1, + } + ], + } + ) + dn.save() + dn.submit() + + company = frappe.defaults.get_defaults().get("company") + settings = frappe.get_doc("BEAM Settings", {"company": company}) + settings.scan_serial_no = 1 + settings.save() + scan = frappe.call( + "beam.beam.scan.scan", **{"barcode": str(serials[1]), "context": {"listview": "Delivery Note"}} + ) + assert scan[0]["action"] == "filter" + assert scan[0]["doctype"] == "Delivery Note" + assert scan[0]["field"] == "name" + assert scan[0]["target"] == dn.name diff --git a/beam/tests/test_utils.py b/beam/tests/test_utils.py new file mode 100644 index 00000000..2f4cd88b --- /dev/null +++ b/beam/tests/test_utils.py @@ -0,0 +1,22 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +from contextlib import contextmanager + +import frappe + + +@contextmanager +def use_current_db_transaction(): + """ + Context manager to refresh the database transaction scope. + + This is needed when testing with Playwright because the browser actions + (like clicking SAVE or RECEIVE) commit data to the database, but the pytest + context maintains its own transaction scope and can't see the committed data + until the transaction is refreshed. + + """ + frappe.db.rollback() + frappe.db.begin() + yield diff --git a/beam/www/beam/Beam.vue b/beam/www/beam/Beam.vue new file mode 100644 index 00000000..282e9015 --- /dev/null +++ b/beam/www/beam/Beam.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/beam/www/beam/__init__.py b/beam/www/beam/__init__.py new file mode 100644 index 00000000..a2ae016a --- /dev/null +++ b/beam/www/beam/__init__.py @@ -0,0 +1,28 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt +from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note +from frappe.model.create_new import make_new_doc as frappe_make_new_doc + + +@frappe.whitelist() +def make_new_doc(doctype, docname=None): + if doctype == "Stock Entry": + doc = frappe_make_new_doc(doctype) + doc.purpose = "Material Transfer" + return doc + elif doctype == "Purchase Receipt": + return make_purchase_receipt(docname).as_dict() + elif doctype == "Delivery Note": + return make_delivery_note(docname).as_dict() + + +# @frappe.whitelist() +# def make_mapped_doc(doctype, docname): +# print(doctype, docname) +# if doctype in ['Purchase Order']: +# return +# elif doctype in ['Sales Order']: +# print('map sales order') diff --git a/beam/www/beam/components/ControlButtons.vue b/beam/www/beam/components/ControlButtons.vue new file mode 100644 index 00000000..11430135 --- /dev/null +++ b/beam/www/beam/components/ControlButtons.vue @@ -0,0 +1,48 @@ + + + + + diff --git a/beam/www/beam/components/DemandFilters.vue b/beam/www/beam/components/DemandFilters.vue new file mode 100644 index 00000000..d5c2ee64 --- /dev/null +++ b/beam/www/beam/components/DemandFilters.vue @@ -0,0 +1,85 @@ + + + diff --git a/beam/www/beam/components/ScanOutput.vue b/beam/www/beam/components/ScanOutput.vue new file mode 100644 index 00000000..8d26a6c7 --- /dev/null +++ b/beam/www/beam/components/ScanOutput.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/beam/www/beam/components/UserFilter.vue b/beam/www/beam/components/UserFilter.vue new file mode 100644 index 00000000..bf05eb09 --- /dev/null +++ b/beam/www/beam/components/UserFilter.vue @@ -0,0 +1,53 @@ + + + diff --git a/beam/www/beam/env.d.ts b/beam/www/beam/env.d.ts new file mode 100644 index 00000000..ca53223b --- /dev/null +++ b/beam/www/beam/env.d.ts @@ -0,0 +1,7 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +/// +/// + +export {} diff --git a/beam/www/beam/icon/beam_favicon.svg b/beam/www/beam/icon/beam_favicon.svg new file mode 100644 index 00000000..4418b083 --- /dev/null +++ b/beam/www/beam/icon/beam_favicon.svg @@ -0,0 +1,64 @@ + + + + + + + + + + image/svg+xml + + + + + + + + + diff --git a/beam/www/beam/icon/beam_icon-192x192.png b/beam/www/beam/icon/beam_icon-192x192.png new file mode 100644 index 00000000..54d89fe3 Binary files /dev/null and b/beam/www/beam/icon/beam_icon-192x192.png differ diff --git a/beam/www/beam/icon/beam_icon-512x512.png b/beam/www/beam/icon/beam_icon-512x512.png new file mode 100644 index 00000000..dd90cbd0 Binary files /dev/null and b/beam/www/beam/icon/beam_icon-512x512.png differ diff --git a/beam/www/beam/index.html b/beam/www/beam/index.html new file mode 100644 index 00000000..45742b97 --- /dev/null +++ b/beam/www/beam/index.html @@ -0,0 +1,43 @@ +{% extends "templates/web.html" %} + +{% block head %} + + + +Beam + + + +{% endblock %} + +{% block content %} +
+
+
+ + +{% endblock %} + +{%- block style -%} + +{% endblock %} diff --git a/beam/www/beam/index.py b/beam/www/beam/index.py new file mode 100644 index 00000000..975e80f2 --- /dev/null +++ b/beam/www/beam/index.py @@ -0,0 +1,13 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe + +no_cache = True + + +def get_context(context): + csrf_token = frappe.sessions.get_csrf_token() + context.csrf_token = csrf_token + context.user = frappe.session.user + frappe.db.commit() diff --git a/beam/www/beam/index.ts b/beam/www/beam/index.ts new file mode 100644 index 00000000..df969ce1 --- /dev/null +++ b/beam/www/beam/index.ts @@ -0,0 +1,56 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { install as BeamPlugin } from '@stonecrop/beam' +import { install as AformPlugin } from '@stonecrop/aform' +import { createPinia } from 'pinia' +import { createApp, markRaw } from 'vue' +import { createRouter, createWebHashHistory } from 'vue-router' +import { routes, handleHotUpdate } from 'vue-router/auto-routes' + +import Beam from '@/Beam.vue' +import { useInitStore } from '@/stores/init.js' +import { BeamWindow } from '@/types/index.js' + +declare const window: BeamWindow + +const router = createRouter({ + history: createWebHashHistory(), + routes, +}) + +if (import.meta.hot) { + handleHotUpdate(router) +} + +router.beforeEach(async (to, from, next) => { + if (to.meta.requiresAuth) { + if (window.frappe.user === 'Guest') { + next(false) + // TODO: 6 Sep, 2024: tried redirecting to intended path, but Frappe + // ignores everything after the hash + window.location.href = '/login?redirect-to=/beam#' + } else { + const store = useInitStore() + await store.init(to) + next() + } + } else { + // assuming user is logged in and authenticated for all Beam views + const store = useInitStore() + await store.init(to) + next() + } +}) + +const pinia = createPinia() +pinia.use(({ store }) => { + store.router = markRaw(router) +}) + +const app = createApp(Beam) +app.use(router) +app.use(BeamPlugin) +app.use(AformPlugin) +app.use(pinia) +app.mount('#beam') diff --git a/beam/www/beam/pages/404.vue b/beam/www/beam/pages/404.vue new file mode 100644 index 00000000..09622233 --- /dev/null +++ b/beam/www/beam/pages/404.vue @@ -0,0 +1,23 @@ + + + diff --git a/beam/www/beam/pages/DeliveryNote.vue b/beam/www/beam/pages/DeliveryNote.vue new file mode 100644 index 00000000..884a7e66 --- /dev/null +++ b/beam/www/beam/pages/DeliveryNote.vue @@ -0,0 +1,141 @@ + + + + + diff --git a/beam/www/beam/pages/Demand.vue b/beam/www/beam/pages/Demand.vue new file mode 100644 index 00000000..da06a621 --- /dev/null +++ b/beam/www/beam/pages/Demand.vue @@ -0,0 +1,139 @@ + + + diff --git a/beam/www/beam/pages/Home.vue b/beam/www/beam/pages/Home.vue new file mode 100644 index 00000000..4cb63bb8 --- /dev/null +++ b/beam/www/beam/pages/Home.vue @@ -0,0 +1,65 @@ + + + + + diff --git a/beam/www/beam/pages/JobCard.vue b/beam/www/beam/pages/JobCard.vue new file mode 100644 index 00000000..452a0f4d --- /dev/null +++ b/beam/www/beam/pages/JobCard.vue @@ -0,0 +1,42 @@ + + + + + diff --git a/beam/www/beam/pages/Manufacture.vue b/beam/www/beam/pages/Manufacture.vue new file mode 100644 index 00000000..a9ff1af5 --- /dev/null +++ b/beam/www/beam/pages/Manufacture.vue @@ -0,0 +1,150 @@ + + + diff --git a/beam/www/beam/pages/Move.vue b/beam/www/beam/pages/Move.vue new file mode 100644 index 00000000..84115fa8 --- /dev/null +++ b/beam/www/beam/pages/Move.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/beam/www/beam/pages/Operation.vue b/beam/www/beam/pages/Operation.vue new file mode 100644 index 00000000..3ca9f811 --- /dev/null +++ b/beam/www/beam/pages/Operation.vue @@ -0,0 +1,95 @@ + + + + + diff --git a/beam/www/beam/pages/PurchaseReceipt.vue b/beam/www/beam/pages/PurchaseReceipt.vue new file mode 100644 index 00000000..5bf2b5ca --- /dev/null +++ b/beam/www/beam/pages/PurchaseReceipt.vue @@ -0,0 +1,139 @@ + + + + + diff --git a/beam/www/beam/pages/Receive.vue b/beam/www/beam/pages/Receive.vue new file mode 100644 index 00000000..02f8677d --- /dev/null +++ b/beam/www/beam/pages/Receive.vue @@ -0,0 +1,140 @@ + + + + + diff --git a/beam/www/beam/pages/Repack.vue b/beam/www/beam/pages/Repack.vue new file mode 100644 index 00000000..285483be --- /dev/null +++ b/beam/www/beam/pages/Repack.vue @@ -0,0 +1,338 @@ + + + + diff --git a/beam/www/beam/pages/Ship.vue b/beam/www/beam/pages/Ship.vue new file mode 100644 index 00000000..b8bf9a33 --- /dev/null +++ b/beam/www/beam/pages/Ship.vue @@ -0,0 +1,133 @@ + + + diff --git a/beam/www/beam/pages/WorkOrder.vue b/beam/www/beam/pages/WorkOrder.vue new file mode 100644 index 00000000..10ea5a73 --- /dev/null +++ b/beam/www/beam/pages/WorkOrder.vue @@ -0,0 +1,229 @@ + + + + + diff --git a/beam/www/beam/pages/Workstation.vue b/beam/www/beam/pages/Workstation.vue new file mode 100644 index 00000000..0c9f3779 --- /dev/null +++ b/beam/www/beam/pages/Workstation.vue @@ -0,0 +1,42 @@ + + + diff --git a/beam/www/beam/pinia.d.ts b/beam/www/beam/pinia.d.ts new file mode 100644 index 00000000..821d3a24 --- /dev/null +++ b/beam/www/beam/pinia.d.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import 'pinia' +import type { Router } from 'vue-router' + +declare module 'pinia' { + export interface PiniaCustomProperties { + router: Router + } +} + +export {} diff --git a/beam/www/beam/plugins/component.ts b/beam/www/beam/plugins/component.ts new file mode 100644 index 00000000..c0fba2b0 --- /dev/null +++ b/beam/www/beam/plugins/component.ts @@ -0,0 +1,51 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import type { ComponentResolver, Options } from 'unplugin-vue-components' + +import { getAppConfigs, mergeConfigs } from './hooks.js' +import type { HookConfig } from '@/types/config.js' + +export function getComponentPluginOptions(): Options { + // let globs: Options['globs'] + // const components = getComponents() + // if ('*' in components) { + // globs = components['*'] + // delete components['*'] + // } + + const dts: Options['dts'] = 'beam/www/beam/components.d.ts' + const resolvers: Options['resolvers'] = [BEAMResolver()] + + return { + dts, + resolvers, + } +} + +export function BEAMResolver(): ComponentResolver { + const components = getComponents() + return { + type: 'component', + resolve: name => { + if (components[name]) { + return { + name, + from: components[name], + } + } + }, + } +} + +export function getComponents(): HookConfig['components'] { + const appConfigs = getAppConfigs() + let components = {} + for (const config of appConfigs) { + if (config.components) { + console.log(`Custom BEAM components found in ${config.file}`) + components = mergeConfigs(components, config.components) + } + } + return components +} diff --git a/beam/www/beam/plugins/hooks.ts b/beam/www/beam/plugins/hooks.ts new file mode 100644 index 00000000..621dbb5b --- /dev/null +++ b/beam/www/beam/plugins/hooks.ts @@ -0,0 +1,101 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { readFileSync } from 'fs' +import { globSync } from 'glob' +import { resolve } from 'path' + +import type { AppList, HookConfig, HookRoute } from '@/types/config.js' + +const HOOK_NAME = 'beam_mobile' + +export function getAppConfigs(): HookConfig[] { + const appHooks = getAppHooks() + const configs = [] + for (const hookFile of appHooks) { + const fileContent = readFileSync(hookFile, { encoding: 'utf-8' }) + if (fileContent.includes(HOOK_NAME)) { + const config = extractConfig(fileContent) + if (config) { + config.file = hookFile + configs.push(config) + } + } + } + return configs +} + +export function getAppHooks(): string[] { + // respects installed app order + const appsPath = resolve(process.cwd(), '..') + const appsListPath = resolve(appsPath, '../sites/apps.json') + const appsList: AppList = JSON.parse(readFileSync(appsListPath, { encoding: 'utf-8' })) + const appHooks = globSync(`${appsPath}/**/hooks.py`) + + const appOrderMap = Object.entries(appsList).reduce( + (acc, [appName, config]) => { + acc[appName] = config.idx + return acc + }, + {} as Record + ) + + return appHooks.sort((prevHook, nextHook) => { + const appNameA = prevHook.split('/').slice(-3)[0] // assumes ../app_name/*/hooks.py + const appNameB = nextHook.split('/').slice(-3)[0] + + const indexA = appOrderMap[appNameA] ?? Infinity + const indexB = appOrderMap[appNameB] ?? Infinity + + return indexA - indexB + }) +} + +export function extractConfig(fileContent: string): HookConfig | undefined { + const hookRegex = new RegExp(`${HOOK_NAME}\\s*=\\s*({[^]*?})(?=\\s*$|\\s*#|\\s*[\r\n])`) + const match = fileContent.match(hookRegex) + + if (match) { + try { + const formattedConfig = preFormatHooks(match[1]) + return JSON.parse(formattedConfig) + } catch (error) { + console.error('Failed to parse hooks:', error) + console.debug('Extracted content:', match[1]) + return + } + } +} + +export function preFormatHooks(rawText: string): string { + return ( + rawText + // Remove comments first + .replace(/^\s*#.*$/gm, '') // Remove full-line comments + .replace(/(.+?)#.*$/gm, '$1') // Remove inline comments + // Convert Python syntax to JavaScript + .replace(/'/g, '"') // Replace single quotes + .replace(/True/g, 'true') // Convert booleans + .replace(/False/g, 'false') + .replace(/None/g, 'null') // Convert None to null + // Clean up JSON structure + .replace(/,(\s*[\]}])/g, '$1') // Remove trailing commas + .replace(/\s+/g, ' ') // Normalize whitespace + .replace(/\t/g, ' ') // Replace tabs with spaces + .trim() + ) +} + +export function mergeConfigs(...configs: Array | undefined>): Record { + return configs.reduce((result, config) => { + Object.entries(config ?? {}).forEach(([key, value]) => (result[key] = value)) + return result + }, {}) +} + +export function transformRoutes(routes: HookConfig['routes']): Record { + return routes.reduce((acc, route) => { + acc[route.path] = route + return acc + }, {}) +} diff --git a/beam/www/beam/plugins/router.ts b/beam/www/beam/plugins/router.ts new file mode 100644 index 00000000..cdfb63e7 --- /dev/null +++ b/beam/www/beam/plugins/router.ts @@ -0,0 +1,39 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { resolve } from 'path' + +import { getAppConfigs, mergeConfigs, transformRoutes } from './hooks.js' +import type { HookConfig } from '@/types/config.js' + +export function getRoutes(): HookConfig['routes'] { + const appConfigs = getAppConfigs() + let routes = {} + for (const config of appConfigs) { + if (config.routes) { + console.log(`Custom BEAM routes found in ${config.file}`) + const _routes = transformRoutes(config.routes) + routes = mergeConfigs(routes, _routes) + } + } + return Object.values(routes) +} + +export function getComponentPaths(): Record { + const appsPath = resolve(process.cwd(), '..') + const appConfigs = getAppConfigs() + let paths = {} + for (const config of appConfigs) { + if (config.components) { + const componentPaths = Object.entries(config.components).reduce( + (acc, [name, path]) => { + acc[name] = resolve(appsPath, path) + return acc + }, + {} as Record + ) + paths = mergeConfigs(paths, componentPaths) + } + } + return paths +} diff --git a/beam/www/beam/router.d.ts b/beam/www/beam/router.d.ts new file mode 100644 index 00000000..18479455 --- /dev/null +++ b/beam/www/beam/router.d.ts @@ -0,0 +1,14 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import 'vue-router' + +declare module 'vue-router' { + interface RouteMeta { + doctype: string + requiresAuth: boolean + view: 'list' | 'form' + } +} + +export {} diff --git a/beam/www/beam/routes/index.vue b/beam/www/beam/routes/index.vue new file mode 100644 index 00000000..c586288b --- /dev/null +++ b/beam/www/beam/routes/index.vue @@ -0,0 +1,6 @@ + + + diff --git a/beam/www/beam/stores/beam.ts b/beam/www/beam/stores/beam.ts new file mode 100644 index 00000000..83639791 --- /dev/null +++ b/beam/www/beam/stores/beam.ts @@ -0,0 +1,334 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { defineStore } from 'pinia' +import { reactive, ref } from 'vue' +import type { RouteLocationNormalized } from 'vue-router' + +import { useHttpStore } from '@/stores/http.js' +import type { + BeamCache, + BeamHome, + BomItem, + DeliveryNoteItem, + Demand, + FormContext, + FrappeResponse, + ListContext, + ParentDoctypes, + ParentDoctypesForStockTransfer, + Receive, + ScanConfig, + ScanContext, + StockEntry, +} from '@/types/index.js' +import { handleErrors } from '@/utils/error.js' +import { useBeamToast } from '@/utils/toast.js' + +declare const frappe: any + +const BEAM_HOME_URL = '/api/method/beam.beam.doctype.beam_settings.beam_settings.get_beam_home' +const LOGOUT_URL = '/api/method/logout' +const MAPPED_STOCK_ENTRY_URL = '/api/method/erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry' +const NEW_DOC_URL = '/api/method/beam.www.beam.make_new_doc' +const PURCHASE_DEMAND_URL = '/api/method/beam.beam.demand.receiving.get_receiving_demand' +const SALES_DEMAND_URL = '/api/method/beam.beam.demand.demand.get_demand' +const SCAN_CONFIG_URL = '/api/method/beam.beam.scan.config.get_scan_doctypes' +const SCAN_URL = 'beam.beam.scan.scan' // frappe.xcall doesn't require prefix + +export const useBeamStore = defineStore('beam', () => { + const toast = useBeamToast() + const httpStore = useHttpStore() + + const recordsPerPage = 20 + const cache = ref({ mappers: {} }) + const form = ref>({}) + const warehouseList = ref() + const scanner = reactive({ + config: {} as ScanConfig, + context: {} as ScanContext, + lastScan: '' as string, + lastDocType: '' as string, + }) + + const getScanDoctypes = async () => { + const response = await httpStore.get(SCAN_CONFIG_URL) + const { message }: { message: ScanConfig } = await response.json() + scanner.config = message + } + + // TODO: vue-router's useRoute() composable is not working as intended here, so accepting route input + const setForm = async (currentRoute: RouteLocationNormalized) => { + form.value = {} + if (!currentRoute.params.id) return + + const meta = currentRoute.meta + if (meta.view === 'form' && scanner.config.frm.includes(meta.doctype)) { + const docname = currentRoute.params.id.toString() + form.value = await getOne(meta.doctype, docname) + } + } + + const setMappedDoc = async (currentRoute: RouteLocationNormalized) => { + const id = currentRoute.query.id || currentRoute.params.id + if (!id) return + + const meta = currentRoute.meta + if (meta.view === 'form' && scanner.config.frm.includes(meta.doctype)) { + const docname = id.toString() + let newDoc: ParentDoctypesForStockTransfer + if (meta.doctype === 'Work Order') { + // check if a draft Stock Entry already exists for this work order + const existingEntries = await getAll('Stock Entry', { + filters: JSON.stringify({ + docstatus: 0, + work_order: id, + purpose: 'Material Transfer for Manufacture', + }), + }) + + if (existingEntries.length) { + newDoc = await getOne('Stock Entry', existingEntries[0].name!) + } else { + newDoc = await getMappedStockEntry({ + work_order_id: id, + purpose: 'Material Transfer for Manufacture', + }) + } + } else { + newDoc = await makeNewDoc(meta.doctype, docname) + if (newDoc.doctype === 'Delivery Note') { + for (const item of newDoc.items) { + ;(item as DeliveryNoteItem).delivered_qty = 0 + } + } + } + cache.value.mappers[docname] = newDoc + } + } + + const setScanContext = async (currentRoute: RouteLocationNormalized) => { + const meta = currentRoute.meta + if (meta.view === 'list' && scanner.config.listview.includes(meta.doctype)) { + scanner.context = { listview: meta.doctype } + } else if (meta.view === 'form' && scanner.config.frm.includes(meta.doctype)) { + scanner.context = { frm: meta.doctype } + } + } + + const setWarehouses = async () => { + warehouseList.value = await getAll<{ name: string }[]>('Warehouse', { + fields: JSON.stringify(['company', 'disabled', 'is_group', 'name', 'warehouse_name']), + }) + } + + const getOne = async (doctype: string, name: string) => { + const url = `/api/resource/${doctype}/${name}` + const response = await httpStore.get(url) + const { data }: { data: T } = await response.json() + return data + } + + const getAll = async (doctype: string, params?: Record, page?: number) => { + if (page) { + const start = (page - 1) * recordsPerPage + const end = start + recordsPerPage + params = { ...params, limit_start: start, limit_page_length: end } + } + + const url = `/api/resource/${doctype}` + const response = await httpStore.get(url, params) + const { data }: { data: T[] } = await response.json() + return data + } + + const getHome = async (params?: Record) => { + const response = await httpStore.get(BEAM_HOME_URL, params) + const { message }: { message: BeamHome } = await response.json() + return { data: message } + } + + const getDemand = async (params?: Record) => { + // automatically fetch all pages of demand data based on parameters + const response = await httpStore.get(SALES_DEMAND_URL, params) + const { message }: { message: Demand[] } = await response.json() + return { data: message } + } + + const getReceiving = async (params?: Record) => { + // automatically fetch all pages of demand data based on parameters + const response = await httpStore.get(PURCHASE_DEMAND_URL, params) + const { message }: { message: Receive[] } = await response.json() + return { data: message } + } + + const scan = async (barcode: string, qty: number): Promise<(FormContext | ListContext)[] | undefined> => { + try { + const response = await frappe.xcall(SCAN_URL, { + barcode, + current_qty: qty, + context: scanner.context, + }) + + if (!response) { + toast.error(`Barcode '${barcode}' not found`) + return + } + + return response + } catch (error) { + // TODO: handle API error + console.error(error) + } + + return [] + } + + const insert = async >(doctype: string, body: T) => { + const url = `/api/resource/${doctype}` + const response = await httpStore.post(url, body) + if (response.ok) { + toast.success('Document created') + const { data }: FrappeResponse = await response.json() + return { data, response } + } else { + await handleErrors(response) + return { data: null, response } + } + } + + const update = async (doctype: string, name: string, body: Partial) => { + const url = `/api/resource/${doctype}/${name}` + const response = await httpStore.put(url, body) + if (response.ok) { + toast.success('Document updated') + const { data }: FrappeResponse = await response.json() + return { data, response } + } else { + await handleErrors(response) + return { data: null, response } + } + } + + const submit = async (doctype: string, name: string) => { + const url = `/api/resource/${doctype}/${name}` + const response = await httpStore.put(url, { docstatus: 1 }) + if (response.ok) { + toast.success('Document status changed to Submitted') + const { data }: FrappeResponse = await response.json() + return { data, response } + } else { + await handleErrors(response) + return { data: null, response } + } + } + + const cancel = async (doctype: string, name: string) => { + const url = `/api/resource/${doctype}/${name}` + const response = await httpStore.put(url, { docstatus: 2 }) + if (response.ok) { + toast.success('Document status changed to Cancelled') + const { data }: FrappeResponse = await response.json() + return { data, response } + } else { + await handleErrors(response) + return { data: null, response } + } + } + + const makeNewDoc = async (doctype: string, docname?: string) => { + const response = await httpStore.post(NEW_DOC_URL, { doctype, docname }) + const { message }: { message: T } = await response.json() + return message + } + + const getMappedStockEntry = async (data: Record) => { + // return a work order object with attached stock entry/ies and job card(s) + const response = await httpStore.post(MAPPED_STOCK_ENTRY_URL, data) + const { message }: { message: StockEntry } = await response.json() + if (!message || !message.items || !message.items.length) { + toast.error('Error: Could not map Work Order to Stock Entry') + return + } + // initialize pending stock entry items with zero quantity + message.items.map(item => { + if (!item.is_finished_item) { + item.qty = 0 + } + }) + return message + } + + const getStockEntryItems = async (bomName: string, qty = 1, purpose = 'Manufacture') => { + try { + const homeData = await getHome() + const company = homeData.data.company + const response = await httpStore.get('/api/method/erpnext.manufacturing.doctype.bom.bom.get_bom_items', { + bom: bomName, + company, + fetch_exploded: 1, + qty, + purpose, + }) + const { message }: { message: BomItem[] } = await response.json() + if (!message) return [] + + return message + } catch (error) { + console.error(error) + return [] + } + } + + const logout = async () => { + await httpStore.get(LOGOUT_URL) + window.location.href = '/login?redirect-to=/beam#' + } + + const formatDate = (date: Date) => { + if (isNaN(Date.parse(date.toString()))) { + return '' + } + + return date.toLocaleString(frappe.boot.time_zone, { + year: 'numeric', + month: 'numeric', + day: 'numeric', + hour: '2-digit', + minute: '2-digit', + }) + } + + return { + // state + cache, + form, + scanner, + warehouseList, + // store context actions + getScanDoctypes, + setForm, + setMappedDoc, + setScanContext, + setWarehouses, + + // document workflow actions + cancel, + insert, + update, + submit, + + // other api actions + formatDate, + getAll, + getDemand, + getHome, + getMappedStockEntry, + getOne, + getReceiving, + getStockEntryItems, + logout, + makeNewDoc, + scan, + } +}) diff --git a/beam/www/beam/stores/http.ts b/beam/www/beam/stores/http.ts new file mode 100644 index 00000000..f77276d5 --- /dev/null +++ b/beam/www/beam/stores/http.ts @@ -0,0 +1,67 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { defineStore } from 'pinia' +import { computed } from 'vue' + +declare const frappe: { + csrf_token: string +} + +export const useHttpStore = defineStore('http', () => { + const headers = computed(() => { + // setup as a computed property to allow Frappe to set the CSRF token + return { + 'Content-Type': 'application/json', + 'X-Frappe-CSRF-Token': frappe.csrf_token, + } + }) + + const formatUrl = (url: string, params?: Record) => { + let fragment: string + if (params) { + const query = new URLSearchParams(params) + fragment = `${url}?${query.toString()}` + } else { + fragment = url + } + return fragment + } + + const get = async (url: string, params?: Record) => { + const fragment = formatUrl(url, params) + const formattedUrl = new URL(fragment, window.location.origin) + return await fetch(formattedUrl, { + method: 'GET', + headers: headers.value, + }) + } + + const post = async (url: string, data: Record) => { + const formattedUrl = new URL(url, window.location.origin) + return await fetch(formattedUrl, { + method: 'POST', + headers: headers.value, + body: JSON.stringify(data), + }) + } + + const put = async (url: string, data: Record) => { + const formattedUrl = new URL(url, window.location.origin) + return await fetch(formattedUrl, { + method: 'PUT', + headers: headers.value, + body: JSON.stringify(data), + }) + } + + return { + // getters + headers, + + // http actions + get, + post, + put, + } +}) diff --git a/beam/www/beam/stores/init.ts b/beam/www/beam/stores/init.ts new file mode 100644 index 00000000..d164de51 --- /dev/null +++ b/beam/www/beam/stores/init.ts @@ -0,0 +1,40 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { defineStore } from 'pinia' +import { type RouteLocationNormalized, useRoute } from 'vue-router' + +import { useBeamStore } from '@/stores/beam.js' + +export const useInitStore = defineStore('init', () => { + const route = useRoute() + const store = useBeamStore() + + const init = async (currentRoute?: RouteLocationNormalized) => { + const resolvedRoute = currentRoute || route + + await store.getScanDoctypes() + await store.setForm(resolvedRoute) + await store.setMappedDoc(resolvedRoute) + await store.setScanContext(resolvedRoute) + await store.setWarehouses() + + // only check store actions to control toggling dirty state (vs. all state mutations); + store.$onAction(({ name, after }) => { + // 15 Nov '24: only scan actions affect the document's dirty state + if (name === 'scan') { + after(() => { + const id = resolvedRoute.params.id || resolvedRoute.query.id + const doc = store.cache.mappers[id] + if (doc) { + store.$patch(() => { + doc.dirty = true + }) + } + }) + } + }) + } + + return { init } +}) diff --git a/beam/www/beam/stores/scan.ts b/beam/www/beam/stores/scan.ts new file mode 100644 index 00000000..3392c919 --- /dev/null +++ b/beam/www/beam/stores/scan.ts @@ -0,0 +1,253 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { defineStore } from 'pinia' +import { computed } from 'vue' + +import { useBeamStore } from '@/stores/beam.js' +import type { + FormContext, + ListContext, + ParentDoctypesForStockTransfer, + PurchaseReceipt, + StockEntry, + StockEntryItem, +} from '@/types/index.js' + +export const useScanStore = defineStore('scan', () => { + const store = useBeamStore() + + const documentId = computed(() => { + const currentRoute = store.router.currentRoute.value + return currentRoute.params.id || currentRoute.query.id || currentRoute.name || '' + }) + + const mappedDoc = computed(() => store.cache.mappers[documentId.value]) + + const scan = async (barcode: string, qty: number) => { + store.scanner.lastScan = barcode + store.scanner.lastDocType = '' + const response = await store.scan(barcode, qty) + if (response && response.length > 0) { + let fn: Function + const action = response[0].action + if (response[0]?.context?.doc) { + store.scanner.lastDocType = `${response[0].context.doc.doctype}: ${response[0].context.doc.name}` + } else { + store.scanner.lastDocType = `${response[0].parenttype}: ${response[0].parent}` + } + + const scanHooks = store.scanner.config.client + + // an empty array indicates no additional client actions are registered + if (!Array.isArray(scanHooks) && action in scanHooks) { + const path = scanHooks[action][0] + // call (first) custom built callback registered in hooks + fn = path.split('.').reduce((previous, current) => previous[current], window) + return await fn(response) + } else { + return await actions[action](response) // TODO: this only calls the first function + } + } + } + + const add_or_associate = (barcode_context: FormContext[]) => { + const is_stock_entry = + mappedDoc.value.doctype === 'Stock Entry' && + [ + 'Send to Subcontractor', + 'Material Transfer for Manufacture', + 'Material Transfer', + 'Material Receipt', + 'Manufacture', + ].includes((mappedDoc.value as StockEntry).stock_entry_type) + + for (const action of barcode_context) { + const existing_rows = mappedDoc.value.items.filter(row => { + if (is_stock_entry) { + return row.item_code === action.context.item_code || row.handling_unit + } else { + return ( + (row.item_code === action.context.item_code && row.stock_qty === action.context.stock_qty) || + row.handling_unit === action.context.handling_unit + ) + } + }) + + if (existing_rows.length > 0) { + for (const row of existing_rows) { + if (action.field === 'qty') { + if (row.doctype === 'Stock Entry Detail') { + row[action.field] = Math.min((row as StockEntryItem).transfer_qty!, action.target) + } + } else { + row[action.field] = action.target + } + } + } else { + if (mappedDoc.value.doctype === 'Purchase Receipt') { + ;(mappedDoc.value as PurchaseReceipt).items.push({ + item_code: action.context.item_code, + received_qty: 1, + [action.field]: action.target, + }) + } else { + ;(mappedDoc.value as Exclude).items.push({ + item_code: action.context.item_code, + qty: 1, + [action.field]: action.target, + }) + } + } + + store.$patch(state => (state.cache.mappers[documentId.value] = mappedDoc.value)) + } + } + + const add_or_increment = (barcode_context: FormContext[]) => { + for (const action of barcode_context) { + const existing_rows = mappedDoc.value.items.filter( + row => + (row.item_code === action.context.item_code && !row.handling_unit) || + row.barcode === action.context.barcode || + row.item_code === action.context.doc?.item_code + ) + + const itemQtyFieldMap = { + 'Delivery Note Item': 'delivered_qty', + 'Purchase Receipt Item': 'received_qty', + 'Stock Entry Detail': 'qty', + } + + if (existing_rows.length > 0) { + const field = itemQtyFieldMap[action.doctype] || 'qty' + for (const row of existing_rows) { + row[field] = row[field] + 1 + } + } else if (action.doctype === 'Stock Entry') { + const source_warehouses = ['Material Consumption for Manufacture', 'Material Issue'] + const target_warehouses = ['Material Receipt', 'Manufacture'] + const both_warehouses = [ + 'Material Transfer for Manufacture', + 'Material Transfer', + 'Send to Subcontractor', + 'Repack', + ] + + const item: StockEntryItem = { + item_code: action.context.item_code, + qty: 1, + [action.field]: action.target, + } + + const entry_type = (mappedDoc.value as StockEntry).stock_entry_type + if (source_warehouses.includes(entry_type)) { + item.s_warehouse = action.context.warehouse + } else if (target_warehouses.includes(entry_type)) { + item.t_warehouse = action.context.warehouse + } else if (both_warehouses.includes(entry_type)) { + item.s_warehouse = action.context.warehouse + item.t_warehouse = action.context.warehouse + } + + ;(mappedDoc.value as StockEntry).items.push(item) + } else { + const item: StockEntryItem = { + item_code: action.context.doc?.item_code, + qty: 1, + } + + ;(mappedDoc.value as StockEntry).items.push(item) + } + store.$patch(state => (state.cache.mappers[documentId.value] = mappedDoc.value)) + } + } + + const filter = (barcode_context: ListContext[]) => { + // TODO: apply filters to listview; use store router + } + + const route = (barcode_context: ListContext[]) => { + // only route based on the last action in hooks + const action = barcode_context && barcode_context.at(-1) + if (action?.route) { + store.router.push(action.route) + } + } + + const set_item_code_and_handling_unit = (barcode_context: FormContext[]) => { + for (const action of barcode_context) { + store.$patch(state => { + state.form[action.field] = action.target + }) + } + } + + const set_warehouse = (barcode_context: FormContext[]) => { + for (const action of barcode_context) { + if (action.doctype !== 'Stock Entry') { + return + } + + const source_warehouses = ['Material Consumption for Manufacture', 'Material Issue'] + const target_warehouses = ['Material Receipt', 'Manufacture'] + const both_warehouses = [ + 'Material Transfer for Manufacture', + 'Material Transfer', + 'Send to Subcontractor', + 'Repack', + ] + + const entry_type = (store.form as StockEntry).stock_entry_type + if (entry_type) { + store.$patch(state => { + const form = state.form as StockEntry + if (source_warehouses.includes(entry_type)) { + form.from_warehouse = action.target + for (const row of form.items) { + row.s_warehouse = action.target + } + } else if (target_warehouses.includes(entry_type)) { + form.to_warehouse = action.target + for (const row of form.items) { + row.t_warehouse = action.target + } + } else if (both_warehouses.includes(entry_type)) { + form.from_warehouse = action.target + form.to_warehouse = action.target + for (const row of form.items) { + row.s_warehouse = action.target + row.t_warehouse = action.target + } + } + }) + } else { + const warehouse = barcode_context[0].context.doc?.name + if (!(mappedDoc.value as StockEntry).from_warehouse) { + ;(mappedDoc.value as StockEntry).from_warehouse = warehouse + } else if (!(mappedDoc.value as StockEntry).to_warehouse) { + ;(mappedDoc.value as StockEntry).to_warehouse = warehouse + } + + store.$patch(state => (state.cache.mappers[documentId.value] = mappedDoc.value)) + } + } + } + + const actions = { + add_or_associate, + add_or_increment, + filter, + route, + set_item_code_and_handling_unit, + set_warehouse, + } + + return { + // getters + documentId, + mappedDoc, + // actions + scan, + } +}) diff --git a/beam/www/beam/tsconfig.json b/beam/www/beam/tsconfig.json new file mode 100644 index 00000000..ddd2d20a --- /dev/null +++ b/beam/www/beam/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "esnext", + "module": "nodenext", + "moduleResolution": "nodenext", + "lib": ["esnext", "DOM", "DOM.Iterable"], + "jsx": "preserve", + "isolatedModules": true, + "noEmit": true, + "resolveJsonModule": true, + "skipLibCheck": true, + "strictNullChecks": true, + "paths": { + "@/*": ["./*"] + } + }, + "include": ["**/*.ts", "**/*.d.ts", "**/*.vue"] +} diff --git a/beam/www/beam/types/beam.ts b/beam/www/beam/types/beam.ts new file mode 100644 index 00000000..da37d9cf --- /dev/null +++ b/beam/www/beam/types/beam.ts @@ -0,0 +1,90 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import type { ListViewItem } from '@stonecrop/beam' +import type { ButtonHTMLAttributes, CSSProperties, HTMLAttributes } from 'vue' + +import type { ParentDoctypesForStockTransfer, StockEntry } from '@/types/frappe.js' + +export interface BeamWindow extends Window { + frappe: any + scanner: any +} + +export type BeamHome = { + routes: ListViewItem[] + company: string +} + +export type BeamCache = { + mappers: { + move?: StockEntry + repack?: StockEntry + [key: string]: ParentDoctypesForStockTransfer + } +} + +export type ControlButton = { + action: HTMLAttributes['onClick'] + label: string + + color?: { + background: CSSProperties['backgroundColor'] + text: CSSProperties['color'] + } + disabled?: ButtonHTMLAttributes['disabled'] + hidden?: boolean +} + +export type DemandFilter = { + status?: Demand['status'] | Receive['status'] | ['in', (Demand['status'] | Receive['status'])[]] + date?: string | ['<' | '>' | '>=' | '<=', string] + user?: string +} + +export type Demand = { + allocated_date: string | null + allocated_qty: number + assigned: string + bom_no: string + company: string + creation: string + customer: string + delivery_date: string + demand: string + doctype: string + idx: number + item_code: string + item_warehouse: string + key: string + modified: string + name: string + net_required_qty: number + parent: string + production_item: string + status: '' | 'Unallocated' | 'Partially Allocated' | 'Soft Allocated' + stock_uom: string + total_required_qty: number + warehouse: string +} + +export type Receive = { + assigned: string + company: string + creation: string + doctype: string + idx: number + item_code: string + key: string + modified: string + name: string + parent: string + received_qty: number + rejected_qty: number + schedule_date: string + status: '' | 'Unallocated' | 'Partially Allocated' | 'Soft Allocated' + stock_qty: number + stock_uom: string + supplier: string + warehouse: string +} diff --git a/beam/www/beam/types/config.ts b/beam/www/beam/types/config.ts new file mode 100644 index 00000000..15ffc02d --- /dev/null +++ b/beam/www/beam/types/config.ts @@ -0,0 +1,30 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +export type AppList = { + [app: string]: AppConfig +} + +export type AppConfig = { + idx?: number + is_repo?: boolean + required?: string[] + version?: string + resolution?: { + commit_hash?: string + branch?: string + } +} + +export type HookRoute = { + path: string + name: string + component: string + meta: { requiresAuth: boolean; doctype: string; view: 'list' | 'form' } +} + +export type HookConfig = { + components?: Record + file?: string + routes?: HookRoute[] +} diff --git a/beam/www/beam/types/frappe.ts b/beam/www/beam/types/frappe.ts new file mode 100644 index 00000000..d5884b46 --- /dev/null +++ b/beam/www/beam/types/frappe.ts @@ -0,0 +1,168 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import type { StoreMetadata } from '@/types/store.js' + +export type FrappeResponse = { + _exc_source?: string + _server_messages?: string + data?: T + exc_type?: string + exc?: string + exception?: string + home_page?: string +} + +export type DocActionResponse = FrappeResponse & { + response?: Response +} + +export type ParentDoctype = StoreMetadata & { + creation?: string + docstatus?: number + doctype?: string + modified_by?: string + modified?: string + name?: string + owner?: string + __islocal?: number +} + +export type ChildDoctypeMeta = ParentDoctype & { + idx?: number + parent?: string + parenttype?: string + parentfield?: string +} + +export type ChildDoctype = ChildDoctypeMeta & { + // may not exist for all child doctypes + barcode?: string + handling_unit?: string + item_code?: string + item_name?: string + qty?: number + stock_qty?: number + warehouse?: string + doc?: Omit +} + +export type User = ParentDoctype & { + enabled: boolean + email: string + first_name: string + + last_name?: string + full_name?: string +} + +export type JobCard = ParentDoctype & { + total_time_in_mins: number + items?: JobCardItem[] +} + +export type JobCardItem = ChildDoctype & { + item_code?: string + required_qty?: number + source_warehouse?: string + transferred_qty?: number +} + +export type StockEntry = ParentDoctype & { + stock_entry_type: string + items: StockEntryItem[] + + from_warehouse?: string + purpose?: string + to_warehouse?: string +} + +export type StockEntryItem = ChildDoctype & { + s_warehouse?: string + t_warehouse?: string + transfer_qty?: number + transferred_qty?: number + is_finished_item?: boolean + is_scrap_item?: boolean +} + +export type WorkOrder = ParentDoctype & { + planned_start_date: string + production_item: string + status: 'Draft' | 'Submitted' | 'Not Started' | 'In Process' | 'Completed' | 'Stopped' | 'Closed' | 'Cancelled' + qty: number + + item_name?: string + produced_qty?: number + skip_transfer?: boolean + wip_warehouse?: string + operations?: WorkOrderOperation[] + required_items?: WorkOrderItem[] +} + +export type WorkOrderOperation = ChildDoctype & { + operation: string + time_in_mins: number + + actual_operation_time?: number + completed_qty?: number + description?: string + workstation?: string +} + +export type WorkOrderItem = ChildDoctype & { + required_qty?: number + source_warehouse?: string + transferred_qty?: number +} + +export type Workstation = ParentDoctype & { + production_capacity: number + workstation_name: string + + status?: string +} + +export type PurchaseReceipt = ParentDoctype & { + items: PurchaseReceiptItem[] +} + +export type PurchaseReceiptItem = ChildDoctype & { + received_qty: number + + qty?: number + warehouse?: string +} + +export type DeliveryNote = ParentDoctype & { + items: DeliveryNoteItem[] +} + +export type DeliveryNoteItem = ChildDoctype & { + qty: number + + delivered_qty?: number // doesn't exist in the schema, but is used in the app + warehouse?: string +} + +export type BomItem = { + allow_alternative_item: number + amount: number + cost_center: string + default_warehouse: string + description: string + expense_account: string + idx: number + include_item_in_manufacturing: number + item_code: string + item_group: string + item_name: string + qty: number + rate: number + sourced_by_supplier: number + stock_uom: string +} + +export type ParentDoctypesForStockTransfer = DeliveryNote | PurchaseReceipt | StockEntry +export type ParentDoctypesWithItems = ParentDoctypesForStockTransfer | JobCard | WorkOrder +export type ParentDoctypes = ParentDoctypesWithItems & Workstation diff --git a/beam/www/beam/types/index.ts b/beam/www/beam/types/index.ts new file mode 100644 index 00000000..e39abd56 --- /dev/null +++ b/beam/www/beam/types/index.ts @@ -0,0 +1,8 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +export type * from './beam.js' +export type * from './config.js' +export type * from './frappe.js' +export type * from './scan.js' +export type * from './store.js' diff --git a/beam/www/beam/types/scan.ts b/beam/www/beam/types/scan.ts new file mode 100644 index 00000000..75936f22 --- /dev/null +++ b/beam/www/beam/types/scan.ts @@ -0,0 +1,71 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import type { ChildDoctype } from '@/types/index.js' + +export type BaseContext = { + /** + * The action to be performed on the scanned item. + * @example + * 'add_or_associate' + * 'add_or_increment' + * 'filter' + * 'route' + * 'set_item_code_and_handling_unit' + * 'set_warehouse' + */ + action: string + + doctype: string + + /** + * The field to be set on the target document. + * @example + * 'item_code' + * 'warehouse' + * 'handling_unit' + * 'qty' + */ + field: string + + /** + * The value to be set on the field. + */ + target: any + + /** + * The route to navigate to. + */ + route?: string + + /** + * The parent document to be set on the target document. + */ + parent?: string + + /** + * The parent doctype to be set on the target document. + */ + parenttype?: string +} + +export type FormContext = BaseContext & { + context: ChildDoctype +} + +export type ListContext = BaseContext & { + context: string +} + +export type ScanContext = { + frm?: string + listview?: string +} + +export type ScanConfig = { + client: Record | never[] + frm: string[] + listview: string[] + scannable_doctypes: string[] + show_scan_output: boolean +} diff --git a/beam/www/beam/types/store.ts b/beam/www/beam/types/store.ts new file mode 100644 index 00000000..520947ac --- /dev/null +++ b/beam/www/beam/types/store.ts @@ -0,0 +1,6 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +export type StoreMetadata = { + dirty?: boolean +} diff --git a/beam/www/beam/utils/error.ts b/beam/www/beam/utils/error.ts new file mode 100644 index 00000000..06496c95 --- /dev/null +++ b/beam/www/beam/utils/error.ts @@ -0,0 +1,27 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import type { FrappeResponse } from '@/types/frappe.js' +import { useBeamToast } from '@/utils/toast.js' + +const getFormattedErrors = (serverMessages: string) => { + const formattedMessages: string[] = [] + const parsedMessages: string[] = JSON.parse(serverMessages) + if (Array.isArray(parsedMessages)) { + for (const message of parsedMessages) { + formattedMessages.push(JSON.parse(message).message) + } + } + return formattedMessages +} + +const handleErrors = async (response: Response) => { + const toast = useBeamToast() + const { _server_messages }: FrappeResponse = await response.json() + const errors = getFormattedErrors(_server_messages) + for (const error of errors) { + toast.error(error) + } +} + +export { getFormattedErrors, handleErrors } diff --git a/beam/www/beam/utils/toast.ts b/beam/www/beam/utils/toast.ts new file mode 100644 index 00000000..f9003b78 --- /dev/null +++ b/beam/www/beam/utils/toast.ts @@ -0,0 +1,17 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { type ToastProps, useToast } from 'vue-toast-notification' +import 'vue-toast-notification/dist/theme-default.css' + +const useBeamToast = (props?: ToastProps) => { + return useToast({ + position: 'top', + dismissible: true, + duration: 3000, + queue: false, + ...props, + }) +} + +export { useBeamToast } diff --git a/beam/www/beam/vite.config.ts b/beam/www/beam/vite.config.ts new file mode 100644 index 00000000..e8a0cb67 --- /dev/null +++ b/beam/www/beam/vite.config.ts @@ -0,0 +1,156 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import vue from '@vitejs/plugin-vue' +import { existsSync } from 'fs' +import { resolve, dirname } from 'path' +import Components from 'unplugin-vue-components/vite' +import VueRouter from 'unplugin-vue-router/vite' +import { defineConfig } from 'vite' +import { VitePWA } from 'vite-plugin-pwa' + +import { getComponentPluginOptions } from './plugins/component.js' +import { getComponentPaths, getRoutes } from './plugins/router.js' + +function findAppsRoot(startPath: string = __dirname): string { + let currentPath = startPath + let parentDir = dirname(currentPath) + + while (currentPath !== '/' && currentPath !== parentDir) { + const dirName = currentPath.split('/').pop() + if (dirName === 'apps' && existsSync(currentPath)) { + return currentPath + } + + currentPath = parentDir + parentDir = dirname(currentPath) + } + + throw new Error('Could not find "apps" directory in parent path') +} + +function getBeamWebRoot() { + const appsRoot = findAppsRoot() + const beamWebPath = resolve(appsRoot, 'beam/beam/www/beam') + + if (!existsSync(beamWebPath)) { + throw new Error(`Beam web directory not found at expected path: ${beamWebPath}`) + } + + return beamWebPath +} + +function getBeamNode() { + const appsRoot = findAppsRoot() + const beamWebPath = resolve(appsRoot, 'beam/node_modules') + + if (!existsSync(beamWebPath)) { + throw new Error(`Beam web directory not found at expected path: ${beamWebPath}`) + } + + return beamWebPath +} + +export default defineConfig({ + plugins: [ + Components({ ...getComponentPluginOptions() }), + VueRouter({ + routesFolder: resolve(__dirname, 'routes'), + dts: 'beam/www/beam/typed-router.d.ts', + + beforeWriteFiles: root => { + // remove all existing routes + for (const child of root.children) { + child.delete() + } + + // add routes from all apps that have defined Beam routes + const routes = getRoutes() + const componentPaths = getComponentPaths() + if (routes) { + for (const route of routes) { + if (componentPaths[route.component]) { + const routeNode = root.insert(route.path, componentPaths[route.component]) + routeNode.name = route.name + routeNode.addToMeta({ ...route.meta }) + } + } + } + }, + }), + vue(), + VitePWA({ + registerType: 'autoUpdate', + manifest: { + name: 'Beam', + short_name: 'Beam', + description: 'AgriTheory Beam', + theme_color: '#7B4112', + icons: [ + { + src: '/beam/icon/beam_icon-192x192.png', + sizes: '192x192', + type: 'image/png', + }, + { + src: '/beam/icon/beam_icon-512x512.png', + sizes: '512x512', + type: 'image/png', + }, + ], + }, + workbox: { + globPatterns: ['**/*.{html,js,css,woff2,webmanifest}'], + maximumFileSizeToCacheInBytes: 5 * 1024 * 1024, + }, + }), + ], + resolve: { + alias: { + '@beam': getBeamWebRoot(), + '@beamNode': getBeamNode(), + '@': getBeamWebRoot(), + '@/plugins': resolve(__dirname, 'plugins'), + '@/types': resolve(__dirname, 'types'), + }, + }, + + build: { + emptyOutDir: false, + sourcemap: true, + outDir: './beam/www/beam/', + target: 'esnext', + lib: { + entry: resolve(__dirname, 'index.ts'), + name: 'beam', + formats: ['umd'], // only create module output for Frappe + fileName: () => 'index.js', + }, + rollupOptions: { + output: { + globals: { + vue: 'Vue', + 'vue-router': 'VueRouter', + pinia: 'Pinia', + '@vueuse/core': 'VueUse', + '@stonecrop/beam': 'Beam', + 'onscan.js': 'onScan', + 'vue-toast-notification': 'VueToast', + typescript: 'ts', + }, + chunkFileNames: 'chunks/[name].[hash].js', + assetFileNames: 'assets/[name].[ext]', + extend: true, + amd: { + id: 'beam', + }, + inlineDynamicImports: true, + }, + }, + }, + + define: { + 'process.env': process.env, + __VUE_PROD_DEVTOOLS__: true, + }, +}) diff --git a/package.json b/package.json index 1fce29d6..6fac20b9 100644 --- a/package.json +++ b/package.json @@ -1,22 +1,41 @@ { "name": "beam", - "scripts": {}, - "dependencies": { - "onscan.js": "^1.5.2" - }, - "devDependencies": {}, + "private": true, + "type": "module", "repository": { "type": "git", "url": "https://github.com/agritheory/beam.git" }, + "scripts": { + "dev": "vite --config=./beam/www/beam/vite.config.ts", + "build": "vite build --config=./beam/www/beam/vite.config.ts", + "build:watch": "vite build --watch --config=./beam/www/beam/vite.config.ts", + "register": "node ./beam/www/beam/resolvers.ts" + }, + "dependencies": { + "@stonecrop/aform": "^0.4.11", + "@stonecrop/beam": "^0.4.11", + "@vueuse/core": "^13.0.0", + "acorn": "^8.14.1", + "glob": "^11.0.1", + "onscan.js": "^1.5.2", + "pinia": "^3.0.1", + "vue": "^3.5.13", + "vue-router": "^4.5.0", + "vue-toast-notification": "^3.1.3" + }, + "devDependencies": { + "@types/node": "^20.17.30", + "@vitejs/plugin-vue": "^5.2.3", + "typescript": "^5.8.2", + "unplugin-vue-components": "^28.4.1", + "unplugin-vue-router": "^0.12.0", + "vite": "^5.4.16", + "vite-plugin-pwa": "^1.0.0", + "workbox-build": "^7.3.0", + "workbox-window": "^7.3.0" + }, "publishConfig": { "access": "restricted" - }, - "private": true, - "release": { - "branches": [ - "version-14", - "version-15" - ] } } diff --git a/poetry.lock b/poetry.lock index b8ca4a28..622b215d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,4 +1,139 @@ -# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. +# This file is automatically @generated by Poetry 2.3.2 and should not be changed by hand. + +[[package]] +name = "certifi" +version = "2026.2.25" +description = "Python package for providing Mozilla's CA Bundle." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa"}, + {file = "certifi-2026.2.25.tar.gz", hash = "sha256:e887ab5cee78ea814d3472169153c2d12cd43b14bd03329a39a9c6e2e80bfba7"}, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.4" +description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, + {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, + {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, + {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, + {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, + {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, + {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, + {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, + {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, + {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, +] [[package]] name = "colorama" @@ -6,6 +141,8 @@ version = "0.4.6" description = "Cross-platform colored terminal text." optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +groups = ["dev"] +markers = "sys_platform == \"win32\"" files = [ {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, @@ -13,90 +150,81 @@ files = [ [[package]] name = "coverage" -version = "7.6.1" +version = "7.6.8" description = "Code coverage measurement for Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "coverage-7.6.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b06079abebbc0e89e6163b8e8f0e16270124c154dc6e4a47b413dd538859af16"}, - {file = "coverage-7.6.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cf4b19715bccd7ee27b6b120e7e9dd56037b9c0681dcc1adc9ba9db3d417fa36"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e61c0abb4c85b095a784ef23fdd4aede7a2628478e7baba7c5e3deba61070a02"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:fd21f6ae3f08b41004dfb433fa895d858f3f5979e7762d052b12aef444e29afc"}, - {file = "coverage-7.6.1-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f59d57baca39b32db42b83b2a7ba6f47ad9c394ec2076b084c3f029b7afca23"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:a1ac0ae2b8bd743b88ed0502544847c3053d7171a3cff9228af618a068ed9c34"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e6a08c0be454c3b3beb105c0596ebdc2371fab6bb90c0c0297f4e58fd7e1012c"}, - {file = "coverage-7.6.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f5796e664fe802da4f57a168c85359a8fbf3eab5e55cd4e4569fbacecc903959"}, - {file = "coverage-7.6.1-cp310-cp310-win32.whl", hash = "sha256:7bb65125fcbef8d989fa1dd0e8a060999497629ca5b0efbca209588a73356232"}, - {file = "coverage-7.6.1-cp310-cp310-win_amd64.whl", hash = "sha256:3115a95daa9bdba70aea750db7b96b37259a81a709223c8448fa97727d546fe0"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7dea0889685db8550f839fa202744652e87c60015029ce3f60e006f8c4462c93"}, - {file = "coverage-7.6.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ed37bd3c3b063412f7620464a9ac1314d33100329f39799255fb8d3027da50d3"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d85f5e9a5f8b73e2350097c3756ef7e785f55bd71205defa0bfdaf96c31616ff"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9bc572be474cafb617672c43fe989d6e48d3c83af02ce8de73fff1c6bb3c198d"}, - {file = "coverage-7.6.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c0420b573964c760df9e9e86d1a9a622d0d27f417e1a949a8a66dd7bcee7bc6"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f4aa8219db826ce6be7099d559f8ec311549bfc4046f7f9fe9b5cea5c581c56"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:fc5a77d0c516700ebad189b587de289a20a78324bc54baee03dd486f0855d234"}, - {file = "coverage-7.6.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b48f312cca9621272ae49008c7f613337c53fadca647d6384cc129d2996d1133"}, - {file = "coverage-7.6.1-cp311-cp311-win32.whl", hash = "sha256:1125ca0e5fd475cbbba3bb67ae20bd2c23a98fac4e32412883f9bcbaa81c314c"}, - {file = "coverage-7.6.1-cp311-cp311-win_amd64.whl", hash = "sha256:8ae539519c4c040c5ffd0632784e21b2f03fc1340752af711f33e5be83a9d6c6"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:95cae0efeb032af8458fc27d191f85d1717b1d4e49f7cb226cf526ff28179778"}, - {file = "coverage-7.6.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5621a9175cf9d0b0c84c2ef2b12e9f5f5071357c4d2ea6ca1cf01814f45d2391"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:260933720fdcd75340e7dbe9060655aff3af1f0c5d20f46b57f262ab6c86a5e8"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:07e2ca0ad381b91350c0ed49d52699b625aab2b44b65e1b4e02fa9df0e92ad2d"}, - {file = "coverage-7.6.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c44fee9975f04b33331cb8eb272827111efc8930cfd582e0320613263ca849ca"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877abb17e6339d96bf08e7a622d05095e72b71f8afd8a9fefc82cf30ed944163"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3e0cadcf6733c09154b461f1ca72d5416635e5e4ec4e536192180d34ec160f8a"}, - {file = "coverage-7.6.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c3c02d12f837d9683e5ab2f3d9844dc57655b92c74e286c262e0fc54213c216d"}, - {file = "coverage-7.6.1-cp312-cp312-win32.whl", hash = "sha256:e05882b70b87a18d937ca6768ff33cc3f72847cbc4de4491c8e73880766718e5"}, - {file = "coverage-7.6.1-cp312-cp312-win_amd64.whl", hash = "sha256:b5d7b556859dd85f3a541db6a4e0167b86e7273e1cdc973e5b175166bb634fdb"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a4acd025ecc06185ba2b801f2de85546e0b8ac787cf9d3b06e7e2a69f925b106"}, - {file = "coverage-7.6.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a6d3adcf24b624a7b778533480e32434a39ad8fa30c315208f6d3e5542aeb6e9"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d0c212c49b6c10e6951362f7c6df3329f04c2b1c28499563d4035d964ab8e08c"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e81d7a3e58882450ec4186ca59a3f20a5d4440f25b1cff6f0902ad890e6748a"}, - {file = "coverage-7.6.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:78b260de9790fd81e69401c2dc8b17da47c8038176a79092a89cb2b7d945d060"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a78d169acd38300060b28d600344a803628c3fd585c912cacc9ea8790fe96862"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c09f4ce52cb99dd7505cd0fc8e0e37c77b87f46bc9c1eb03fe3bc9991085388"}, - {file = "coverage-7.6.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6878ef48d4227aace338d88c48738a4258213cd7b74fd9a3d4d7582bb1d8a155"}, - {file = "coverage-7.6.1-cp313-cp313-win32.whl", hash = "sha256:44df346d5215a8c0e360307d46ffaabe0f5d3502c8a1cefd700b34baf31d411a"}, - {file = "coverage-7.6.1-cp313-cp313-win_amd64.whl", hash = "sha256:8284cf8c0dd272a247bc154eb6c95548722dce90d098c17a883ed36e67cdb129"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3296782ca4eab572a1a4eca686d8bfb00226300dcefdf43faa25b5242ab8a3e"}, - {file = "coverage-7.6.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:502753043567491d3ff6d08629270127e0c31d4184c4c8d98f92c26f65019962"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6a89ecca80709d4076b95f89f308544ec8f7b4727e8a547913a35f16717856cb"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a318d68e92e80af8b00fa99609796fdbcdfef3629c77c6283566c6f02c6d6704"}, - {file = "coverage-7.6.1-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:13b0a73a0896988f053e4fbb7de6d93388e6dd292b0d87ee51d106f2c11b465b"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4421712dbfc5562150f7554f13dde997a2e932a6b5f352edcce948a815efee6f"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:166811d20dfea725e2e4baa71fffd6c968a958577848d2131f39b60043400223"}, - {file = "coverage-7.6.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:225667980479a17db1048cb2bf8bfb39b8e5be8f164b8f6628b64f78a72cf9d3"}, - {file = "coverage-7.6.1-cp313-cp313t-win32.whl", hash = "sha256:170d444ab405852903b7d04ea9ae9b98f98ab6d7e63e1115e82620807519797f"}, - {file = "coverage-7.6.1-cp313-cp313t-win_amd64.whl", hash = "sha256:b9f222de8cded79c49bf184bdbc06630d4c58eec9459b939b4a690c82ed05657"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:6db04803b6c7291985a761004e9060b2bca08da6d04f26a7f2294b8623a0c1a0"}, - {file = "coverage-7.6.1-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:f1adfc8ac319e1a348af294106bc6a8458a0f1633cc62a1446aebc30c5fa186a"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a95324a9de9650a729239daea117df21f4b9868ce32e63f8b650ebe6cef5595b"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b43c03669dc4618ec25270b06ecd3ee4fa94c7f9b3c14bae6571ca00ef98b0d3"}, - {file = "coverage-7.6.1-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8929543a7192c13d177b770008bc4e8119f2e1f881d563fc6b6305d2d0ebe9de"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:a09ece4a69cf399510c8ab25e0950d9cf2b42f7b3cb0374f95d2e2ff594478a6"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:9054a0754de38d9dbd01a46621636689124d666bad1936d76c0341f7d71bf569"}, - {file = "coverage-7.6.1-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:0dbde0f4aa9a16fa4d754356a8f2e36296ff4d83994b2c9d8398aa32f222f989"}, - {file = "coverage-7.6.1-cp38-cp38-win32.whl", hash = "sha256:da511e6ad4f7323ee5702e6633085fb76c2f893aaf8ce4c51a0ba4fc07580ea7"}, - {file = "coverage-7.6.1-cp38-cp38-win_amd64.whl", hash = "sha256:3f1156e3e8f2872197af3840d8ad307a9dd18e615dc64d9ee41696f287c57ad8"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:abd5fd0db5f4dc9289408aaf34908072f805ff7792632250dcb36dc591d24255"}, - {file = "coverage-7.6.1-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:547f45fa1a93154bd82050a7f3cddbc1a7a4dd2a9bf5cb7d06f4ae29fe94eaf8"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:645786266c8f18a931b65bfcefdbf6952dd0dea98feee39bd188607a9d307ed2"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9e0b2df163b8ed01d515807af24f63de04bebcecbd6c3bfeff88385789fdf75a"}, - {file = "coverage-7.6.1-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:609b06f178fe8e9f89ef676532760ec0b4deea15e9969bf754b37f7c40326dbc"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:702855feff378050ae4f741045e19a32d57d19f3e0676d589df0575008ea5004"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:2bdb062ea438f22d99cba0d7829c2ef0af1d768d1e4a4f528087224c90b132cb"}, - {file = "coverage-7.6.1-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:9c56863d44bd1c4fe2abb8a4d6f5371d197f1ac0ebdee542f07f35895fc07f36"}, - {file = "coverage-7.6.1-cp39-cp39-win32.whl", hash = "sha256:6e2cd258d7d927d09493c8df1ce9174ad01b381d4729a9d8d4e38670ca24774c"}, - {file = "coverage-7.6.1-cp39-cp39-win_amd64.whl", hash = "sha256:06a737c882bd26d0d6ee7269b20b12f14a8704807a01056c80bb881a4b2ce6ca"}, - {file = "coverage-7.6.1-pp38.pp39.pp310-none-any.whl", hash = "sha256:e9a6e0eb86070e8ccaedfbd9d38fec54864f3125ab95419970575b42af7541df"}, - {file = "coverage-7.6.1.tar.gz", hash = "sha256:953510dfb7b12ab69d20135a0662397f077c59b1e6379a768e97c59d852ee51d"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b39e6011cd06822eb964d038d5dff5da5d98652b81f5ecd439277b32361a3a50"}, + {file = "coverage-7.6.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:63c19702db10ad79151a059d2d6336fe0c470f2e18d0d4d1a57f7f9713875dcf"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3985b9be361d8fb6b2d1adc9924d01dec575a1d7453a14cccd73225cb79243ee"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:644ec81edec0f4ad17d51c838a7d01e42811054543b76d4ba2c5d6af741ce2a6"}, + {file = "coverage-7.6.8-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f188a2402f8359cf0c4b1fe89eea40dc13b52e7b4fd4812450da9fcd210181d"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:e19122296822deafce89a0c5e8685704c067ae65d45e79718c92df7b3ec3d331"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:13618bed0c38acc418896005732e565b317aa9e98d855a0e9f211a7ffc2d6638"}, + {file = "coverage-7.6.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:193e3bffca48ad74b8c764fb4492dd875038a2f9925530cb094db92bb5e47bed"}, + {file = "coverage-7.6.8-cp310-cp310-win32.whl", hash = "sha256:3988665ee376abce49613701336544041f2117de7b7fbfe91b93d8ff8b151c8e"}, + {file = "coverage-7.6.8-cp310-cp310-win_amd64.whl", hash = "sha256:f56f49b2553d7dd85fd86e029515a221e5c1f8cb3d9c38b470bc38bde7b8445a"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:86cffe9c6dfcfe22e28027069725c7f57f4b868a3f86e81d1c62462764dc46d4"}, + {file = "coverage-7.6.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d82ab6816c3277dc962cfcdc85b1efa0e5f50fb2c449432deaf2398a2928ab94"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13690e923a3932e4fad4c0ebfb9cb5988e03d9dcb4c5150b5fcbf58fd8bddfc4"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4be32da0c3827ac9132bb488d331cb32e8d9638dd41a0557c5569d57cf22c9c1"}, + {file = "coverage-7.6.8-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:44e6c85bbdc809383b509d732b06419fb4544dca29ebe18480379633623baafb"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:768939f7c4353c0fac2f7c37897e10b1414b571fd85dd9fc49e6a87e37a2e0d8"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e44961e36cb13c495806d4cac67640ac2866cb99044e210895b506c26ee63d3a"}, + {file = "coverage-7.6.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3ea8bb1ab9558374c0ab591783808511d135a833c3ca64a18ec927f20c4030f0"}, + {file = "coverage-7.6.8-cp311-cp311-win32.whl", hash = "sha256:629a1ba2115dce8bf75a5cce9f2486ae483cb89c0145795603d6554bdc83e801"}, + {file = "coverage-7.6.8-cp311-cp311-win_amd64.whl", hash = "sha256:fb9fc32399dca861584d96eccd6c980b69bbcd7c228d06fb74fe53e007aa8ef9"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e683e6ecc587643f8cde8f5da6768e9d165cd31edf39ee90ed7034f9ca0eefee"}, + {file = "coverage-7.6.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1defe91d41ce1bd44b40fabf071e6a01a5aa14de4a31b986aa9dfd1b3e3e414a"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7ad66e8e50225ebf4236368cc43c37f59d5e6728f15f6e258c8639fa0dd8e6d"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3fe47da3e4fda5f1abb5709c156eca207eacf8007304ce3019eb001e7a7204cb"}, + {file = "coverage-7.6.8-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:202a2d645c5a46b84992f55b0a3affe4f0ba6b4c611abec32ee88358db4bb649"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4674f0daa1823c295845b6a740d98a840d7a1c11df00d1fd62614545c1583787"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:74610105ebd6f33d7c10f8907afed696e79c59e3043c5f20eaa3a46fddf33b4c"}, + {file = "coverage-7.6.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:37cda8712145917105e07aab96388ae76e787270ec04bcb9d5cc786d7cbb8443"}, + {file = "coverage-7.6.8-cp312-cp312-win32.whl", hash = "sha256:9e89d5c8509fbd6c03d0dd1972925b22f50db0792ce06324ba069f10787429ad"}, + {file = "coverage-7.6.8-cp312-cp312-win_amd64.whl", hash = "sha256:379c111d3558272a2cae3d8e57e6b6e6f4fe652905692d54bad5ea0ca37c5ad4"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b0c69f4f724c64dfbfe79f5dfb503b42fe6127b8d479b2677f2b227478db2eb"}, + {file = "coverage-7.6.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c15b32a7aca8038ed7644f854bf17b663bc38e1671b5d6f43f9a2b2bd0c46f63"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:63068a11171e4276f6ece913bde059e77c713b48c3a848814a6537f35afb8365"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6f4548c5ead23ad13fb7a2c8ea541357474ec13c2b736feb02e19a3085fac002"}, + {file = "coverage-7.6.8-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b4b4299dd0d2c67caaaf286d58aef5e75b125b95615dda4542561a5a566a1e3"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9ebfb2507751f7196995142f057d1324afdab56db1d9743aab7f50289abd022"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:c1b4474beee02ede1eef86c25ad4600a424fe36cff01a6103cb4533c6bf0169e"}, + {file = "coverage-7.6.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d9fd2547e6decdbf985d579cf3fc78e4c1d662b9b0ff7cc7862baaab71c9cc5b"}, + {file = "coverage-7.6.8-cp313-cp313-win32.whl", hash = "sha256:8aae5aea53cbfe024919715eca696b1a3201886ce83790537d1c3668459c7146"}, + {file = "coverage-7.6.8-cp313-cp313-win_amd64.whl", hash = "sha256:ae270e79f7e169ccfe23284ff5ea2d52a6f401dc01b337efb54b3783e2ce3f28"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:de38add67a0af869b0d79c525d3e4588ac1ffa92f39116dbe0ed9753f26eba7d"}, + {file = "coverage-7.6.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b07c25d52b1c16ce5de088046cd2432b30f9ad5e224ff17c8f496d9cb7d1d451"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:62a66ff235e4c2e37ed3b6104d8b478d767ff73838d1222132a7a026aa548764"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:09b9f848b28081e7b975a3626e9081574a7b9196cde26604540582da60235fdf"}, + {file = "coverage-7.6.8-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:093896e530c38c8e9c996901858ac63f3d4171268db2c9c8b373a228f459bbc5"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9a7b8ac36fd688c8361cbc7bf1cb5866977ece6e0b17c34aa0df58bda4fa18a4"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:38c51297b35b3ed91670e1e4efb702b790002e3245a28c76e627478aa3c10d83"}, + {file = "coverage-7.6.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2e4e0f60cb4bd7396108823548e82fdab72d4d8a65e58e2c19bbbc2f1e2bfa4b"}, + {file = "coverage-7.6.8-cp313-cp313t-win32.whl", hash = "sha256:6535d996f6537ecb298b4e287a855f37deaf64ff007162ec0afb9ab8ba3b8b71"}, + {file = "coverage-7.6.8-cp313-cp313t-win_amd64.whl", hash = "sha256:c79c0685f142ca53256722a384540832420dff4ab15fec1863d7e5bc8691bdcc"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:3ac47fa29d8d41059ea3df65bd3ade92f97ee4910ed638e87075b8e8ce69599e"}, + {file = "coverage-7.6.8-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:24eda3a24a38157eee639ca9afe45eefa8d2420d49468819ac5f88b10de84f4c"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4c81ed2820b9023a9a90717020315e63b17b18c274a332e3b6437d7ff70abe0"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bd55f8fc8fa494958772a2a7302b0354ab16e0b9272b3c3d83cdb5bec5bd1779"}, + {file = "coverage-7.6.8-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f39e2f3530ed1626c66e7493be7a8423b023ca852aacdc91fb30162c350d2a92"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:716a78a342679cd1177bc8c2fe957e0ab91405bd43a17094324845200b2fddf4"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:177f01eeaa3aee4a5ffb0d1439c5952b53d5010f86e9d2667963e632e30082cc"}, + {file = "coverage-7.6.8-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:912e95017ff51dc3d7b6e2be158dedc889d9a5cc3382445589ce554f1a34c0ea"}, + {file = "coverage-7.6.8-cp39-cp39-win32.whl", hash = "sha256:4db3ed6a907b555e57cc2e6f14dc3a4c2458cdad8919e40b5357ab9b6db6c43e"}, + {file = "coverage-7.6.8-cp39-cp39-win_amd64.whl", hash = "sha256:428ac484592f780e8cd7b6b14eb568f7c85460c92e2a37cb0c0e5186e1a0d076"}, + {file = "coverage-7.6.8-pp39.pp310-none-any.whl", hash = "sha256:5c52a036535d12590c32c49209e79cabaad9f9ad8aa4cbd875b68c4d67a9cbce"}, + {file = "coverage-7.6.8.tar.gz", hash = "sha256:8b2b8503edb06822c86d82fa64a4a5cb0760bb8f31f26e138ec743f422f37cfc"}, ] [package.dependencies] tomli = {version = "*", optional = true, markers = "python_full_version <= \"3.11.0a6\" and extra == \"toml\""} [package.extras] -toml = ["tomli"] +toml = ["tomli ; python_full_version <= \"3.11.0a6\""] [[package]] name = "exceptiongroup" @@ -104,6 +232,8 @@ version = "1.2.2" description = "Backport of PEP 654 (exception groups)" optional = false python-versions = ">=3.7" +groups = ["dev"] +markers = "python_version == \"3.10\"" files = [ {file = "exceptiongroup-1.2.2-py3-none-any.whl", hash = "sha256:3111b9d131c238bec2f8f516e123e14ba243563fb135d3fe885990585aa7795b"}, {file = "exceptiongroup-1.2.2.tar.gz", hash = "sha256:47c2edf7c6738fafb49fd34290706d1a1a2f4d1c6df275526b62cbb4aa5393cc"}, @@ -112,12 +242,115 @@ files = [ [package.extras] test = ["pytest (>=6)"] +[[package]] +name = "greenlet" +version = "3.1.1" +description = "Lightweight in-process concurrent programming" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "greenlet-3.1.1-cp310-cp310-macosx_11_0_universal2.whl", hash = "sha256:0bbae94a29c9e5c7e4a2b7f0aae5c17e8e90acbfd3bf6270eeba60c39fce3563"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0fde093fb93f35ca72a556cf72c92ea3ebfda3d79fc35bb19fbe685853869a83"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:36b89d13c49216cadb828db8dfa6ce86bbbc476a82d3a6c397f0efae0525bdd0"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94b6150a85e1b33b40b1464a3f9988dcc5251d6ed06842abff82e42632fac120"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:93147c513fac16385d1036b7e5b102c7fbbdb163d556b791f0f11eada7ba65dc"}, + {file = "greenlet-3.1.1-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:da7a9bff22ce038e19bf62c4dd1ec8391062878710ded0a845bcf47cc0200617"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b2795058c23988728eec1f36a4e5e4ebad22f8320c85f3587b539b9ac84128d7"}, + {file = "greenlet-3.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:ed10eac5830befbdd0c32f83e8aa6288361597550ba669b04c48f0f9a2c843c6"}, + {file = "greenlet-3.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:77c386de38a60d1dfb8e55b8c1101d68c79dfdd25c7095d51fec2dd800892b80"}, + {file = "greenlet-3.1.1-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:e4d333e558953648ca09d64f13e6d8f0523fa705f51cae3f03b5983489958c70"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:09fc016b73c94e98e29af67ab7b9a879c307c6731a2c9da0db5a7d9b7edd1159"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d5e975ca70269d66d17dd995dafc06f1b06e8cb1ec1e9ed54c1d1e4a7c4cf26e"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b2813dc3de8c1ee3f924e4d4227999285fd335d1bcc0d2be6dc3f1f6a318ec1"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e347b3bfcf985a05e8c0b7d462ba6f15b1ee1c909e2dcad795e49e91b152c383"}, + {file = "greenlet-3.1.1-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9e8f8c9cb53cdac7ba9793c276acd90168f416b9ce36799b9b885790f8ad6c0a"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:62ee94988d6b4722ce0028644418d93a52429e977d742ca2ccbe1c4f4a792511"}, + {file = "greenlet-3.1.1-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:1776fd7f989fc6b8d8c8cb8da1f6b82c5814957264d1f6cf818d475ec2bf6395"}, + {file = "greenlet-3.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:48ca08c771c268a768087b408658e216133aecd835c0ded47ce955381105ba39"}, + {file = "greenlet-3.1.1-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:4afe7ea89de619adc868e087b4d2359282058479d7cfb94970adf4b55284574d"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f406b22b7c9a9b4f8aa9d2ab13d6ae0ac3e85c9a809bd590ad53fed2bf70dc79"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c3a701fe5a9695b238503ce5bbe8218e03c3bcccf7e204e455e7462d770268aa"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2846930c65b47d70b9d178e89c7e1a69c95c1f68ea5aa0a58646b7a96df12441"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99cfaa2110534e2cf3ba31a7abcac9d328d1d9f1b95beede58294a60348fba36"}, + {file = "greenlet-3.1.1-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1443279c19fca463fc33e65ef2a935a5b09bb90f978beab37729e1c3c6c25fe9"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b7cede291382a78f7bb5f04a529cb18e068dd29e0fb27376074b6d0317bf4dd0"}, + {file = "greenlet-3.1.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:23f20bb60ae298d7d8656c6ec6db134bca379ecefadb0b19ce6f19d1f232a942"}, + {file = "greenlet-3.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:7124e16b4c55d417577c2077be379514321916d5790fa287c9ed6f23bd2ffd01"}, + {file = "greenlet-3.1.1-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:05175c27cb459dcfc05d026c4232f9de8913ed006d42713cb8a5137bd49375f1"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:935e943ec47c4afab8965954bf49bfa639c05d4ccf9ef6e924188f762145c0ff"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:667a9706c970cb552ede35aee17339a18e8f2a87a51fba2ed39ceeeb1004798a"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:b8a678974d1f3aa55f6cc34dc480169d58f2e6d8958895d68845fa4ab566509e"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:efc0f674aa41b92da8c49e0346318c6075d734994c3c4e4430b1c3f853e498e4"}, + {file = "greenlet-3.1.1-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0153404a4bb921f0ff1abeb5ce8a5131da56b953eda6e14b88dc6bbc04d2049e"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:275f72decf9932639c1c6dd1013a1bc266438eb32710016a1c742df5da6e60a1"}, + {file = "greenlet-3.1.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:c4aab7f6381f38a4b42f269057aee279ab0fc7bf2e929e3d4abfae97b682a12c"}, + {file = "greenlet-3.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:b42703b1cf69f2aa1df7d1030b9d77d3e584a70755674d60e710f0af570f3761"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f1695e76146579f8c06c1509c7ce4dfe0706f49c6831a817ac04eebb2fd02011"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7876452af029456b3f3549b696bb36a06db7c90747740c5302f74a9e9fa14b13"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4ead44c85f8ab905852d3de8d86f6f8baf77109f9da589cb4fa142bd3b57b475"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8320f64b777d00dd7ccdade271eaf0cad6636343293a25074cc5566160e4de7b"}, + {file = "greenlet-3.1.1-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6510bf84a6b643dabba74d3049ead221257603a253d0a9873f55f6a59a65f822"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_aarch64.whl", hash = "sha256:04b013dc07c96f83134b1e99888e7a79979f1a247e2a9f59697fa14b5862ed01"}, + {file = "greenlet-3.1.1-cp313-cp313t-musllinux_1_1_x86_64.whl", hash = "sha256:411f015496fec93c1c8cd4e5238da364e1da7a124bcb293f085bf2860c32c6f6"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:47da355d8687fd65240c364c90a31569a133b7b60de111c255ef5b606f2ae291"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:98884ecf2ffb7d7fe6bd517e8eb99d31ff7855a840fa6d0d63cd07c037f6a981"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f1d4aeb8891338e60d1ab6127af1fe45def5259def8094b9c7e34690c8858803"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db32b5348615a04b82240cc67983cb315309e88d444a288934ee6ceaebcad6cc"}, + {file = "greenlet-3.1.1-cp37-cp37m-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dcc62f31eae24de7f8dce72134c8651c58000d3b1868e01392baea7c32c247de"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:1d3755bcb2e02de341c55b4fca7a745a24a9e7212ac953f6b3a48d117d7257aa"}, + {file = "greenlet-3.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:b8da394b34370874b4572676f36acabac172602abf054cbc4ac910219f3340af"}, + {file = "greenlet-3.1.1-cp37-cp37m-win32.whl", hash = "sha256:a0dfc6c143b519113354e780a50381508139b07d2177cb6ad6a08278ec655798"}, + {file = "greenlet-3.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:54558ea205654b50c438029505def3834e80f0869a70fb15b871c29b4575ddef"}, + {file = "greenlet-3.1.1-cp38-cp38-macosx_11_0_universal2.whl", hash = "sha256:346bed03fe47414091be4ad44786d1bd8bef0c3fcad6ed3dee074a032ab408a9"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dfc59d69fc48664bc693842bd57acfdd490acafda1ab52c7836e3fc75c90a111"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d21e10da6ec19b457b82636209cbe2331ff4306b54d06fa04b7c138ba18c8a81"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:37b9de5a96111fc15418819ab4c4432e4f3c2ede61e660b1e33971eba26ef9ba"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6ef9ea3f137e5711f0dbe5f9263e8c009b7069d8a1acea822bd5e9dae0ae49c8"}, + {file = "greenlet-3.1.1-cp38-cp38-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:85f3ff71e2e60bd4b4932a043fbbe0f499e263c628390b285cb599154a3b03b1"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:95ffcf719966dd7c453f908e208e14cde192e09fde6c7186c8f1896ef778d8cd"}, + {file = "greenlet-3.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:03a088b9de532cbfe2ba2034b2b85e82df37874681e8c470d6fb2f8c04d7e4b7"}, + {file = "greenlet-3.1.1-cp38-cp38-win32.whl", hash = "sha256:8b8b36671f10ba80e159378df9c4f15c14098c4fd73a36b9ad715f057272fbef"}, + {file = "greenlet-3.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:7017b2be767b9d43cc31416aba48aab0d2309ee31b4dbf10a1d38fb7972bdf9d"}, + {file = "greenlet-3.1.1-cp39-cp39-macosx_11_0_universal2.whl", hash = "sha256:396979749bd95f018296af156201d6211240e7a23090f50a8d5d18c370084dc3"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ca9d0ff5ad43e785350894d97e13633a66e2b50000e8a183a50a88d834752d42"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f6ff3b14f2df4c41660a7dec01045a045653998784bf8cfcb5a525bdffffbc8f"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:94ebba31df2aa506d7b14866fed00ac141a867e63143fe5bca82a8e503b36437"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:73aaad12ac0ff500f62cebed98d8789198ea0e6f233421059fa68a5aa7220145"}, + {file = "greenlet-3.1.1-cp39-cp39-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:63e4844797b975b9af3a3fb8f7866ff08775f5426925e1e0bbcfe7932059a12c"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:7939aa3ca7d2a1593596e7ac6d59391ff30281ef280d8632fa03d81f7c5f955e"}, + {file = "greenlet-3.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:d0028e725ee18175c6e422797c407874da24381ce0690d6b9396c204c7f7276e"}, + {file = "greenlet-3.1.1-cp39-cp39-win32.whl", hash = "sha256:5e06afd14cbaf9e00899fae69b24a32f2196c19de08fcb9f4779dd4f004e5e7c"}, + {file = "greenlet-3.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:3319aa75e0e0639bc15ff54ca327e8dc7a6fe404003496e3c6925cd3142e0e22"}, + {file = "greenlet-3.1.1.tar.gz", hash = "sha256:4ce3ac6cdb6adf7946475d7ef31777c26d94bccc377e070a7986bd2d5c515467"}, +] + +[package.extras] +docs = ["Sphinx", "furo"] +test = ["objgraph", "psutil"] + +[[package]] +name = "idna" +version = "3.10" +description = "Internationalized Domain Names in Applications (IDNA)" +optional = false +python-versions = ">=3.6" +groups = ["dev"] +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] + +[package.extras] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] + [[package]] name = "iniconfig" version = "2.0.0" description = "brain-dead simple config-ini parsing" optional = false python-versions = ">=3.7" +groups = ["dev"] files = [ {file = "iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374"}, {file = "iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3"}, @@ -125,21 +358,44 @@ files = [ [[package]] name = "packaging" -version = "24.1" +version = "24.2" description = "Core utilities for Python packages" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "packaging-24.1-py3-none-any.whl", hash = "sha256:5b8f2217dbdbd2f7f384c41c628544e6d52f2d0f53c6d0c3ea61aa5d1d7ff124"}, - {file = "packaging-24.1.tar.gz", hash = "sha256:026ed72c8ed3fcce5bf8950572258698927fd1dbda10a5e981cdf0ac37f4f002"}, + {file = "packaging-24.2-py3-none-any.whl", hash = "sha256:09abb1bccd265c01f4a3aa3f7a7db064b36514d2cba19a2f694fe6150451a759"}, + {file = "packaging-24.2.tar.gz", hash = "sha256:c228a6dc5e932d346bc5739379109d49e8853dd8223571c7c5b55260edc0b97f"}, ] +[[package]] +name = "playwright" +version = "1.49.0" +description = "A high-level API to automate web browsers" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "playwright-1.49.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:704532a2d8ba580ec9e1895bfeafddce2e3d52320d4eb8aa38e80376acc5cbb0"}, + {file = "playwright-1.49.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:e453f02c4e5cc2db7e9759c47e7425f32e50ac76c76b7eb17c69eed72f01c4d8"}, + {file = "playwright-1.49.0-py3-none-macosx_11_0_universal2.whl", hash = "sha256:37ae985309184472946a6eb1a237e5d93c9e58a781fa73b75c8751325002a5d4"}, + {file = "playwright-1.49.0-py3-none-manylinux1_x86_64.whl", hash = "sha256:68d94beffb3c9213e3ceaafa66171affd9a5d9162e0c8a3eed1b1132c2e57598"}, + {file = "playwright-1.49.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7f12d2aecdb41fc25a624cb15f3e8391c252ebd81985e3d5c1c261fe93779345"}, + {file = "playwright-1.49.0-py3-none-win32.whl", hash = "sha256:91103de52d470594ad375b512d7143fa95d6039111ae11a93eb4fe2f2b4a4858"}, + {file = "playwright-1.49.0-py3-none-win_amd64.whl", hash = "sha256:34d28a2c2d46403368610be4339898dc9c34eb9f7c578207b4715c49743a072a"}, +] + +[package.dependencies] +greenlet = "3.1.1" +pyee = "12.0.0" + [[package]] name = "pluggy" version = "1.5.0" description = "plugin and hook calling mechanisms for python" optional = false python-versions = ">=3.8" +groups = ["dev"] files = [ {file = "pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669"}, {file = "pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1"}, @@ -149,66 +405,142 @@ files = [ dev = ["pre-commit", "tox"] testing = ["pytest", "pytest-benchmark"] +[[package]] +name = "pyee" +version = "12.0.0" +description = "A rough port of Node.js's EventEmitter to Python with a few tricks of its own" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pyee-12.0.0-py3-none-any.whl", hash = "sha256:7b14b74320600049ccc7d0e0b1becd3b4bd0a03c745758225e31a59f4095c990"}, + {file = "pyee-12.0.0.tar.gz", hash = "sha256:c480603f4aa2927d4766eb41fa82793fe60a82cbfdb8d688e0d08c55a534e145"}, +] + +[package.dependencies] +typing-extensions = "*" + +[package.extras] +dev = ["black", "build", "flake8", "flake8-black", "isort", "jupyter-console", "mkdocs", "mkdocs-include-markdown-plugin", "mkdocstrings[python]", "pytest", "pytest-asyncio ; python_version >= \"3.4\"", "pytest-trio ; python_version >= \"3.7\"", "sphinx", "toml", "tox", "trio", "trio ; python_version > \"3.6\"", "trio-typing ; python_version > \"3.6\"", "twine", "twisted", "validate-pyproject[all]"] + +[[package]] +name = "pygments" +version = "2.19.2" +description = "Pygments is a syntax highlighting package written in Python." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, + {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, +] + +[package.extras] +windows-terminal = ["colorama (>=0.4.6)"] + [[package]] name = "pytest" -version = "8.3.2" +version = "8.4.1" description = "pytest: simple powerful testing with Python" optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pytest-8.3.2-py3-none-any.whl", hash = "sha256:4ba08f9ae7dcf84ded419494d229b48d0903ea6407b030eaec46df5e6a73bba5"}, - {file = "pytest-8.3.2.tar.gz", hash = "sha256:c132345d12ce551242c87269de812483f5bcc87cdbb4722e48487ba194f9fdce"}, + {file = "pytest-8.4.1-py3-none-any.whl", hash = "sha256:539c70ba6fcead8e78eebbf1115e8b589e7565830d7d006a8723f19ac8a0afb7"}, + {file = "pytest-8.4.1.tar.gz", hash = "sha256:7c67fd69174877359ed9371ec3af8a3d2b04741818c51e5e99cc1742251fa93c"}, ] [package.dependencies] -colorama = {version = "*", markers = "sys_platform == \"win32\""} -exceptiongroup = {version = ">=1.0.0rc8", markers = "python_version < \"3.11\""} -iniconfig = "*" -packaging = "*" +colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} +exceptiongroup = {version = ">=1", markers = "python_version < \"3.11\""} +iniconfig = ">=1" +packaging = ">=20" pluggy = ">=1.5,<2" +pygments = ">=2.7.2" tomli = {version = ">=1", markers = "python_version < \"3.11\""} [package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "pygments (>=2.7.2)", "requests", "setuptools", "xmlschema"] +dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] + +[[package]] +name = "pytest-base-url" +version = "2.1.0" +description = "pytest plugin for URL based testing" +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "pytest_base_url-2.1.0-py3-none-any.whl", hash = "sha256:3ad15611778764d451927b2a53240c1a7a591b521ea44cebfe45849d2d2812e6"}, + {file = "pytest_base_url-2.1.0.tar.gz", hash = "sha256:02748589a54f9e63fcbe62301d6b0496da0d10231b753e950c63e03aee745d45"}, +] + +[package.dependencies] +pytest = ">=7.0.0" +requests = ">=2.9" + +[package.extras] +test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "pytest-localserver (>=0.7.1)", "tox (>=3.24.5)"] [[package]] name = "pytest-cov" -version = "5.0.0" +version = "6.2.1" description = "Pytest plugin for measuring coverage." optional = false -python-versions = ">=3.8" +python-versions = ">=3.9" +groups = ["dev"] files = [ - {file = "pytest-cov-5.0.0.tar.gz", hash = "sha256:5837b58e9f6ebd335b0f8060eecce69b662415b16dc503883a02f45dfeb14857"}, - {file = "pytest_cov-5.0.0-py3-none-any.whl", hash = "sha256:4f0764a1219df53214206bf1feea4633c3b558a2925c8b59f144f682861ce652"}, + {file = "pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5"}, + {file = "pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2"}, ] [package.dependencies] -coverage = {version = ">=5.2.1", extras = ["toml"]} -pytest = ">=4.6" +coverage = {version = ">=7.5", extras = ["toml"]} +pluggy = ">=1.2" +pytest = ">=6.2.5" [package.extras] testing = ["fields", "hunter", "process-tests", "pytest-xdist", "virtualenv"] [[package]] name = "pytest-order" -version = "1.2.1" +version = "1.3.0" description = "pytest plugin to run your tests in a specific order" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +groups = ["dev"] files = [ - {file = "pytest-order-1.2.1.tar.gz", hash = "sha256:4451bd8821ba4fa2109455a2fcc882af60ef8e53e09d244d67674be08f56eac3"}, - {file = "pytest_order-1.2.1-py3-none-any.whl", hash = "sha256:c3082fc73f9ddcf13e4a22dda9bbcc2f39865bf537438a1d50fa241e028dd743"}, + {file = "pytest_order-1.3.0-py3-none-any.whl", hash = "sha256:2cd562a21380345dd8d5774aa5fd38b7849b6ee7397ca5f6999bbe6e89f07f6e"}, + {file = "pytest_order-1.3.0.tar.gz", hash = "sha256:51608fec3d3ee9c0adaea94daa124a5c4c1d2bb99b00269f098f414307f23dde"}, ] [package.dependencies] pytest = {version = ">=6.2.4", markers = "python_version >= \"3.10\""} +[[package]] +name = "pytest-playwright" +version = "0.7.0" +description = "A pytest wrapper with fixtures for Playwright to automate web browsers" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "pytest_playwright-0.7.0-py3-none-any.whl", hash = "sha256:2516d0871fa606634bfe32afbcc0342d68da2dbff97fe3459849e9c428486da2"}, + {file = "pytest_playwright-0.7.0.tar.gz", hash = "sha256:b3f2ea514bbead96d26376fac182f68dcd6571e7cb41680a89ff1673c05d60b6"}, +] + +[package.dependencies] +playwright = ">=1.18" +pytest = ">=6.2.4,<9.0.0" +pytest-base-url = ">=1.0.0,<3.0.0" +python-slugify = ">=6.0.0,<9.0.0" + [[package]] name = "python-barcode" version = "0.15.1" description = "Create standard barcodes with Python. No external modules needed. (optional Pillow support included)." optional = false python-versions = "*" +groups = ["main"] files = [ {file = "python-barcode-0.15.1.tar.gz", hash = "sha256:3b1825fbdb11e597466dff4286b4ea9b1e86a57717b59e563ae679726fc854de"}, {file = "python_barcode-0.15.1-py3-none-any.whl", hash = "sha256:057636fba37369c22852410c8535b36adfbeb965ddfd4e5b6924455d692e0886"}, @@ -217,23 +549,108 @@ files = [ [package.extras] images = ["pillow"] +[[package]] +name = "python-slugify" +version = "8.0.4" +description = "A Python slugify application that also handles Unicode" +optional = false +python-versions = ">=3.7" +groups = ["dev"] +files = [ + {file = "python-slugify-8.0.4.tar.gz", hash = "sha256:59202371d1d05b54a9e7720c5e038f928f45daaffe41dd10822f3907b937c856"}, + {file = "python_slugify-8.0.4-py2.py3-none-any.whl", hash = "sha256:276540b79961052b66b7d116620b36518847f52d5fd9e3a70164fc8c50faa6b8"}, +] + +[package.dependencies] +text-unidecode = ">=1.3" + +[package.extras] +unidecode = ["Unidecode (>=1.1.1)"] + +[[package]] +name = "requests" +version = "2.32.3" +description = "Python HTTP for Humans." +optional = false +python-versions = ">=3.8" +groups = ["dev"] +files = [ + {file = "requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6"}, + {file = "requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760"}, +] + +[package.dependencies] +certifi = ">=2017.4.17" +charset-normalizer = ">=2,<4" +idna = ">=2.5,<4" +urllib3 = ">=1.21.1,<3" + +[package.extras] +socks = ["PySocks (>=1.5.6,!=1.5.7)"] +use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] + +[[package]] +name = "text-unidecode" +version = "1.3" +description = "The most basic Text::Unidecode port" +optional = false +python-versions = "*" +groups = ["dev"] +files = [ + {file = "text-unidecode-1.3.tar.gz", hash = "sha256:bad6603bb14d279193107714b288be206cac565dfa49aa5b105294dd5c4aab93"}, + {file = "text_unidecode-1.3-py2.py3-none-any.whl", hash = "sha256:1311f10e8b895935241623731c2ba64f4c455287888b18189350b67134a822e8"}, +] + [[package]] name = "tomli" -version = "2.0.1" +version = "2.1.0" description = "A lil' TOML parser" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +groups = ["dev"] +markers = "python_full_version <= \"3.11.0a6\"" +files = [ + {file = "tomli-2.1.0-py3-none-any.whl", hash = "sha256:a5c57c3d1c56f5ccdf89f6523458f60ef716e210fc47c4cfb188c5ba473e0391"}, + {file = "tomli-2.1.0.tar.gz", hash = "sha256:3f646cae2aec94e17d04973e4249548320197cfabdf130015d023de4b74d8ab8"}, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +description = "Backported and Experimental Type Hints for Python 3.9+" +optional = false +python-versions = ">=3.9" +groups = ["dev"] +files = [ + {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, + {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, +] + +[[package]] +name = "urllib3" +version = "2.2.3" +description = "HTTP library with thread-safe connection pooling, file post, and more." +optional = false +python-versions = ">=3.8" +groups = ["dev"] files = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, + {file = "urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac"}, + {file = "urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9"}, ] +[package.extras] +brotli = ["brotli (>=1.0.9) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=0.8.0) ; platform_python_implementation != \"CPython\""] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] + [[package]] name = "zebra-zpl" version = "0.1.0" description = "Python library to generate usable and printable ZPL2 code" optional = false python-versions = "*" +groups = ["main"] files = [] develop = false @@ -244,6 +661,6 @@ reference = "HEAD" resolved_reference = "45ffc60638814df575d9fe11c7504b1a533e4ecb" [metadata] -lock-version = "2.0" -python-versions = ">=3.10" -content-hash = "12be1441efa3267b06c916360cc5d6b3f0a9588850b760b5946904e6ac3cbdbe" +lock-version = "2.1" +python-versions = ">=3.10,<3.14" +content-hash = "943528828f8114492fa2ba8112b91b96557d0182e2507a3c8627db881cb22e8e" diff --git a/pyproject.toml b/pyproject.toml index 1c7b305c..9b3c7c8a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,33 +1,51 @@ -[tool.poetry] +[project] name = "beam" -version = "15.4.0" -authors = ["AgriTheory "] +version = "15.8.0" +authors = [ + { name = "AgriTheory", email = "support@agritheory.dev" } +] description = "Barcode Scanning for ERPNext" +requires-python = ">=3.10" readme = "README.md" +license = { file = "LICENSE" } +dynamic = [ "version", "dependencies", "requires-python" ] + +[tool.poetry] +version = "15.4.0" [tool.poetry.dependencies] -python = ">=3.10" +python = ">=3.10,<3.14" python-barcode = "^0.15.1" zebra-zpl = {git = "https://github.com/mtking2/py-zebra-zpl.git"} +[tool.bench.dev-dependencies] +pytest = "~=8.3.2" +pytest-cov = "~=5.0.0" +pytest-order = "~=1.2.1" + [tool.poetry.group.dev.dependencies] -pytest = "^8.3.2" -pytest-order = "^1.2.1" -pytest-cov = "^5.0.0" +pytest = "^8.4.1" +pytest-cov = "^6.2.1" +pytest-order = "^1.3.0" +pytest-playwright = "^0.7.0" [build-system] -requires = ["poetry-core"] +requires = ["poetry-core>=2.0.0,<3.0.0"] build-backend = "poetry.core.masonry.api" +[tool.bench.frappe-dependencies] +frappe = ">=15.0.0,<16.0.0" +erpnext = ">=15.0.0,<16.0.0" + [tool.pytest.ini_options] addopts = "--cov=beam --cov-report term-missing" -[tool.codespell] -skip = "CHANGELOG.md,*.js.map" - [tool.black] line-length = 99 +[tool.codespell] +skip = '*.md, yarn.lock, *.js.map' + [tool.isort] line_length = 99 multi_line_output = 3 @@ -37,11 +55,37 @@ use_parentheses = true ensure_newline_before_comments = true indent = "\t" +[tool.coverage.report] +skip_covered = true +skip_empty = true +omit = [ + "*/boot.py", + "*/customize.py", + "*/hooks.py", + "*/install.py", + "*/patches/*", + "*/test_*.py", + "*/tests/*", +] +# https://coverage.readthedocs.io/en/latest/excluding.html#advanced-exclusion +exclude_also = [ + "def __repr__", + "if self.debug:", + "if settings.DEBUG", + "raise AssertionError", + "raise NotImplementedError", + "if 0:", + "if __name__ == .__main__.:", + "if TYPE_CHECKING:", + "class .*\\bProtocol\\):", + "@(abc\\.)?abstractmethod", +] + [tool.semantic_release] -version_variables = [ - "beam/__init__.py:__version__", - "pyproject.toml:version" +version_toml = ["pyproject.toml:tool.poetry.version"] +version_variable = [ + "beam/__init__.py:__version__" ] [tool.semantic_release.branches.version] -match = "version-15" +match = "version-15" \ No newline at end of file diff --git a/setup.py b/setup.py index 8e79b4d1..09463f34 100644 --- a/setup.py +++ b/setup.py @@ -1,9 +1,12 @@ -# Copyright (c) 2025, AgriTheory and contributors +# Copyright (c) 2026, AgriTheory and contributors # For license information, please see license.txt -from setuptools import setup +from setuptools import find_packages, setup -# TODO: Remove this file when bench >=v5.11.0 is adopted / v15.0.0 is released -name = "beam" - -setup() +setup( + name="beam", + version="14.8.7", + packages=find_packages(), + include_package_data=True, + zip_safe=False, +) diff --git a/yarn.lock b/yarn.lock index fa1b1d67..4feccf77 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,4081 @@ # yarn lockfile v1 +"@ampproject/remapping@^2.2.0": + version "2.3.0" + resolved "https://registry.yarnpkg.com/@ampproject/remapping/-/remapping-2.3.0.tgz#ed441b6fa600072520ce18b43d2c8cc8caecc7f4" + integrity sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.24" + +"@apideck/better-ajv-errors@^0.3.1": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@apideck/better-ajv-errors/-/better-ajv-errors-0.3.6.tgz#957d4c28e886a64a8141f7522783be65733ff097" + integrity sha512-P+ZygBLZtkp0qqOAJJVX4oX/sFo5JR3eBWwwuqHHhK0GIgQOKWrAfiAaWX0aArHkRWHMuggFEgAZNxVPwPZYaA== + dependencies: + json-schema "^0.4.0" + jsonpointer "^5.0.0" + leven "^3.1.0" + +"@babel/code-frame@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/code-frame/-/code-frame-7.27.1.tgz#200f715e66d52a23b221a9435534a91cc13ad5be" + integrity sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg== + dependencies: + "@babel/helper-validator-identifier" "^7.27.1" + js-tokens "^4.0.0" + picocolors "^1.1.1" + +"@babel/compat-data@^7.22.6", "@babel/compat-data@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/compat-data/-/compat-data-7.27.1.tgz#db7cf122745e0a332c44e847ddc4f5e5221a43f6" + integrity sha512-Q+E+rd/yBzNQhXkG+zQnF58e4zoZfBedaxwzPmicKsiK3nt8iJYrSrDbjwFFDGC4f+rPafqRaPH6TsDoSvMf7A== + +"@babel/core@^7.24.4": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/core/-/core-7.27.1.tgz#89de51e86bd12246003e3524704c49541b16c3e6" + integrity sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ== + dependencies: + "@ampproject/remapping" "^2.2.0" + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.1" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helpers" "^7.27.1" + "@babel/parser" "^7.27.1" + "@babel/template" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + convert-source-map "^2.0.0" + debug "^4.1.0" + gensync "^1.0.0-beta.2" + json5 "^2.2.3" + semver "^6.3.1" + +"@babel/generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/generator/-/generator-7.27.1.tgz#862d4fad858f7208edd487c28b58144036b76230" + integrity sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w== + dependencies: + "@babel/parser" "^7.27.1" + "@babel/types" "^7.27.1" + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + jsesc "^3.0.2" + +"@babel/helper-annotate-as-pure@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz#4345d81a9a46a6486e24d069469f13e60445c05d" + integrity sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-compilation-targets@^7.22.6", "@babel/helper-compilation-targets@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.1.tgz#eac1096c7374f161e4f33fc8ae38f4ddf122087a" + integrity sha512-2YaDd/Rd9E598B5+WIc8wJPmWETiiJXFYVE60oX8FDohv7rAUU3CQj+A1MgeEmcsk2+dQuEjIe/GDvig0SqL4g== + dependencies: + "@babel/compat-data" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + browserslist "^4.24.0" + lru-cache "^5.1.1" + semver "^6.3.1" + +"@babel/helper-create-class-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz#5bee4262a6ea5ddc852d0806199eb17ca3de9281" + integrity sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/traverse" "^7.27.1" + semver "^6.3.1" + +"@babel/helper-create-regexp-features-plugin@^7.18.6", "@babel/helper-create-regexp-features-plugin@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz#05b0882d97ba1d4d03519e4bce615d70afa18c53" + integrity sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + regexpu-core "^6.2.0" + semver "^6.3.1" + +"@babel/helper-define-polyfill-provider@^0.6.3", "@babel/helper-define-polyfill-provider@^0.6.4": + version "0.6.4" + resolved "https://registry.yarnpkg.com/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.4.tgz#15e8746368bfa671785f5926ff74b3064c291fab" + integrity sha512-jljfR1rGnXXNWnmQg2K3+bvhkxB51Rl32QRaOTuwwjviGrHzIbSc8+x9CpraDtbT7mfyjXObULP4w/adunNwAw== + dependencies: + "@babel/helper-compilation-targets" "^7.22.6" + "@babel/helper-plugin-utils" "^7.22.5" + debug "^4.1.1" + lodash.debounce "^4.0.8" + resolve "^1.14.2" + +"@babel/helper-member-expression-to-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz#ea1211276be93e798ce19037da6f06fbb994fa44" + integrity sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-imports@^7.10.4", "@babel/helper-module-imports@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz#7ef769a323e2655e126673bb6d2d6913bbead204" + integrity sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-module-transforms@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-module-transforms/-/helper-module-transforms-7.27.1.tgz#e1663b8b71d2de948da5c4fb2a20ca4f3ec27a6f" + integrity sha512-9yHn519/8KvTU5BjTVEEeIM3w9/2yXNKoD82JifINImhpKkARMJKPP59kLo+BafpdN5zgNeIcS4jsGDmd3l58g== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-optimise-call-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz#c65221b61a643f3e62705e5dd2b5f115e35f9200" + integrity sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/helper-plugin-utils@^7.0.0", "@babel/helper-plugin-utils@^7.18.6", "@babel/helper-plugin-utils@^7.22.5", "@babel/helper-plugin-utils@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz#ddb2f876534ff8013e6c2b299bf4d39b3c51d44c" + integrity sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw== + +"@babel/helper-remap-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz#4601d5c7ce2eb2aea58328d43725523fcd362ce6" + integrity sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-wrap-function" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-replace-supers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz#b1ed2d634ce3bdb730e4b52de30f8cccfd692bc0" + integrity sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA== + dependencies: + "@babel/helper-member-expression-to-functions" "^7.27.1" + "@babel/helper-optimise-call-expression" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/helper-skip-transparent-expression-wrappers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz#62bb91b3abba8c7f1fec0252d9dbea11b3ee7a56" + integrity sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg== + dependencies: + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helper-string-parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz#54da796097ab19ce67ed9f88b47bb2ec49367687" + integrity sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA== + +"@babel/helper-validator-identifier@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz#a7054dcc145a967dd4dc8fee845a57c1316c9df8" + integrity sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow== + +"@babel/helper-validator-option@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz#fa52f5b1e7db1ab049445b421c4471303897702f" + integrity sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg== + +"@babel/helper-wrap-function@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz#b88285009c31427af318d4fe37651cd62a142409" + integrity sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ== + dependencies: + "@babel/template" "^7.27.1" + "@babel/traverse" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/helpers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/helpers/-/helpers-7.27.1.tgz#ffc27013038607cdba3288e692c3611c06a18aa4" + integrity sha512-FCvFTm0sWV8Fxhpp2McP5/W53GPllQ9QeQ7SiqGWjMf/LVG07lFa5+pgK05IRhVwtvafT22KF+ZSnM9I545CvQ== + dependencies: + "@babel/template" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/parser@^7.25.3", "@babel/parser@^7.27.0", "@babel/parser@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.27.1.tgz#c55d5bed74449d1223701f1869b9ee345cc94cc9" + integrity sha512-I0dZ3ZpCrJ1c04OqlNsQcKiZlsrXf/kkE4FXzID9rIOYICsAbA8mMDzhW/luRNAHdCNt7os/u8wenklZDlUVUQ== + dependencies: + "@babel/types" "^7.27.1" + +"@babel/plugin-bugfix-firefox-class-in-computed-class-key@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz#61dd8a8e61f7eb568268d1b5f129da3eee364bf9" + integrity sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-bugfix-safari-class-field-initializer-scope@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz#43f70a6d7efd52370eefbdf55ae03d91b293856d" + integrity sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz#beb623bd573b8b6f3047bd04c32506adc3e58a72" + integrity sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz#e134a5479eb2ba9c02714e8c1ebf1ec9076124fd" + integrity sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + +"@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz#bb1c25af34d75115ce229a1de7fa44bf8f955670" + integrity sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-proposal-private-property-in-object@7.21.0-placeholder-for-preset-env.2": + version "7.21.0-placeholder-for-preset-env.2" + resolved "https://registry.yarnpkg.com/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz#7844f9289546efa9febac2de4cfe358a050bd703" + integrity sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w== + +"@babel/plugin-syntax-import-assertions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz#88894aefd2b03b5ee6ad1562a7c8e1587496aecd" + integrity sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-import-attributes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz#34c017d54496f9b11b61474e7ea3dfd5563ffe07" + integrity sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-syntax-unicode-sets-regex@^7.18.6": + version "7.18.6" + resolved "https://registry.yarnpkg.com/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz#d49a3b3e6b52e5be6740022317580234a6a47357" + integrity sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.18.6" + "@babel/helper-plugin-utils" "^7.18.6" + +"@babel/plugin-transform-arrow-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz#6e2061067ba3ab0266d834a9f94811196f2aba9a" + integrity sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-async-generator-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz#ca433df983d68e1375398e7ca71bf2a4f6fd89d7" + integrity sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-transform-async-to-generator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz#9a93893b9379b39466c74474f55af03de78c66e7" + integrity sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA== + dependencies: + "@babel/helper-module-imports" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-remap-async-to-generator" "^7.27.1" + +"@babel/plugin-transform-block-scoped-functions@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz#558a9d6e24cf72802dd3b62a4b51e0d62c0f57f9" + integrity sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-block-scoping@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.1.tgz#bc0dbe8ac6de5602981ba58ef68c6df8ef9bfbb3" + integrity sha512-QEcFlMl9nGTgh1rn2nIeU5bkfb9BAjaQcWbiP4LvKxUot52ABcTkpcyJ7f2Q2U2RuQ84BNLgts3jRme2dTx6Fw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-class-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz#dd40a6a370dfd49d32362ae206ddaf2bb082a925" + integrity sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-class-static-block@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz#7e920d5625b25bbccd3061aefbcc05805ed56ce4" + integrity sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-classes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz#03bb04bea2c7b2f711f0db7304a8da46a85cced4" + integrity sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + "@babel/traverse" "^7.27.1" + globals "^11.1.0" + +"@babel/plugin-transform-computed-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz#81662e78bf5e734a97982c2b7f0a793288ef3caa" + integrity sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/template" "^7.27.1" + +"@babel/plugin-transform-destructuring@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.1.tgz#d5916ef7089cb254df0418ae524533c1b72ba656" + integrity sha512-ttDCqhfvpE9emVkXbPD8vyxxh4TWYACVybGkDj+oReOGwnp066ITEivDlLwe0b1R0+evJ13IXQuLNB5w1fhC5Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-dotall-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz#aa6821de864c528b1fecf286f0a174e38e826f4d" + integrity sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-duplicate-keys@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz#f1fbf628ece18e12e7b32b175940e68358f546d1" + integrity sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-duplicate-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz#5043854ca620a94149372e69030ff8cb6a9eb0ec" + integrity sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-dynamic-import@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz#4c78f35552ac0e06aa1f6e3c573d67695e8af5a4" + integrity sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-exponentiation-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz#fc497b12d8277e559747f5a3ed868dd8064f83e1" + integrity sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-export-namespace-from@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz#71ca69d3471edd6daa711cf4dfc3400415df9c23" + integrity sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-for-of@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz#bc24f7080e9ff721b63a70ac7b2564ca15b6c40a" + integrity sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-function-name@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz#4d0bf307720e4dce6d7c30fcb1fd6ca77bdeb3a7" + integrity sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-transform-json-strings@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz#a2e0ce6ef256376bd527f290da023983527a4f4c" + integrity sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz#baaefa4d10a1d4206f9dcdda50d7d5827bb70b24" + integrity sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-logical-assignment-operators@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz#890cb20e0270e0e5bebe3f025b434841c32d5baa" + integrity sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-member-expression-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz#37b88ba594d852418e99536f5612f795f23aeaf9" + integrity sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-amd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz#a4145f9d87c2291fe2d05f994b65dba4e3e7196f" + integrity sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-commonjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz#8e44ed37c2787ecc23bdc367f49977476614e832" + integrity sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-modules-systemjs@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz#00e05b61863070d0f3292a00126c16c0e024c4ed" + integrity sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + "@babel/traverse" "^7.27.1" + +"@babel/plugin-transform-modules-umd@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz#63f2cf4f6dc15debc12f694e44714863d34cd334" + integrity sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w== + dependencies: + "@babel/helper-module-transforms" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-named-capturing-groups-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz#f32b8f7818d8fc0cc46ee20a8ef75f071af976e1" + integrity sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-new-target@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz#259c43939728cad1706ac17351b7e6a7bea1abeb" + integrity sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-nullish-coalescing-operator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz#4f9d3153bf6782d73dd42785a9d22d03197bc91d" + integrity sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-numeric-separator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz#614e0b15cc800e5997dadd9bd6ea524ed6c819c6" + integrity sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-object-rest-spread@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.1.tgz#845bdcd74c87b8f565c25cc6812f7f4f43c9ed79" + integrity sha512-/sSliVc9gHE20/7D5qsdGlq7RG5NCDTWsAhyqzGuq174EtWJoGzIu1BQ7G56eDsTcy1jseBZwv50olSdXOlGuA== + dependencies: + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/plugin-transform-parameters" "^7.27.1" + +"@babel/plugin-transform-object-super@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz#1c932cd27bf3874c43a5cac4f43ebf970c9871b5" + integrity sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-replace-supers" "^7.27.1" + +"@babel/plugin-transform-optional-catch-binding@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz#84c7341ebde35ccd36b137e9e45866825072a30c" + integrity sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-optional-chaining@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz#874ce3c4f06b7780592e946026eb76a32830454f" + integrity sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-parameters@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz#80334b54b9b1ac5244155a0c8304a187a618d5a7" + integrity sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-private-methods@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz#fdacbab1c5ed81ec70dfdbb8b213d65da148b6af" + integrity sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA== + dependencies: + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-private-property-in-object@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz#4dbbef283b5b2f01a21e81e299f76e35f900fb11" + integrity sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ== + dependencies: + "@babel/helper-annotate-as-pure" "^7.27.1" + "@babel/helper-create-class-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-property-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz#07eafd618800591e88073a0af1b940d9a42c6424" + integrity sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-regenerator@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.1.tgz#0a471df9213416e44cd66bf67176b66f65768401" + integrity sha512-B19lbbL7PMrKr52BNPjCqg1IyNUIjTcxKj8uX9zHO+PmWN93s19NDr/f69mIkEp2x9nmDJ08a7lgHaTTzvW7mw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-regexp-modifiers@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz#df9ba5577c974e3f1449888b70b76169998a6d09" + integrity sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-reserved-words@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz#40fba4878ccbd1c56605a4479a3a891ac0274bb4" + integrity sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-shorthand-properties@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz#532abdacdec87bfee1e0ef8e2fcdee543fe32b90" + integrity sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-spread@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz#1a264d5fc12750918f50e3fe3e24e437178abb08" + integrity sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-skip-transparent-expression-wrappers" "^7.27.1" + +"@babel/plugin-transform-sticky-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz#18984935d9d2296843a491d78a014939f7dcd280" + integrity sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-template-literals@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz#1a0eb35d8bb3e6efc06c9fd40eb0bcef548328b8" + integrity sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-typeof-symbol@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz#70e966bb492e03509cf37eafa6dcc3051f844369" + integrity sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-escapes@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz#3e3143f8438aef842de28816ece58780190cf806" + integrity sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg== + dependencies: + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-property-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz#bdfe2d3170c78c5691a3c3be934c8c0087525956" + integrity sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz#25948f5c395db15f609028e370667ed8bae9af97" + integrity sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/plugin-transform-unicode-sets-regex@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz#6ab706d10f801b5c72da8bb2548561fa04193cd1" + integrity sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw== + dependencies: + "@babel/helper-create-regexp-features-plugin" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + +"@babel/preset-env@^7.11.0": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/preset-env/-/preset-env-7.27.1.tgz#23463ab94f36540630924f5de3b4c7a8dde3b6a2" + integrity sha512-TZ5USxFpLgKDpdEt8YWBR7p6g+bZo6sHaXLqP2BY/U0acaoI8FTVflcYCr/v94twM1C5IWFdZ/hscq9WjUeLXA== + dependencies: + "@babel/compat-data" "^7.27.1" + "@babel/helper-compilation-targets" "^7.27.1" + "@babel/helper-plugin-utils" "^7.27.1" + "@babel/helper-validator-option" "^7.27.1" + "@babel/plugin-bugfix-firefox-class-in-computed-class-key" "^7.27.1" + "@babel/plugin-bugfix-safari-class-field-initializer-scope" "^7.27.1" + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression" "^7.27.1" + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining" "^7.27.1" + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly" "^7.27.1" + "@babel/plugin-proposal-private-property-in-object" "7.21.0-placeholder-for-preset-env.2" + "@babel/plugin-syntax-import-assertions" "^7.27.1" + "@babel/plugin-syntax-import-attributes" "^7.27.1" + "@babel/plugin-syntax-unicode-sets-regex" "^7.18.6" + "@babel/plugin-transform-arrow-functions" "^7.27.1" + "@babel/plugin-transform-async-generator-functions" "^7.27.1" + "@babel/plugin-transform-async-to-generator" "^7.27.1" + "@babel/plugin-transform-block-scoped-functions" "^7.27.1" + "@babel/plugin-transform-block-scoping" "^7.27.1" + "@babel/plugin-transform-class-properties" "^7.27.1" + "@babel/plugin-transform-class-static-block" "^7.27.1" + "@babel/plugin-transform-classes" "^7.27.1" + "@babel/plugin-transform-computed-properties" "^7.27.1" + "@babel/plugin-transform-destructuring" "^7.27.1" + "@babel/plugin-transform-dotall-regex" "^7.27.1" + "@babel/plugin-transform-duplicate-keys" "^7.27.1" + "@babel/plugin-transform-duplicate-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-dynamic-import" "^7.27.1" + "@babel/plugin-transform-exponentiation-operator" "^7.27.1" + "@babel/plugin-transform-export-namespace-from" "^7.27.1" + "@babel/plugin-transform-for-of" "^7.27.1" + "@babel/plugin-transform-function-name" "^7.27.1" + "@babel/plugin-transform-json-strings" "^7.27.1" + "@babel/plugin-transform-literals" "^7.27.1" + "@babel/plugin-transform-logical-assignment-operators" "^7.27.1" + "@babel/plugin-transform-member-expression-literals" "^7.27.1" + "@babel/plugin-transform-modules-amd" "^7.27.1" + "@babel/plugin-transform-modules-commonjs" "^7.27.1" + "@babel/plugin-transform-modules-systemjs" "^7.27.1" + "@babel/plugin-transform-modules-umd" "^7.27.1" + "@babel/plugin-transform-named-capturing-groups-regex" "^7.27.1" + "@babel/plugin-transform-new-target" "^7.27.1" + "@babel/plugin-transform-nullish-coalescing-operator" "^7.27.1" + "@babel/plugin-transform-numeric-separator" "^7.27.1" + "@babel/plugin-transform-object-rest-spread" "^7.27.1" + "@babel/plugin-transform-object-super" "^7.27.1" + "@babel/plugin-transform-optional-catch-binding" "^7.27.1" + "@babel/plugin-transform-optional-chaining" "^7.27.1" + "@babel/plugin-transform-parameters" "^7.27.1" + "@babel/plugin-transform-private-methods" "^7.27.1" + "@babel/plugin-transform-private-property-in-object" "^7.27.1" + "@babel/plugin-transform-property-literals" "^7.27.1" + "@babel/plugin-transform-regenerator" "^7.27.1" + "@babel/plugin-transform-regexp-modifiers" "^7.27.1" + "@babel/plugin-transform-reserved-words" "^7.27.1" + "@babel/plugin-transform-shorthand-properties" "^7.27.1" + "@babel/plugin-transform-spread" "^7.27.1" + "@babel/plugin-transform-sticky-regex" "^7.27.1" + "@babel/plugin-transform-template-literals" "^7.27.1" + "@babel/plugin-transform-typeof-symbol" "^7.27.1" + "@babel/plugin-transform-unicode-escapes" "^7.27.1" + "@babel/plugin-transform-unicode-property-regex" "^7.27.1" + "@babel/plugin-transform-unicode-regex" "^7.27.1" + "@babel/plugin-transform-unicode-sets-regex" "^7.27.1" + "@babel/preset-modules" "0.1.6-no-external-plugins" + babel-plugin-polyfill-corejs2 "^0.4.10" + babel-plugin-polyfill-corejs3 "^0.11.0" + babel-plugin-polyfill-regenerator "^0.6.1" + core-js-compat "^3.40.0" + semver "^6.3.1" + +"@babel/preset-modules@0.1.6-no-external-plugins": + version "0.1.6-no-external-plugins" + resolved "https://registry.yarnpkg.com/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz#ccb88a2c49c817236861fee7826080573b8a923a" + integrity sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA== + dependencies: + "@babel/helper-plugin-utils" "^7.0.0" + "@babel/types" "^7.4.4" + esutils "^2.0.2" + +"@babel/runtime@^7.11.2", "@babel/runtime@^7.23.8", "@babel/runtime@^7.24.5": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/runtime/-/runtime-7.27.1.tgz#9fce313d12c9a77507f264de74626e87fd0dc541" + integrity sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog== + +"@babel/template@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/template/-/template-7.27.1.tgz#b9e4f55c17a92312774dfbdde1b3c01c547bbae2" + integrity sha512-Fyo3ghWMqkHHpHQCoBs2VnYjR4iWFFjguTDEqA5WgZDOrFesVjMhMM2FSqTKSoUSDO1VQtavj8NFpdRBEvJTtg== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/parser" "^7.27.1" + "@babel/types" "^7.27.1" + +"@babel/traverse@^7.27.1": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/traverse/-/traverse-7.27.1.tgz#4db772902b133bbddd1c4f7a7ee47761c1b9f291" + integrity sha512-ZCYtZciz1IWJB4U61UPu4KEaqyfj+r5T1Q5mqPo+IBpcG9kHv30Z0aD8LXPgC1trYa6rK0orRyAhqUgk4MjmEg== + dependencies: + "@babel/code-frame" "^7.27.1" + "@babel/generator" "^7.27.1" + "@babel/parser" "^7.27.1" + "@babel/template" "^7.27.1" + "@babel/types" "^7.27.1" + debug "^4.3.1" + globals "^11.1.0" + +"@babel/types@^7.26.8", "@babel/types@^7.27.1", "@babel/types@^7.4.4": + version "7.27.1" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.27.1.tgz#9defc53c16fc899e46941fc6901a9eea1c9d8560" + integrity sha512-+EzkxvLNfiUeKMgy/3luqfsCWFRXLb7U6wNQTk60tovuckwB15B191tJWvpp4HjiQWdJkCxO3Wbvc6jlk3Xb2Q== + dependencies: + "@babel/helper-string-parser" "^7.27.1" + "@babel/helper-validator-identifier" "^7.27.1" + +"@esbuild/aix-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.21.5.tgz#c7184a326533fcdf1b8ee0733e21c713b975575f" + integrity sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ== + +"@esbuild/android-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.21.5.tgz#09d9b4357780da9ea3a7dfb833a1f1ff439b4052" + integrity sha512-c0uX9VAUBQ7dTDCjq+wdyGLowMdtR/GoC2U5IYk/7D1H1JYC0qseD7+11iMP2mRLN9RcCMRcjC4YMclCzGwS/A== + +"@esbuild/android-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.21.5.tgz#9b04384fb771926dfa6d7ad04324ecb2ab9b2e28" + integrity sha512-vCPvzSjpPHEi1siZdlvAlsPxXl7WbOVUBBAowWug4rJHb68Ox8KualB+1ocNvT5fjv6wpkX6o/iEpbDrf68zcg== + +"@esbuild/android-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.21.5.tgz#29918ec2db754cedcb6c1b04de8cd6547af6461e" + integrity sha512-D7aPRUUNHRBwHxzxRvp856rjUHRFW1SdQATKXH2hqA0kAZb1hKmi02OpYRacl0TxIGz/ZmXWlbZgjwWYaCakTA== + +"@esbuild/darwin-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.21.5.tgz#e495b539660e51690f3928af50a76fb0a6ccff2a" + integrity sha512-DwqXqZyuk5AiWWf3UfLiRDJ5EDd49zg6O9wclZ7kUMv2WRFr4HKjXp/5t8JZ11QbQfUS6/cRCKGwYhtNAY88kQ== + +"@esbuild/darwin-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.21.5.tgz#c13838fa57372839abdddc91d71542ceea2e1e22" + integrity sha512-se/JjF8NlmKVG4kNIuyWMV/22ZaerB+qaSi5MdrXtd6R08kvs2qCN4C09miupktDitvh8jRFflwGFBQcxZRjbw== + +"@esbuild/freebsd-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.21.5.tgz#646b989aa20bf89fd071dd5dbfad69a3542e550e" + integrity sha512-5JcRxxRDUJLX8JXp/wcBCy3pENnCgBR9bN6JsY4OmhfUtIHe3ZW0mawA7+RDAcMLrMIZaf03NlQiX9DGyB8h4g== + +"@esbuild/freebsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.21.5.tgz#aa615cfc80af954d3458906e38ca22c18cf5c261" + integrity sha512-J95kNBj1zkbMXtHVH29bBriQygMXqoVQOQYA+ISs0/2l3T9/kj42ow2mpqerRBxDJnmkUDCaQT/dfNXWX/ZZCQ== + +"@esbuild/linux-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.21.5.tgz#70ac6fa14f5cb7e1f7f887bcffb680ad09922b5b" + integrity sha512-ibKvmyYzKsBeX8d8I7MH/TMfWDXBF3db4qM6sy+7re0YXya+K1cem3on9XgdT2EQGMu4hQyZhan7TeQ8XkGp4Q== + +"@esbuild/linux-arm@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.21.5.tgz#fc6fd11a8aca56c1f6f3894f2bea0479f8f626b9" + integrity sha512-bPb5AHZtbeNGjCKVZ9UGqGwo8EUu4cLq68E95A53KlxAPRmUyYv2D6F0uUI65XisGOL1hBP5mTronbgo+0bFcA== + +"@esbuild/linux-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.21.5.tgz#3271f53b3f93e3d093d518d1649d6d68d346ede2" + integrity sha512-YvjXDqLRqPDl2dvRODYmmhz4rPeVKYvppfGYKSNGdyZkA01046pLWyRKKI3ax8fbJoK5QbxblURkwK/MWY18Tg== + +"@esbuild/linux-loong64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.21.5.tgz#ed62e04238c57026aea831c5a130b73c0f9f26df" + integrity sha512-uHf1BmMG8qEvzdrzAqg2SIG/02+4/DHB6a9Kbya0XDvwDEKCoC8ZRWI5JJvNdUjtciBGFQ5PuBlpEOXQj+JQSg== + +"@esbuild/linux-mips64el@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.21.5.tgz#e79b8eb48bf3b106fadec1ac8240fb97b4e64cbe" + integrity sha512-IajOmO+KJK23bj52dFSNCMsz1QP1DqM6cwLUv3W1QwyxkyIWecfafnI555fvSGqEKwjMXVLokcV5ygHW5b3Jbg== + +"@esbuild/linux-ppc64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.21.5.tgz#5f2203860a143b9919d383ef7573521fb154c3e4" + integrity sha512-1hHV/Z4OEfMwpLO8rp7CvlhBDnjsC3CttJXIhBi+5Aj5r+MBvy4egg7wCbe//hSsT+RvDAG7s81tAvpL2XAE4w== + +"@esbuild/linux-riscv64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.21.5.tgz#07bcafd99322d5af62f618cb9e6a9b7f4bb825dc" + integrity sha512-2HdXDMd9GMgTGrPWnJzP2ALSokE/0O5HhTUvWIbD3YdjME8JwvSCnNGBnTThKGEB91OZhzrJ4qIIxk/SBmyDDA== + +"@esbuild/linux-s390x@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.21.5.tgz#b7ccf686751d6a3e44b8627ababc8be3ef62d8de" + integrity sha512-zus5sxzqBJD3eXxwvjN1yQkRepANgxE9lgOW2qLnmr8ikMTphkjgXu1HR01K4FJg8h1kEEDAqDcZQtbrRnB41A== + +"@esbuild/linux-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.21.5.tgz#6d8f0c768e070e64309af8004bb94e68ab2bb3b0" + integrity sha512-1rYdTpyv03iycF1+BhzrzQJCdOuAOtaqHTWJZCWvijKD2N5Xu0TtVC8/+1faWqcP9iBCWOmjmhoH94dH82BxPQ== + +"@esbuild/netbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.21.5.tgz#bbe430f60d378ecb88decb219c602667387a6047" + integrity sha512-Woi2MXzXjMULccIwMnLciyZH4nCIMpWQAs049KEeMvOcNADVxo0UBIQPfSmxB3CWKedngg7sWZdLvLczpe0tLg== + +"@esbuild/openbsd-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.21.5.tgz#99d1cf2937279560d2104821f5ccce220cb2af70" + integrity sha512-HLNNw99xsvx12lFBUwoT8EVCsSvRNDVxNpjZ7bPn947b8gJPzeHWyNVhFsaerc0n3TsbOINvRP2byTZ5LKezow== + +"@esbuild/sunos-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.21.5.tgz#08741512c10d529566baba837b4fe052c8f3487b" + integrity sha512-6+gjmFpfy0BHU5Tpptkuh8+uw3mnrvgs+dSPQXQOv3ekbordwnzTVEb4qnIvQcYXq6gzkyTnoZ9dZG+D4garKg== + +"@esbuild/win32-arm64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.21.5.tgz#675b7385398411240735016144ab2e99a60fc75d" + integrity sha512-Z0gOTd75VvXqyq7nsl93zwahcTROgqvuAcYDUr+vOv8uHhNSKROyU961kgtCD1e95IqPKSQKH7tBTslnS3tA8A== + +"@esbuild/win32-ia32@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.21.5.tgz#1bfc3ce98aa6ca9a0969e4d2af72144c59c1193b" + integrity sha512-SWXFF1CL2RVNMaVs+BBClwtfZSvDgtL//G/smwAc5oVK/UPu2Gu9tIaRgFmYFFKrmg3SyAjSrElf0TiJ1v8fYA== + +"@esbuild/win32-x64@0.21.5": + version "0.21.5" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.21.5.tgz#acad351d582d157bb145535db2a6ff53dd514b5c" + integrity sha512-tQd/1efJuzPC6rCFwEvLtci/xNFcTZknmXs98FYDfGE4wP9ClFV98nyKrzJKVPMhdDnjzLhdUyMX4PsQAPjwIw== + +"@isaacs/cliui@^8.0.2": + version "8.0.2" + resolved "https://registry.yarnpkg.com/@isaacs/cliui/-/cliui-8.0.2.tgz#b37667b7bc181c168782259bab42474fbf52b550" + integrity sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA== + dependencies: + string-width "^5.1.2" + string-width-cjs "npm:string-width@^4.2.0" + strip-ansi "^7.0.1" + strip-ansi-cjs "npm:strip-ansi@^6.0.1" + wrap-ansi "^8.1.0" + wrap-ansi-cjs "npm:wrap-ansi@^7.0.0" + +"@jridgewell/gen-mapping@^0.3.5": + version "0.3.8" + resolved "https://registry.yarnpkg.com/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz#4f0e06362e01362f823d348f1872b08f666d8142" + integrity sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA== + dependencies: + "@jridgewell/set-array" "^1.2.1" + "@jridgewell/sourcemap-codec" "^1.4.10" + "@jridgewell/trace-mapping" "^0.3.24" + +"@jridgewell/resolve-uri@^3.1.0": + version "3.1.2" + resolved "https://registry.yarnpkg.com/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz#7a0ee601f60f99a20c7c7c5ff0c80388c1189bd6" + integrity sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw== + +"@jridgewell/set-array@^1.2.1": + version "1.2.1" + resolved "https://registry.yarnpkg.com/@jridgewell/set-array/-/set-array-1.2.1.tgz#558fb6472ed16a4c850b889530e6b36438c49280" + integrity sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A== + +"@jridgewell/source-map@^0.3.3": + version "0.3.6" + resolved "https://registry.yarnpkg.com/@jridgewell/source-map/-/source-map-0.3.6.tgz#9d71ca886e32502eb9362c9a74a46787c36df81a" + integrity sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ== + dependencies: + "@jridgewell/gen-mapping" "^0.3.5" + "@jridgewell/trace-mapping" "^0.3.25" + +"@jridgewell/sourcemap-codec@^1.4.10", "@jridgewell/sourcemap-codec@^1.4.14", "@jridgewell/sourcemap-codec@^1.5.0": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@jridgewell/trace-mapping@^0.3.24", "@jridgewell/trace-mapping@^0.3.25": + version "0.3.25" + resolved "https://registry.yarnpkg.com/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz#15f190e98895f3fc23276ee14bc76b675c2e50f0" + integrity sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ== + dependencies: + "@jridgewell/resolve-uri" "^3.1.0" + "@jridgewell/sourcemap-codec" "^1.4.14" + +"@nodelib/fs.scandir@2.1.5": + version "2.1.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz#7619c2eb21b25483f6d167548b4cfd5a7488c3d5" + integrity sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g== + dependencies: + "@nodelib/fs.stat" "2.0.5" + run-parallel "^1.1.9" + +"@nodelib/fs.stat@2.0.5", "@nodelib/fs.stat@^2.0.2": + version "2.0.5" + resolved "https://registry.yarnpkg.com/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz#5bd262af94e9d25bd1e71b05deed44876a222e8b" + integrity sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A== + +"@nodelib/fs.walk@^1.2.3": + version "1.2.8" + resolved "https://registry.yarnpkg.com/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz#e95737e8bb6746ddedf69c556953494f196fe69a" + integrity sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg== + dependencies: + "@nodelib/fs.scandir" "2.1.5" + fastq "^1.6.0" + +"@rollup/plugin-babel@^5.2.0": + version "5.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz#04bc0608f4aa4b2e4b1aebf284344d0f68fda283" + integrity sha512-WFfdLWU/xVWKeRQnKmIAQULUI7Il0gZnBIH/ZFO069wYIfPu+8zrfp/KMW0atmELoRDq8FbiP3VCss9MhCut7Q== + dependencies: + "@babel/helper-module-imports" "^7.10.4" + "@rollup/pluginutils" "^3.1.0" + +"@rollup/plugin-node-resolve@^15.2.3": + version "15.3.1" + resolved "https://registry.yarnpkg.com/@rollup/plugin-node-resolve/-/plugin-node-resolve-15.3.1.tgz#66008953c2524be786aa319d49e32f2128296a78" + integrity sha512-tgg6b91pAybXHJQMAAwW9VuWBO6Thi+q7BCNARLwSqlmsHz0XYURtGvh/AuwSADXSI4h/2uHbs7s4FzlZDGSGA== + dependencies: + "@rollup/pluginutils" "^5.0.1" + "@types/resolve" "1.20.2" + deepmerge "^4.2.2" + is-module "^1.0.0" + resolve "^1.22.1" + +"@rollup/plugin-replace@^2.4.1": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@rollup/plugin-replace/-/plugin-replace-2.4.2.tgz#a2d539314fbc77c244858faa523012825068510a" + integrity sha512-IGcu+cydlUMZ5En85jxHH4qj2hta/11BHq95iHEyb2sbgiN0eCdzvUcHw5gt9pBL5lTi4JDYJ1acCoMGpTvEZg== + dependencies: + "@rollup/pluginutils" "^3.1.0" + magic-string "^0.25.7" + +"@rollup/plugin-terser@^0.4.3": + version "0.4.4" + resolved "https://registry.yarnpkg.com/@rollup/plugin-terser/-/plugin-terser-0.4.4.tgz#15dffdb3f73f121aa4fbb37e7ca6be9aeea91962" + integrity sha512-XHeJC5Bgvs8LfukDwWZp7yeqin6ns8RTl2B9avbejt6tZqsqvVoWI7ZTQrcNsfKEDWBTnTxM8nMDkO2IFFbd0A== + dependencies: + serialize-javascript "^6.0.1" + smob "^1.0.0" + terser "^5.17.4" + +"@rollup/pluginutils@^3.1.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-3.1.0.tgz#706b4524ee6dc8b103b3c995533e5ad680c02b9b" + integrity sha512-GksZ6pr6TpIjHm8h9lSQ8pi8BE9VeubNT0OMJ3B5uZJ8pz73NPiqOtCog/x2/QzM1ENChPKxMDhiQuRHsqc+lg== + dependencies: + "@types/estree" "0.0.39" + estree-walker "^1.0.1" + picomatch "^2.2.2" + +"@rollup/pluginutils@^5.0.1": + version "5.1.4" + resolved "https://registry.yarnpkg.com/@rollup/pluginutils/-/pluginutils-5.1.4.tgz#bb94f1f9eaaac944da237767cdfee6c5b2262d4a" + integrity sha512-USm05zrsFxYLPdWWq+K3STlWiT/3ELn3RcV5hJMghpeAIhxfsUIg6mt12CBJBInWMV4VneoV7SfGv8xIwo2qNQ== + dependencies: + "@types/estree" "^1.0.0" + estree-walker "^2.0.2" + picomatch "^4.0.2" + +"@rollup/rollup-android-arm-eabi@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.1.tgz#e1562d360bca73c7bef6feef86098de3a2f1d442" + integrity sha512-kxz0YeeCrRUHz3zyqvd7n+TVRlNyTifBsmnmNPtk3hQURUyG9eAB+usz6DAwagMusjx/zb3AjvDUvhFGDAexGw== + +"@rollup/rollup-android-arm64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.1.tgz#37ba63940211673e15dcc5f469a78e34276dbca7" + integrity sha512-PPkxTOisoNC6TpnDKatjKkjRMsdaWIhyuMkA4UsBXT9WEZY4uHezBTjs6Vl4PbqQQeu6oION1w2voYZv9yquCw== + +"@rollup/rollup-darwin-arm64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.1.tgz#58b1eb86d997d71dabc5b78903233a3c27438ca0" + integrity sha512-VWXGISWFY18v/0JyNUy4A46KCFCb9NVsH+1100XP31lud+TzlezBbz24CYzbnA4x6w4hx+NYCXDfnvDVO6lcAA== + +"@rollup/rollup-darwin-x64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.1.tgz#5e22dab3232b1e575d930ce891abb18fe19c58c9" + integrity sha512-nIwkXafAI1/QCS7pxSpv/ZtFW6TXcNUEHAIA9EIyw5OzxJZQ1YDrX+CL6JAIQgZ33CInl1R6mHet9Y/UZTg2Bw== + +"@rollup/rollup-freebsd-arm64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.1.tgz#04c892d9ff864d66e31419634726ab0bebb33707" + integrity sha512-BdrLJ2mHTrIYdaS2I99mriyJfGGenSaP+UwGi1kB9BLOCu9SR8ZpbkmmalKIALnRw24kM7qCN0IOm6L0S44iWw== + +"@rollup/rollup-freebsd-x64@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.1.tgz#f4b1e091f7cf5afc9e3a029d70128ad56409ecfb" + integrity sha512-VXeo/puqvCG8JBPNZXZf5Dqq7BzElNJzHRRw3vjBE27WujdzuOPecDPc/+1DcdcTptNBep3861jNq0mYkT8Z6Q== + +"@rollup/rollup-linux-arm-gnueabihf@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.1.tgz#c8814bb5ce047a81b1fe4a33628dfd4ac52bd864" + integrity sha512-ehSKrewwsESPt1TgSE/na9nIhWCosfGSFqv7vwEtjyAqZcvbGIg4JAcV7ZEh2tfj/IlfBeZjgOXm35iOOjadcg== + +"@rollup/rollup-linux-arm-musleabihf@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.1.tgz#5b4e7bd83cbebbf5ffe958802dcfd4ee34bf73a3" + integrity sha512-m39iO/aaurh5FVIu/F4/Zsl8xppd76S4qoID8E+dSRQvTyZTOI2gVk3T4oqzfq1PtcvOfAVlwLMK3KRQMaR8lg== + +"@rollup/rollup-linux-arm64-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.1.tgz#141c848e53cee011e82a11777b8a51f1b3e8d77c" + integrity sha512-Y+GHnGaku4aVLSgrT0uWe2o2Rq8te9hi+MwqGF9r9ORgXhmHK5Q71N757u0F8yU1OIwUIFy6YiJtKjtyktk5hg== + +"@rollup/rollup-linux-arm64-musl@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.1.tgz#22ebeaf2fa301aa4aa6c84b760e6cd1d1ac7eb1e" + integrity sha512-jEwjn3jCA+tQGswK3aEWcD09/7M5wGwc6+flhva7dsQNRZZTe30vkalgIzV4tjkopsTS9Jd7Y1Bsj6a4lzz8gQ== + +"@rollup/rollup-linux-loongarch64-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.1.tgz#20b77dc78e622f5814ff8e90c14c938ceb8043bc" + integrity sha512-ySyWikVhNzv+BV/IDCsrraOAZ3UaC8SZB67FZlqVwXwnFhPihOso9rPOxzZbjp81suB1O2Topw+6Ug3JNegejQ== + +"@rollup/rollup-linux-powerpc64le-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.1.tgz#2c90f99c987ef1198d4f8d15d754c286e1f07b13" + integrity sha512-BvvA64QxZlh7WZWqDPPdt0GH4bznuL6uOO1pmgPnnv86rpUpc8ZxgZwcEgXvo02GRIZX1hQ0j0pAnhwkhwPqWg== + +"@rollup/rollup-linux-riscv64-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.1.tgz#9336fd5e47d7f4760d02aa85f76976176eef53ca" + integrity sha512-EQSP+8+1VuSulm9RKSMKitTav89fKbHymTf25n5+Yr6gAPZxYWpj3DzAsQqoaHAk9YX2lwEyAf9S4W8F4l3VBQ== + +"@rollup/rollup-linux-riscv64-musl@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.1.tgz#d75b4d54d46439bb5c6c13762788f57e798f5670" + integrity sha512-n/vQ4xRZXKuIpqukkMXZt9RWdl+2zgGNx7Uda8NtmLJ06NL8jiHxUawbwC+hdSq1rrw/9CghCpEONor+l1e2gA== + +"@rollup/rollup-linux-s390x-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.1.tgz#e9f09b802f1291839247399028beaef9ce034c81" + integrity sha512-h8d28xzYb98fMQKUz0w2fMc1XuGzLLjdyxVIbhbil4ELfk5/orZlSTpF/xdI9C8K0I8lCkq+1En2RJsawZekkg== + +"@rollup/rollup-linux-x64-gnu@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.1.tgz#0413169dc00470667dea8575c1129d4e7a73eb29" + integrity sha512-XiK5z70PEFEFqcNj3/zRSz/qX4bp4QIraTy9QjwJAb/Z8GM7kVUsD0Uk8maIPeTyPCP03ChdI+VVmJriKYbRHQ== + +"@rollup/rollup-linux-x64-musl@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.1.tgz#c76fd593323c60ea219439a00da6c6d33ffd0ea6" + integrity sha512-2BRORitq5rQ4Da9blVovzNCMaUlyKrzMSvkVR0D4qPuOy/+pMCrh1d7o01RATwVy+6Fa1WBw+da7QPeLWU/1mQ== + +"@rollup/rollup-win32-arm64-msvc@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.1.tgz#c7724c386eed0bda5ae7143e4081c1910cab349b" + integrity sha512-b2bcNm9Kbde03H+q+Jjw9tSfhYkzrDUf2d5MAd1bOJuVplXvFhWz7tRtWvD8/ORZi7qSCy0idW6tf2HgxSXQSg== + +"@rollup/rollup-win32-ia32-msvc@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.1.tgz#7749e1b65cb64fe6d41ad1ad9e970a0ccc8ac350" + integrity sha512-DfcogW8N7Zg7llVEfpqWMZcaErKfsj9VvmfSyRjCyo4BI3wPEfrzTtJkZG6gKP/Z92wFm6rz2aDO7/JfiR/whA== + +"@rollup/rollup-win32-x64-msvc@4.40.1": + version "4.40.1" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.1.tgz#8078b71fe0d5825dcbf83d52a7dc858b39da165c" + integrity sha512-ECyOuDeH3C1I8jH2MK1RtBJW+YPMvSfT0a5NN0nHfQYnDSJ6tUiZH3gzwVP5/Kfh/+Tt7tpWVF9LXNTnhTJ3kA== + +"@stonecrop/aform@^0.4.11": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@stonecrop/aform/-/aform-0.4.12.tgz#ed174eab01d4585ea4f448fc37ec68938aa2b5cc" + integrity sha512-aqGD/SZCr1UmNuqY2AHd6/g14HeyVHpWurey5/TbywgGz4rLpQ7x2xnZtEcwgeIJ1fuR8iVy1o17njWR3EhgOA== + dependencies: + "@stonecrop/themes" "0.4.12" + "@stonecrop/utilities" "0.4.12" + "@vueuse/components" "^12.7.0" + "@vueuse/core" "^12.7.0" + vue "^3.5.13" + +"@stonecrop/beam@^0.4.11": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@stonecrop/beam/-/beam-0.4.12.tgz#ee9693b18e5390cead11053ff5573f93ec4a6e3e" + integrity sha512-hIyLCfudittb6Q1qe3QJSwYx+tIfignC6AuCuj1/4waYNCstAPGK5yw3KpUS+OiYvexom/f3ZuxhBPUjMQcKMg== + dependencies: + "@vueuse/components" "^12.7.0" + "@vueuse/core" "^12.7.0" + mqtt "^5.10.3" + onscan.js "^1.5.2" + vue "^3.5.13" + +"@stonecrop/themes@0.4.12": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@stonecrop/themes/-/themes-0.4.12.tgz#36c87e10946c3b0dea6626a86a9a0630c01cb72a" + integrity sha512-TiBF5piiXw+nTHAgVDGnxkObscdPH8MJRk93yvyzPP7KVDQGO9wZFodH8++pXcIlBDtGd8N783TbW90ORI7oRw== + +"@stonecrop/utilities@0.4.12": + version "0.4.12" + resolved "https://registry.yarnpkg.com/@stonecrop/utilities/-/utilities-0.4.12.tgz#0b33dbd22444571537c940671c4509645a1c448f" + integrity sha512-XL5w+Z3Xi939KIcca+Tk3FGpc26QwbH0CTah02iZKvjkwZyrXFX6MBnNCe1nEN0LxFmmGCZMZI1/6sys8Y5+0g== + dependencies: + "@vueuse/core" "^12.7.0" + vue "^3.5.13" + +"@surma/rollup-plugin-off-main-thread@^2.2.3": + version "2.2.3" + resolved "https://registry.yarnpkg.com/@surma/rollup-plugin-off-main-thread/-/rollup-plugin-off-main-thread-2.2.3.tgz#ee34985952ca21558ab0d952f00298ad2190c053" + integrity sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ== + dependencies: + ejs "^3.1.6" + json5 "^2.2.0" + magic-string "^0.25.0" + string.prototype.matchall "^4.0.6" + +"@types/estree@0.0.39": + version "0.0.39" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.39.tgz#e177e699ee1b8c22d23174caaa7422644389509f" + integrity sha512-EYNwp3bU+98cpU4lAWYYL7Zz+2gryWH1qbdDTidVd6hkiR6weksdbMadyXKXNPEkQFhXM+hVO9ZygomHXp+AIw== + +"@types/estree@1.0.7", "@types/estree@^1.0.0": + version "1.0.7" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.7.tgz#4158d3105276773d5b7695cd4834b1722e4f37a8" + integrity sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ== + +"@types/node@*": + version "22.15.3" + resolved "https://registry.yarnpkg.com/@types/node/-/node-22.15.3.tgz#b7fb9396a8ec5b5dfb1345d8ac2502060e9af68b" + integrity sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw== + dependencies: + undici-types "~6.21.0" + +"@types/node@^20.17.30": + version "20.17.32" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.17.32.tgz#cb9703514cd8e172c11beff582c66006644c2d88" + integrity sha512-zeMXFn8zQ+UkjK4ws0RiOC9EWByyW1CcVmLe+2rQocXRsGEDxUCwPEIVgpsGcLHS/P8JkT0oa3839BRABS0oPw== + dependencies: + undici-types "~6.19.2" + +"@types/readable-stream@^4.0.0": + version "4.0.18" + resolved "https://registry.yarnpkg.com/@types/readable-stream/-/readable-stream-4.0.18.tgz#5d8d15d26c776500ce573cae580787d149823bfc" + integrity sha512-21jK/1j+Wg+7jVw1xnSwy/2Q1VgVjWuFssbYGTREPUBeZ+rqVFl2udq0IkxzPC0ZhOzVceUbyIACFZKLqKEBlA== + dependencies: + "@types/node" "*" + safe-buffer "~5.1.1" + +"@types/resolve@1.20.2": + version "1.20.2" + resolved "https://registry.yarnpkg.com/@types/resolve/-/resolve-1.20.2.tgz#97d26e00cd4a0423b4af620abecf3e6f442b7975" + integrity sha512-60BCwRFOZCQhDncwQdxxeOEEkbc5dIMccYLwbxsS4TUNeVECQ/pBJ0j09mrHOl/JJvpRPGwO9SvE4nR2Nb/a4Q== + +"@types/trusted-types@^2.0.2": + version "2.0.7" + resolved "https://registry.yarnpkg.com/@types/trusted-types/-/trusted-types-2.0.7.tgz#baccb07a970b91707df3a3e8ba6896c57ead2d11" + integrity sha512-ScaPdn1dQczgbl0QFTeTOmVHFULt394XJgOQNoyVhZ6r2vLnMLJfBPd53SB52T/3G36VI1/g2MZaX0cwDuXsfw== + +"@types/web-bluetooth@^0.0.21": + version "0.0.21" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.21.tgz#525433c784aed9b457aaa0ee3d92aeb71f346b63" + integrity sha512-oIQLCGWtcFZy2JW77j9k8nHzAOpqMHLQejDA48XXMWH6tjCQHz5RCFz1bzsmROyL6PUm+LLnUiI4BCn221inxA== + +"@vitejs/plugin-vue@^5.2.3": + version "5.2.3" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.2.3.tgz#71a8fc82d4d2e425af304c35bf389506f674d89b" + integrity sha512-IYSLEQj4LgZZuoVpdSUCw3dIynTWQgPlaRP6iAvMle4My0HdYwr5g5wQAfwOeHQBmYwEkqF70nRpSilr6PoUDg== + +"@vue-macros/common@^1.16.1": + version "1.16.1" + resolved "https://registry.yarnpkg.com/@vue-macros/common/-/common-1.16.1.tgz#dac7ebc57ded4d6fb19d7f9a83d2973971d9fa65" + integrity sha512-Pn/AWMTjoMYuquepLZP813BIcq8DTZiNCoaceuNlvaYuOTd8DqBZWc5u0uOMQZMInwME1mdSmmBAcTluiV9Jtg== + dependencies: + "@vue/compiler-sfc" "^3.5.13" + ast-kit "^1.4.0" + local-pkg "^1.0.0" + magic-string-ast "^0.7.0" + pathe "^2.0.2" + picomatch "^4.0.2" + +"@vue/compiler-core@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.13.tgz#b0ae6c4347f60c03e849a05d34e5bf747c9bda05" + integrity sha512-oOdAkwqUfW1WqpwSYJce06wvt6HljgY3fGeM9NcVA1HaYOij3mZG9Rkysn0OHuyUAGMbEbARIpsG+LPVlBJ5/Q== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/shared" "3.5.13" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.13.tgz#bb1b8758dbc542b3658dda973b98a1c9311a8a58" + integrity sha512-ZOJ46sMOKUjO3e94wPdCzQ6P1Lx/vhp2RSvfaab88Ajexs0AHeV0uasYhi99WPaogmBlRHNRuly8xV75cNTMDA== + dependencies: + "@vue/compiler-core" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/compiler-sfc@3.5.13", "@vue/compiler-sfc@^3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.13.tgz#461f8bd343b5c06fac4189c4fef8af32dea82b46" + integrity sha512-6VdaljMpD82w6c2749Zhf5T9u5uLBWKnVue6XWxprDobftnletJ8+oel7sexFfM3qIxNmVE7LSFGTpv6obNyaQ== + dependencies: + "@babel/parser" "^7.25.3" + "@vue/compiler-core" "3.5.13" + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + estree-walker "^2.0.2" + magic-string "^0.30.11" + postcss "^8.4.48" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.13.tgz#e771adcca6d3d000f91a4277c972a996d07f43ba" + integrity sha512-wMH6vrYHxQl/IybKJagqbquvxpWCuVYpoUJfCqFZwa/JY1GdATAQ+TgVtgrwwMZ0D07QhA99rs/EAAWfvG6KpA== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/devtools-api@^6.6.4": + version "6.6.4" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz#cbe97fe0162b365edc1dba80e173f90492535343" + integrity sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g== + +"@vue/devtools-api@^7.7.2": + version "7.7.6" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-7.7.6.tgz#4af5dbc77bcc8543f0a8e6f029f598ed978d6c7d" + integrity sha512-b2Xx0KvXZObePpXPYHvBRRJLDQn5nhKjXh7vUhMEtWxz1AYNFOVIsh5+HLP8xDGL7sy+Q7hXeUxPHB/KgbtsPw== + dependencies: + "@vue/devtools-kit" "^7.7.6" + +"@vue/devtools-kit@^7.7.6": + version "7.7.6" + resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-7.7.6.tgz#3d9cbe2378a65ed7c4baa77ecc0f7ecdfd185fbb" + integrity sha512-geu7ds7tem2Y7Wz+WgbnbZ6T5eadOvozHZ23Atk/8tksHMFOFylKi1xgGlQlVn0wlkEf4hu+vd5ctj1G4kFtwA== + dependencies: + "@vue/devtools-shared" "^7.7.6" + birpc "^2.3.0" + hookable "^5.5.3" + mitt "^3.0.1" + perfect-debounce "^1.0.0" + speakingurl "^14.0.1" + superjson "^2.2.2" + +"@vue/devtools-shared@^7.7.6": + version "7.7.6" + resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-7.7.6.tgz#5da2218df61b605b7b88e725241fc6640df0e4b5" + integrity sha512-yFEgJZ/WblEsojQQceuyK6FzpFDx4kqrz2ohInxNj5/DnhoX023upTv4OD6lNPLAA5LLkbwPVb10o/7b+Y4FVA== + dependencies: + rfdc "^1.4.1" + +"@vue/reactivity@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.13.tgz#b41ff2bb865e093899a22219f5b25f97b6fe155f" + integrity sha512-NaCwtw8o48B9I6L1zl2p41OHo/2Z4wqYGGIK1Khu5T7yxrn+ATOixn/Udn2m+6kZKB/J7cuT9DbWWhRxqixACg== + dependencies: + "@vue/shared" "3.5.13" + +"@vue/runtime-core@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.13.tgz#1fafa4bf0b97af0ebdd9dbfe98cd630da363a455" + integrity sha512-Fj4YRQ3Az0WTZw1sFe+QDb0aXCerigEpw418pw1HBUKFtnQHWzwojaukAs2X/c9DQz4MQ4bsXTGlcpGxU/RCIw== + dependencies: + "@vue/reactivity" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/runtime-dom@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.13.tgz#610fc795de9246300e8ae8865930d534e1246215" + integrity sha512-dLaj94s93NYLqjLiyFzVs9X6dWhTdAlEAciC3Moq7gzAc13VJUdCnjjRurNM6uTLFATRHexHCTu/Xp3eW6yoog== + dependencies: + "@vue/reactivity" "3.5.13" + "@vue/runtime-core" "3.5.13" + "@vue/shared" "3.5.13" + csstype "^3.1.3" + +"@vue/server-renderer@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.13.tgz#429ead62ee51de789646c22efe908e489aad46f7" + integrity sha512-wAi4IRJV/2SAW3htkTlB+dHeRmpTiVIK1OGLWV1yeStVSebSQQOwGwIq0D3ZIoBj2C2qpgz5+vX9iEBkTdk5YA== + dependencies: + "@vue/compiler-ssr" "3.5.13" + "@vue/shared" "3.5.13" + +"@vue/shared@3.5.13": + version "3.5.13" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.13.tgz#87b309a6379c22b926e696893237826f64339b6f" + integrity sha512-/hnE/qP5ZoGpol0a5mDi45bOd7t3tjYJBjsgCsivow7D48cJeV5l05RD82lPqi7gRiphZM37rnhW1l6ZoCNNnQ== + +"@vueuse/components@^12.7.0": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/components/-/components-12.8.2.tgz#9b896f75f95058f122810010c8025d21ed107c9c" + integrity sha512-Nj27u1KsDWzoTthlChzVndJ9g0sW5APCXO3EJkSxlG11nN/ANTUlPPeoJOFvtbdDRnvsMJalboJyE0rRyg7yNg== + dependencies: + "@vueuse/core" "12.8.2" + "@vueuse/shared" "12.8.2" + vue "^3.5.13" + +"@vueuse/core@12.8.2", "@vueuse/core@^12.7.0": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-12.8.2.tgz#007c6dd29a7d1f6933e916e7a2f8ef3c3f968eaa" + integrity sha512-HbvCmZdzAu3VGi/pWYm5Ut+Kd9mn1ZHnn4L5G8kOQTPs/IwIAmJoBrmYk2ckLArgMXZj0AW3n5CAejLUO+PhdQ== + dependencies: + "@types/web-bluetooth" "^0.0.21" + "@vueuse/metadata" "12.8.2" + "@vueuse/shared" "12.8.2" + vue "^3.5.13" + +"@vueuse/core@^13.0.0": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-13.1.0.tgz#d5964c391e4d4fea3407909819c45de4bcb58211" + integrity sha512-PAauvdRXZvTWXtGLg8cPUFjiZEddTqmogdwYpnn60t08AA5a8Q4hZokBnpTOnVNqySlFlTcRYIC8OqreV4hv3Q== + dependencies: + "@types/web-bluetooth" "^0.0.21" + "@vueuse/metadata" "13.1.0" + "@vueuse/shared" "13.1.0" + +"@vueuse/metadata@12.8.2": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-12.8.2.tgz#6cb3a4e97cdcf528329eebc1bda73cd7f64318d3" + integrity sha512-rAyLGEuoBJ/Il5AmFHiziCPdQzRt88VxR+Y/A/QhJ1EWtWqPBBAxTAFaSkviwEuOEZNtW8pvkPgoCZQ+HxqW1A== + +"@vueuse/metadata@13.1.0": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-13.1.0.tgz#959f8f2da5b18fb9b80529714dddb6ab69131d82" + integrity sha512-+TDd7/a78jale5YbHX9KHW3cEDav1lz1JptwDvep2zSG8XjCsVE+9mHIzjTOaPbHUAk5XiE4jXLz51/tS+aKQw== + +"@vueuse/shared@12.8.2": + version "12.8.2" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-12.8.2.tgz#b9e4611d0603629c8e151f982459da394e22f930" + integrity sha512-dznP38YzxZoNloI0qpEfpkms8knDtaoQ6Y/sfS0L7Yki4zh40LFHEhur0odJC6xTHG5dxWVPiUWBXn+wCG2s5w== + dependencies: + vue "^3.5.13" + +"@vueuse/shared@13.1.0": + version "13.1.0" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-13.1.0.tgz#b01ad108f0de3f1b02e0591c6fc54a9ee0029298" + integrity sha512-IVS/qRRjhPTZ6C2/AM3jieqXACGwFZwWTdw5sNTSKk2m/ZpkuuN+ri+WCVUP8TqaKwJYt/KuMwmXspMAw8E6ew== + +abort-controller@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/abort-controller/-/abort-controller-3.0.0.tgz#eaf54d53b62bae4138e809ca225c8439a6efb392" + integrity sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg== + dependencies: + event-target-shim "^5.0.0" + +acorn@^8.14.0, acorn@^8.14.1, acorn@^8.8.2: + version "8.14.1" + resolved "https://registry.yarnpkg.com/acorn/-/acorn-8.14.1.tgz#721d5dc10f7d5b5609a891773d47731796935dfb" + integrity sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg== + +ajv@^8.6.0: + version "8.17.1" + resolved "https://registry.yarnpkg.com/ajv/-/ajv-8.17.1.tgz#37d9a5c776af6bc92d7f4f9510eba4c0a60d11a6" + integrity sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g== + dependencies: + fast-deep-equal "^3.1.3" + fast-uri "^3.0.1" + json-schema-traverse "^1.0.0" + require-from-string "^2.0.2" + +ansi-regex@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304" + integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ== + +ansi-regex@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-6.1.0.tgz#95ec409c69619d6cb1b8b34f14b660ef28ebd654" + integrity sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA== + +ansi-styles@^4.0.0, ansi-styles@^4.1.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-4.3.0.tgz#edd803628ae71c04c85ae7a0906edad34b648937" + integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg== + dependencies: + color-convert "^2.0.1" + +ansi-styles@^6.1.0: + version "6.2.1" + resolved "https://registry.yarnpkg.com/ansi-styles/-/ansi-styles-6.2.1.tgz#0e62320cf99c21afff3b3012192546aacbfb05c5" + integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== + +anymatch@~3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/anymatch/-/anymatch-3.1.3.tgz#790c58b19ba1720a84205b57c618d5ad8524973e" + integrity sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw== + dependencies: + normalize-path "^3.0.0" + picomatch "^2.0.4" + +array-buffer-byte-length@^1.0.1, array-buffer-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz#384d12a37295aec3769ab022ad323a18a51ccf8b" + integrity sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw== + dependencies: + call-bound "^1.0.3" + is-array-buffer "^3.0.5" + +arraybuffer.prototype.slice@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz#9d760d84dbdd06d0cbf92c8849615a1a7ab3183c" + integrity sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ== + dependencies: + array-buffer-byte-length "^1.0.1" + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + is-array-buffer "^3.0.4" + +ast-kit@^1.0.1, ast-kit@^1.4.0: + version "1.4.3" + resolved "https://registry.yarnpkg.com/ast-kit/-/ast-kit-1.4.3.tgz#030f1bfb55bd72d426dc1d0ba82a8de5c75acd7c" + integrity sha512-MdJqjpodkS5J149zN0Po+HPshkTdUyrvF7CKTafUgv69vBSPtncrj+3IiUgqdd7ElIEkbeXCsEouBUwLrw9Ilg== + dependencies: + "@babel/parser" "^7.27.0" + pathe "^2.0.3" + +ast-walker-scope@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/ast-walker-scope/-/ast-walker-scope-0.6.2.tgz#b827e8949c129802f76fe0f142e95fd7efda57dc" + integrity sha512-1UWOyC50xI3QZkRuDj6PqDtpm1oHWtYs+NQGwqL/2R11eN3Q81PHAHPM0SWW3BNQm53UDwS//Jv8L4CCVLM1bQ== + dependencies: + "@babel/parser" "^7.25.3" + ast-kit "^1.0.1" + +async-function@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/async-function/-/async-function-1.0.0.tgz#509c9fca60eaf85034c6829838188e4e4c8ffb2b" + integrity sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA== + +async@^3.2.3: + version "3.2.6" + resolved "https://registry.yarnpkg.com/async/-/async-3.2.6.tgz#1b0728e14929d51b85b449b7f06e27c1145e38ce" + integrity sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA== + +at-least-node@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/at-least-node/-/at-least-node-1.0.0.tgz#602cd4b46e844ad4effc92a8011a3c46e0238dc2" + integrity sha512-+q/t7Ekv1EDY2l6Gda6LLiX14rU9TV20Wa3ofeQmwPFZbOMo9DXrLbOjFaaclkXKWidIaopwAObQDqwWtGUjqg== + +available-typed-arrays@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz#a5cc375d6a03c2efc87a553f3e0b1522def14846" + integrity sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ== + dependencies: + possible-typed-array-names "^1.0.0" + +babel-plugin-polyfill-corejs2@^0.4.10: + version "0.4.13" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.13.tgz#7d445f0e0607ebc8fb6b01d7e8fb02069b91dd8b" + integrity sha512-3sX/eOms8kd3q2KZ6DAhKPc0dgm525Gqq5NtWKZ7QYYZEv57OQ54KtblzJzH1lQF/eQxO8KjWGIK9IPUJNus5g== + dependencies: + "@babel/compat-data" "^7.22.6" + "@babel/helper-define-polyfill-provider" "^0.6.4" + semver "^6.3.1" + +babel-plugin-polyfill-corejs3@^0.11.0: + version "0.11.1" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz#4e4e182f1bb37c7ba62e2af81d8dd09df31344f6" + integrity sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.3" + core-js-compat "^3.40.0" + +babel-plugin-polyfill-regenerator@^0.6.1: + version "0.6.4" + resolved "https://registry.yarnpkg.com/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.4.tgz#428c615d3c177292a22b4f93ed99e358d7906a9b" + integrity sha512-7gD3pRadPrbjhjLyxebmx/WrFYcuSjZ0XbdUujQMZ/fcE9oeewk2U/7PCvez84UeuK3oSjmPZ0Ch0dlupQvGzw== + dependencies: + "@babel/helper-define-polyfill-provider" "^0.6.4" + +balanced-match@^1.0.0: + version "1.0.2" + resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee" + integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== + +base64-js@^1.3.1: + version "1.5.1" + resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a" + integrity sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA== + +binary-extensions@^2.0.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/binary-extensions/-/binary-extensions-2.3.0.tgz#f6e14a97858d327252200242d4ccfe522c445522" + integrity sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw== + +birpc@^2.3.0: + version "2.3.0" + resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.3.0.tgz#e5a402dc785ef952a2383ef3cfc075e0842f3e8c" + integrity sha512-ijbtkn/F3Pvzb6jHypHRyve2QApOCZDR25D/VnkY2G/lBNcXCTsnsCxgY4k4PkVB7zfwzYbY3O9Lcqe3xufS5g== + +bl@^6.0.8: + version "6.1.0" + resolved "https://registry.yarnpkg.com/bl/-/bl-6.1.0.tgz#cc35ce7a2e8458caa8c8fb5deeed6537b73e4504" + integrity sha512-ClDyJGQkc8ZtzdAAbAwBmhMSpwN/sC9HA8jxdYm6nVUbCfZbe2mgza4qh7AuEYyEPB/c4Kznf9s66bnsKMQDjw== + dependencies: + "@types/readable-stream" "^4.0.0" + buffer "^6.0.3" + inherits "^2.0.4" + readable-stream "^4.2.0" + +brace-expansion@^1.1.7: + version "1.1.11" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-1.1.11.tgz#3c7fcbf529d87226f3d2f52b966ff5271eb441dd" + integrity sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA== + dependencies: + balanced-match "^1.0.0" + concat-map "0.0.1" + +brace-expansion@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/brace-expansion/-/brace-expansion-2.0.1.tgz#1edc459e0f0c548486ecf9fc99f2221364b9a0ae" + integrity sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA== + dependencies: + balanced-match "^1.0.0" + +braces@^3.0.3, braces@~3.0.2: + version "3.0.3" + resolved "https://registry.yarnpkg.com/braces/-/braces-3.0.3.tgz#490332f40919452272d55a8480adc0c441358789" + integrity sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA== + dependencies: + fill-range "^7.1.1" + +browserslist@^4.24.0, browserslist@^4.24.4: + version "4.24.4" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-4.24.4.tgz#c6b2865a3f08bcb860a0e827389003b9fe686e4b" + integrity sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A== + dependencies: + caniuse-lite "^1.0.30001688" + electron-to-chromium "^1.5.73" + node-releases "^2.0.19" + update-browserslist-db "^1.1.1" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +buffer@^6.0.3: + version "6.0.3" + resolved "https://registry.yarnpkg.com/buffer/-/buffer-6.0.3.tgz#2ace578459cc8fbe2a70aaa8f52ee63b6a74c6c6" + integrity sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA== + dependencies: + base64-js "^1.3.1" + ieee754 "^1.2.1" + +call-bind-apply-helpers@^1.0.0, call-bind-apply-helpers@^1.0.1, call-bind-apply-helpers@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz#4b5428c222be985d79c3d82657479dbe0b59b2d6" + integrity sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ== + dependencies: + es-errors "^1.3.0" + function-bind "^1.1.2" + +call-bind@^1.0.7, call-bind@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/call-bind/-/call-bind-1.0.8.tgz#0736a9660f537e3388826f440d5ec45f744eaa4c" + integrity sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww== + dependencies: + call-bind-apply-helpers "^1.0.0" + es-define-property "^1.0.0" + get-intrinsic "^1.2.4" + set-function-length "^1.2.2" + +call-bound@^1.0.2, call-bound@^1.0.3, call-bound@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/call-bound/-/call-bound-1.0.4.tgz#238de935d2a2a692928c538c7ccfa91067fd062a" + integrity sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg== + dependencies: + call-bind-apply-helpers "^1.0.2" + get-intrinsic "^1.3.0" + +caniuse-lite@^1.0.30001688: + version "1.0.30001716" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001716.tgz#39220dfbc58c85d9d4519e7090b656aa11ca4b85" + integrity sha512-49/c1+x3Kwz7ZIWt+4DvK3aMJy9oYXXG6/97JKsnjdCk/6n9vVyWL8NAwVt95Lwt9eigI10Hl782kDfZUUlRXw== + +chalk@^4.0.2: + version "4.1.2" + resolved "https://registry.yarnpkg.com/chalk/-/chalk-4.1.2.tgz#aac4e2b7734a740867aeb16bf02aad556a1e7a01" + integrity sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA== + dependencies: + ansi-styles "^4.1.0" + supports-color "^7.1.0" + +chokidar@^3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.6.0.tgz#197c6cc669ef2a8dc5e7b4d97ee4e092c3eb0d5b" + integrity sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw== + dependencies: + anymatch "~3.1.2" + braces "~3.0.2" + glob-parent "~5.1.2" + is-binary-path "~2.1.0" + is-glob "~4.0.1" + normalize-path "~3.0.0" + readdirp "~3.6.0" + optionalDependencies: + fsevents "~2.3.2" + +chokidar@^4.0.3: + version "4.0.3" + resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-4.0.3.tgz#7be37a4c03c9aee1ecfe862a4a23b2c70c205d30" + integrity sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA== + dependencies: + readdirp "^4.0.1" + +color-convert@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/color-convert/-/color-convert-2.0.1.tgz#72d3a68d598c9bdb3af2ad1e84f21d896abd4de3" + integrity sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ== + dependencies: + color-name "~1.1.4" + +color-name@~1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/color-name/-/color-name-1.1.4.tgz#c2a09a87acbde69543de6f63fa3995c826c536a2" + integrity sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA== + +commander@^2.20.0: + version "2.20.3" + resolved "https://registry.yarnpkg.com/commander/-/commander-2.20.3.tgz#fd485e84c03eb4881c20722ba48035e8531aeb33" + integrity sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ== + +commist@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/commist/-/commist-3.2.0.tgz#da9c8e5f245ac21510badc4b10c46b5bcc9b56cd" + integrity sha512-4PIMoPniho+LqXmpS5d3NuGYncG6XWlkBSVGiWycL22dd42OYdUGil2CWuzklaJoNxyxUSpO4MKIBU94viWNAw== + +common-tags@^1.8.0: + version "1.8.2" + resolved "https://registry.yarnpkg.com/common-tags/-/common-tags-1.8.2.tgz#94ebb3c076d26032745fd54face7f688ef5ac9c6" + integrity sha512-gk/Z852D2Wtb//0I+kRFNKKE9dIIVirjoqPoA1wJU+XePVXZfGeBpk45+A1rKO4Q43prqWBNY/MiIeRLbPWUaA== + +concat-map@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/concat-map/-/concat-map-0.0.1.tgz#d8a96bd77fd68df7793a73036a3ba0d5405d477b" + integrity sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg== + +concat-stream@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/concat-stream/-/concat-stream-2.0.0.tgz#414cf5af790a48c60ab9be4527d56d5e41133cb1" + integrity sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A== + dependencies: + buffer-from "^1.0.0" + inherits "^2.0.3" + readable-stream "^3.0.2" + typedarray "^0.0.6" + +confbox@^0.1.8: + version "0.1.8" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.1.8.tgz#820d73d3b3c82d9bd910652c5d4d599ef8ff8b06" + integrity sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w== + +confbox@^0.2.1: + version "0.2.2" + resolved "https://registry.yarnpkg.com/confbox/-/confbox-0.2.2.tgz#8652f53961c74d9e081784beed78555974a9c110" + integrity sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ== + +convert-source-map@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/convert-source-map/-/convert-source-map-2.0.0.tgz#4b560f649fc4e918dd0ab75cf4961e8bc882d82a" + integrity sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg== + +copy-anything@^3.0.2: + version "3.0.5" + resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-3.0.5.tgz#2d92dce8c498f790fa7ad16b01a1ae5a45b020a0" + integrity sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w== + dependencies: + is-what "^4.1.8" + +core-js-compat@^3.40.0: + version "3.42.0" + resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.42.0.tgz#ce19c29706ee5806e26d3cb3c542d4cfc0ed51bb" + integrity sha512-bQasjMfyDGyaeWKBIu33lHh9qlSR0MFE/Nmc6nMjf/iU9b3rSMdAYz1Baxrv4lPdGUsTqZudHA4jIGSJy0SWZQ== + dependencies: + browserslist "^4.24.4" + +cross-spawn@^7.0.6: + version "7.0.6" + resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-7.0.6.tgz#8a58fe78f00dcd70c370451759dfbfaf03e8ee9f" + integrity sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA== + dependencies: + path-key "^3.1.0" + shebang-command "^2.0.0" + which "^2.0.1" + +crypto-random-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/crypto-random-string/-/crypto-random-string-2.0.0.tgz#ef2a7a966ec11083388369baa02ebead229b30d5" + integrity sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA== + +csstype@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" + integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== + +data-view-buffer@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-buffer/-/data-view-buffer-1.0.2.tgz#211a03ba95ecaf7798a8c7198d79536211f88570" + integrity sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-length@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz#9e80f7ca52453ce3e93d25a35318767ea7704735" + integrity sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-data-view "^1.0.2" + +data-view-byte-offset@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz#068307f9b71ab76dbbe10291389e020856606191" + integrity sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-data-view "^1.0.1" + +debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.4, debug@^4.3.6, debug@^4.4.0: + version "4.4.0" + resolved "https://registry.yarnpkg.com/debug/-/debug-4.4.0.tgz#2b3f2aea2ffeb776477460267377dc8710faba8a" + integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA== + dependencies: + ms "^2.1.3" + +deepmerge@^4.2.2: + version "4.3.1" + resolved "https://registry.yarnpkg.com/deepmerge/-/deepmerge-4.3.1.tgz#44b5f2147cd3b00d4b56137685966f26fd25dd4a" + integrity sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A== + +define-data-property@^1.0.1, define-data-property@^1.1.4: + version "1.1.4" + resolved "https://registry.yarnpkg.com/define-data-property/-/define-data-property-1.1.4.tgz#894dc141bb7d3060ae4366f6a0107e68fbe48c5e" + integrity sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A== + dependencies: + es-define-property "^1.0.0" + es-errors "^1.3.0" + gopd "^1.0.1" + +define-properties@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/define-properties/-/define-properties-1.2.1.tgz#10781cc616eb951a80a034bafcaa7377f6af2b6c" + integrity sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg== + dependencies: + define-data-property "^1.0.1" + has-property-descriptors "^1.0.0" + object-keys "^1.1.1" + +dunder-proto@^1.0.0, dunder-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/dunder-proto/-/dunder-proto-1.0.1.tgz#d7ae667e1dc83482f8b70fd0f6eefc50da30f58a" + integrity sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A== + dependencies: + call-bind-apply-helpers "^1.0.1" + es-errors "^1.3.0" + gopd "^1.2.0" + +eastasianwidth@^0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/eastasianwidth/-/eastasianwidth-0.2.0.tgz#696ce2ec0aa0e6ea93a397ffcf24aa7840c827cb" + integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== + +ejs@^3.1.6: + version "3.1.10" + resolved "https://registry.yarnpkg.com/ejs/-/ejs-3.1.10.tgz#69ab8358b14e896f80cc39e62087b88500c3ac3b" + integrity sha512-UeJmFfOrAQS8OJWPZ4qtgHyWExa088/MtK5UEyoJGFH67cDEXkZSviOiKRCZ4Xij0zxI3JECgYs3oKx+AizQBA== + dependencies: + jake "^10.8.5" + +electron-to-chromium@^1.5.73: + version "1.5.149" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.5.149.tgz#b6d1a468b9537165e2494d0b5b82e9b3392d0ddb" + integrity sha512-UyiO82eb9dVOx8YO3ajDf9jz2kKyt98DEITRdeLPstOEuTlLzDA4Gyq5K9he71TQziU5jUVu2OAu5N48HmQiyQ== + +emoji-regex@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37" + integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A== + +emoji-regex@^9.2.2: + version "9.2.2" + resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-9.2.2.tgz#840c8803b0d8047f4ff0cf963176b32d4ef3ed72" + integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== + +entities@^4.5.0: + version "4.5.0" + resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" + integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== + +es-abstract@^1.23.5, es-abstract@^1.23.6, es-abstract@^1.23.9: + version "1.23.9" + resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.23.9.tgz#5b45994b7de78dada5c1bebf1379646b32b9d606" + integrity sha512-py07lI0wjxAC/DcfK1S6G7iANonniZwTISvdPzk9hzeH0IZIshbuuFxLIU96OyF89Yb9hiqWn8M/bY83KY5vzA== + dependencies: + array-buffer-byte-length "^1.0.2" + arraybuffer.prototype.slice "^1.0.4" + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.3" + data-view-buffer "^1.0.2" + data-view-byte-length "^1.0.2" + data-view-byte-offset "^1.0.1" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + es-set-tostringtag "^2.1.0" + es-to-primitive "^1.3.0" + function.prototype.name "^1.1.8" + get-intrinsic "^1.2.7" + get-proto "^1.0.0" + get-symbol-description "^1.1.0" + globalthis "^1.0.4" + gopd "^1.2.0" + has-property-descriptors "^1.0.2" + has-proto "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + internal-slot "^1.1.0" + is-array-buffer "^3.0.5" + is-callable "^1.2.7" + is-data-view "^1.0.2" + is-regex "^1.2.1" + is-shared-array-buffer "^1.0.4" + is-string "^1.1.1" + is-typed-array "^1.1.15" + is-weakref "^1.1.0" + math-intrinsics "^1.1.0" + object-inspect "^1.13.3" + object-keys "^1.1.1" + object.assign "^4.1.7" + own-keys "^1.0.1" + regexp.prototype.flags "^1.5.3" + safe-array-concat "^1.1.3" + safe-push-apply "^1.0.0" + safe-regex-test "^1.1.0" + set-proto "^1.0.0" + string.prototype.trim "^1.2.10" + string.prototype.trimend "^1.0.9" + string.prototype.trimstart "^1.0.8" + typed-array-buffer "^1.0.3" + typed-array-byte-length "^1.0.3" + typed-array-byte-offset "^1.0.4" + typed-array-length "^1.0.7" + unbox-primitive "^1.1.0" + which-typed-array "^1.1.18" + +es-define-property@^1.0.0, es-define-property@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/es-define-property/-/es-define-property-1.0.1.tgz#983eb2f9a6724e9303f61addf011c72e09e0b0fa" + integrity sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g== + +es-errors@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-errors/-/es-errors-1.3.0.tgz#05f75a25dab98e4fb1dcd5e1472c0546d5057c8f" + integrity sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw== + +es-object-atoms@^1.0.0, es-object-atoms@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/es-object-atoms/-/es-object-atoms-1.1.1.tgz#1c4f2c4837327597ce69d2ca190a7fdd172338c1" + integrity sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA== + dependencies: + es-errors "^1.3.0" + +es-set-tostringtag@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz#f31dbbe0c183b00a6d26eb6325c810c0fd18bd4d" + integrity sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA== + dependencies: + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +es-to-primitive@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/es-to-primitive/-/es-to-primitive-1.3.0.tgz#96c89c82cc49fd8794a24835ba3e1ff87f214e18" + integrity sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g== + dependencies: + is-callable "^1.2.7" + is-date-object "^1.0.5" + is-symbol "^1.0.4" + +esbuild@^0.21.3: + version "0.21.5" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.21.5.tgz#9ca301b120922959b766360d8ac830da0d02997d" + integrity sha512-mg3OPMV4hXywwpoDxu3Qda5xCKQi+vCTZq8S9J/EpkhB2HzKXq4SNFZE3+NK93JYxc8VMSep+lOUSC/RVKaBqw== + 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" + +escalade@^3.2.0: + version "3.2.0" + resolved "https://registry.yarnpkg.com/escalade/-/escalade-3.2.0.tgz#011a3f69856ba189dffa7dc8fcce99d2a87903e5" + integrity sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA== + +estree-walker@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-1.0.1.tgz#31bc5d612c96b704106b477e6dd5d8aa138cb700" + integrity sha512-1fMXF3YP4pZZVozF8j/ZLfvnR8NSIljt56UhbZ5PeeDmmGHpgpdwQt7ITlGvYaQukCvuBRMLEiKiYC+oeIg4cg== + +estree-walker@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/estree-walker/-/estree-walker-2.0.2.tgz#52f010178c2a4c117a7757cfe942adb7d2da4cac" + integrity sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w== + +esutils@^2.0.2: + version "2.0.3" + resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.3.tgz#74d2eb4de0b8da1293711910d50775b9b710ef64" + integrity sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g== + +event-target-shim@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/event-target-shim/-/event-target-shim-5.0.1.tgz#5d4d3ebdf9583d63a5333ce2deb7480ab2b05789" + integrity sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ== + +events@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/events/-/events-3.3.0.tgz#31a95ad0a924e2d2c419a813aeb2c4e878ea7400" + integrity sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q== + +exsolve@^1.0.1: + version "1.0.5" + resolved "https://registry.yarnpkg.com/exsolve/-/exsolve-1.0.5.tgz#1f5b6b4fe82ad6b28a173ccb955a635d77859dcf" + integrity sha512-pz5dvkYYKQ1AHVrgOzBKWeP4u4FRb3a6DNK2ucr0OoNwYIU4QWsJ+NM36LLzORT+z845MzKHHhpXiUF5nvQoJg== + +fast-deep-equal@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz#3a7d56b559d6cbc3eb512325244e619a65c6c525" + integrity sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q== + +fast-glob@^3.3.3: + version "3.3.3" + resolved "https://registry.yarnpkg.com/fast-glob/-/fast-glob-3.3.3.tgz#d06d585ce8dba90a16b0505c543c3ccfb3aeb818" + integrity sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg== + dependencies: + "@nodelib/fs.stat" "^2.0.2" + "@nodelib/fs.walk" "^1.2.3" + glob-parent "^5.1.2" + merge2 "^1.3.0" + micromatch "^4.0.8" + +fast-json-stable-stringify@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz#874bf69c6f404c2b5d99c481341399fd55892633" + integrity sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw== + +fast-unique-numbers@^8.0.13: + version "8.0.13" + resolved "https://registry.yarnpkg.com/fast-unique-numbers/-/fast-unique-numbers-8.0.13.tgz#3c87232061ff5f408a216e1f0121232f76f695d7" + integrity sha512-7OnTFAVPefgw2eBJ1xj2PGGR9FwYzSUso9decayHgCDX4sJkHLdcsYTytTg+tYv+wKF3U8gJuSBz2jJpQV4u/g== + dependencies: + "@babel/runtime" "^7.23.8" + tslib "^2.6.2" + +fast-uri@^3.0.1: + version "3.0.6" + resolved "https://registry.yarnpkg.com/fast-uri/-/fast-uri-3.0.6.tgz#88f130b77cfaea2378d56bf970dea21257a68748" + integrity sha512-Atfo14OibSv5wAp4VWNsFYE1AchQRTv9cBGWET4pZWHzYshFSS9NQI6I57rdKn9croWVMbYFbLhJ+yJvmZIIHw== + +fastq@^1.6.0: + version "1.19.1" + resolved "https://registry.yarnpkg.com/fastq/-/fastq-1.19.1.tgz#d50eaba803c8846a883c16492821ebcd2cda55f5" + integrity sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ== + dependencies: + reusify "^1.0.4" + +fdir@^6.4.4: + version "6.4.4" + resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.4.4.tgz#1cfcf86f875a883e19a8fab53622cfe992e8d2f9" + integrity sha512-1NZP+GK4GfuAv3PqKvxQRDMjdSRZjnkq7KfhlNrCNNlZ0ygQFpebfrnfnq/W7fpUnAv9aGWmY1zKx7FYL3gwhg== + +filelist@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/filelist/-/filelist-1.0.4.tgz#f78978a1e944775ff9e62e744424f215e58352b5" + integrity sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q== + dependencies: + minimatch "^5.0.1" + +fill-range@^7.1.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/fill-range/-/fill-range-7.1.1.tgz#44265d3cac07e3ea7dc247516380643754a05292" + integrity sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg== + dependencies: + to-regex-range "^5.0.1" + +for-each@^0.3.3, for-each@^0.3.5: + version "0.3.5" + resolved "https://registry.yarnpkg.com/for-each/-/for-each-0.3.5.tgz#d650688027826920feeb0af747ee7b9421a41d47" + integrity sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg== + dependencies: + is-callable "^1.2.7" + +foreground-child@^3.1.0: + version "3.3.1" + resolved "https://registry.yarnpkg.com/foreground-child/-/foreground-child-3.3.1.tgz#32e8e9ed1b68a3497befb9ac2b6adf92a638576f" + integrity sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw== + dependencies: + cross-spawn "^7.0.6" + signal-exit "^4.0.1" + +fs-extra@^9.0.1: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== + dependencies: + at-least-node "^1.0.0" + graceful-fs "^4.2.0" + jsonfile "^6.0.1" + universalify "^2.0.0" + +fs.realpath@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/fs.realpath/-/fs.realpath-1.0.0.tgz#1504ad2523158caa40db4a2787cb01411994ea4f" + integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== + +fsevents@~2.3.2, fsevents@~2.3.3: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +function-bind@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/function-bind/-/function-bind-1.1.2.tgz#2c02d864d97f3ea6c8830c464cbd11ab6eab7a1c" + integrity sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA== + +function.prototype.name@^1.1.6, function.prototype.name@^1.1.8: + version "1.1.8" + resolved "https://registry.yarnpkg.com/function.prototype.name/-/function.prototype.name-1.1.8.tgz#e68e1df7b259a5c949eeef95cdbde53edffabb78" + integrity sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + functions-have-names "^1.2.3" + hasown "^2.0.2" + is-callable "^1.2.7" + +functions-have-names@^1.2.3: + version "1.2.3" + resolved "https://registry.yarnpkg.com/functions-have-names/-/functions-have-names-1.2.3.tgz#0404fe4ee2ba2f607f0e0ec3c80bae994133b834" + integrity sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ== + +gensync@^1.0.0-beta.2: + version "1.0.0-beta.2" + resolved "https://registry.yarnpkg.com/gensync/-/gensync-1.0.0-beta.2.tgz#32a6ee76c3d7f52d46b2b1ae5d93fea8580a25e0" + integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg== + +get-intrinsic@^1.2.4, get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.2.7, get-intrinsic@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/get-intrinsic/-/get-intrinsic-1.3.0.tgz#743f0e3b6964a93a5491ed1bffaae054d7f98d01" + integrity sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ== + dependencies: + call-bind-apply-helpers "^1.0.2" + es-define-property "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.1.1" + function-bind "^1.1.2" + get-proto "^1.0.1" + gopd "^1.2.0" + has-symbols "^1.1.0" + hasown "^2.0.2" + math-intrinsics "^1.1.0" + +get-own-enumerable-property-symbols@^3.0.0: + version "3.0.2" + resolved "https://registry.yarnpkg.com/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz#b5fde77f22cbe35f390b4e089922c50bce6ef664" + integrity sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g== + +get-proto@^1.0.0, get-proto@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/get-proto/-/get-proto-1.0.1.tgz#150b3f2743869ef3e851ec0c49d15b1d14d00ee1" + integrity sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g== + dependencies: + dunder-proto "^1.0.1" + es-object-atoms "^1.0.0" + +get-symbol-description@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/get-symbol-description/-/get-symbol-description-1.1.0.tgz#7bdd54e0befe8ffc9f3b4e203220d9f1e881b6ee" + integrity sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + get-intrinsic "^1.2.6" + +glob-parent@^5.1.2, glob-parent@~5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-5.1.2.tgz#869832c58034fe68a4093c17dc15e8340d8401c4" + integrity sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow== + dependencies: + is-glob "^4.0.1" + +glob@^11.0.1: + version "11.0.2" + resolved "https://registry.yarnpkg.com/glob/-/glob-11.0.2.tgz#3261e3897bbc603030b041fd77ba636022d51ce0" + integrity sha512-YT7U7Vye+t5fZ/QMkBFrTJ7ZQxInIUjwyAjVj84CYXqgBdv30MFUPGnBR6sQaVq6Is15wYJUsnzTuWaGRBhBAQ== + dependencies: + foreground-child "^3.1.0" + jackspeak "^4.0.1" + minimatch "^10.0.0" + minipass "^7.1.2" + package-json-from-dist "^1.0.0" + path-scurry "^2.0.0" + +glob@^7.1.6: + version "7.2.3" + resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" + integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== + dependencies: + fs.realpath "^1.0.0" + inflight "^1.0.4" + inherits "2" + minimatch "^3.1.1" + once "^1.3.0" + path-is-absolute "^1.0.0" + +globals@^11.1.0: + version "11.12.0" + resolved "https://registry.yarnpkg.com/globals/-/globals-11.12.0.tgz#ab8795338868a0babd8525758018c2a7eb95c42e" + integrity sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA== + +globalthis@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/globalthis/-/globalthis-1.0.4.tgz#7430ed3a975d97bfb59bcce41f5cabbafa651236" + integrity sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ== + dependencies: + define-properties "^1.2.1" + gopd "^1.0.1" + +gopd@^1.0.1, gopd@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/gopd/-/gopd-1.2.0.tgz#89f56b8217bdbc8802bd299df6d7f1081d7e51a1" + integrity sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg== + +graceful-fs@^4.1.6, graceful-fs@^4.2.0: + version "4.2.11" + resolved "https://registry.yarnpkg.com/graceful-fs/-/graceful-fs-4.2.11.tgz#4183e4e8bf08bb6e05bbb2f7d2e0c8f712ca40e3" + integrity sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ== + +has-bigints@^1.0.2: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-bigints/-/has-bigints-1.1.0.tgz#28607e965ac967e03cd2a2c70a2636a1edad49fe" + integrity sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg== + +has-flag@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-4.0.0.tgz#944771fd9c81c81265c4d6941860da06bb59479b" + integrity sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ== + +has-property-descriptors@^1.0.0, has-property-descriptors@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz#963ed7d071dc7bf5f084c5bfbe0d1b6222586854" + integrity sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg== + dependencies: + es-define-property "^1.0.0" + +has-proto@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/has-proto/-/has-proto-1.2.0.tgz#5de5a6eabd95fdffd9818b43055e8065e39fe9d5" + integrity sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ== + dependencies: + dunder-proto "^1.0.0" + +has-symbols@^1.0.3, has-symbols@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/has-symbols/-/has-symbols-1.1.0.tgz#fc9c6a783a084951d0b971fe1018de813707a338" + integrity sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ== + +has-tostringtag@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/has-tostringtag/-/has-tostringtag-1.0.2.tgz#2cdc42d40bef2e5b4eeab7c01a73c54ce7ab5abc" + integrity sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw== + dependencies: + has-symbols "^1.0.3" + +hasown@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/hasown/-/hasown-2.0.2.tgz#003eaf91be7adc372e84ec59dc37252cedb80003" + integrity sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ== + dependencies: + function-bind "^1.1.2" + +help-me@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/help-me/-/help-me-5.0.0.tgz#b1ebe63b967b74060027c2ac61f9be12d354a6f6" + integrity sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg== + +hookable@^5.5.3: + version "5.5.3" + resolved "https://registry.yarnpkg.com/hookable/-/hookable-5.5.3.tgz#6cfc358984a1ef991e2518cb9ed4a778bbd3215d" + integrity sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ== + +idb@^7.0.1: + version "7.1.1" + resolved "https://registry.yarnpkg.com/idb/-/idb-7.1.1.tgz#d910ded866d32c7ced9befc5bfdf36f572ced72b" + integrity sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ== + +ieee754@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/ieee754/-/ieee754-1.2.1.tgz#8eb7a10a63fff25d15a57b001586d177d1b0d352" + integrity sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA== + +inflight@^1.0.4: + version "1.0.6" + resolved "https://registry.yarnpkg.com/inflight/-/inflight-1.0.6.tgz#49bd6331d7d02d0c09bc910a1075ba8165b56df9" + integrity sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA== + dependencies: + once "^1.3.0" + wrappy "1" + +inherits@2, inherits@^2.0.3, inherits@^2.0.4: + version "2.0.4" + resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" + integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== + +internal-slot@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/internal-slot/-/internal-slot-1.1.0.tgz#1eac91762947d2f7056bc838d93e13b2e9604961" + integrity sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw== + dependencies: + es-errors "^1.3.0" + hasown "^2.0.2" + side-channel "^1.1.0" + +ip-address@^9.0.5: + version "9.0.5" + resolved "https://registry.yarnpkg.com/ip-address/-/ip-address-9.0.5.tgz#117a960819b08780c3bd1f14ef3c1cc1d3f3ea5a" + integrity sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g== + dependencies: + jsbn "1.1.0" + sprintf-js "^1.1.3" + +is-array-buffer@^3.0.4, is-array-buffer@^3.0.5: + version "3.0.5" + resolved "https://registry.yarnpkg.com/is-array-buffer/-/is-array-buffer-3.0.5.tgz#65742e1e687bd2cc666253068fd8707fe4d44280" + integrity sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +is-async-function@^2.0.0: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-async-function/-/is-async-function-2.1.1.tgz#3e69018c8e04e73b738793d020bfe884b9fd3523" + integrity sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ== + dependencies: + async-function "^1.0.0" + call-bound "^1.0.3" + get-proto "^1.0.1" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-bigint@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-bigint/-/is-bigint-1.1.0.tgz#dda7a3445df57a42583db4228682eba7c4170672" + integrity sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ== + dependencies: + has-bigints "^1.0.2" + +is-binary-path@~2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/is-binary-path/-/is-binary-path-2.1.0.tgz#ea1f7f3b80f064236e83470f86c09c254fb45b09" + integrity sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw== + dependencies: + binary-extensions "^2.0.0" + +is-boolean-object@^1.2.1: + version "1.2.2" + resolved "https://registry.yarnpkg.com/is-boolean-object/-/is-boolean-object-1.2.2.tgz#7067f47709809a393c71ff5bb3e135d8a9215d9e" + integrity sha512-wa56o2/ElJMYqjCjGkXri7it5FbebW5usLw/nPmCMs5DeZ7eziSYZhSmPRn0txqeW4LnAmQQU7FgqLpsEFKM4A== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-callable@^1.2.7: + version "1.2.7" + resolved "https://registry.yarnpkg.com/is-callable/-/is-callable-1.2.7.tgz#3bc2a85ea742d9e36205dcacdd72ca1fdc51b055" + integrity sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA== + +is-core-module@^2.16.0: + version "2.16.1" + resolved "https://registry.yarnpkg.com/is-core-module/-/is-core-module-2.16.1.tgz#2a98801a849f43e2add644fbb6bc6229b19a4ef4" + integrity sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w== + dependencies: + hasown "^2.0.2" + +is-data-view@^1.0.1, is-data-view@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/is-data-view/-/is-data-view-1.0.2.tgz#bae0a41b9688986c2188dda6657e56b8f9e63b8e" + integrity sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw== + dependencies: + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + is-typed-array "^1.1.13" + +is-date-object@^1.0.5, is-date-object@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-date-object/-/is-date-object-1.1.0.tgz#ad85541996fc7aa8b2729701d27b7319f95d82f7" + integrity sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg== + dependencies: + call-bound "^1.0.2" + has-tostringtag "^1.0.2" + +is-extglob@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" + integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ== + +is-finalizationregistry@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz#eefdcdc6c94ddd0674d9c85887bf93f944a97c90" + integrity sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg== + dependencies: + call-bound "^1.0.3" + +is-fullwidth-code-point@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d" + integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg== + +is-generator-function@^1.0.10: + version "1.1.0" + resolved "https://registry.yarnpkg.com/is-generator-function/-/is-generator-function-1.1.0.tgz#bf3eeda931201394f57b5dba2800f91a238309ca" + integrity sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ== + dependencies: + call-bound "^1.0.3" + get-proto "^1.0.0" + has-tostringtag "^1.0.2" + safe-regex-test "^1.1.0" + +is-glob@^4.0.1, is-glob@~4.0.1: + version "4.0.3" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.3.tgz#64f61e42cbbb2eec2071a9dac0b28ba1e65d5084" + integrity sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg== + dependencies: + is-extglob "^2.1.1" + +is-map@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-map/-/is-map-2.0.3.tgz#ede96b7fe1e270b3c4465e3a465658764926d62e" + integrity sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw== + +is-module@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" + integrity sha512-51ypPSPCoTEIN9dy5Oy+h4pShgJmPCygKfyRCISBI+JoWT/2oJvK8QPxmwv7b/p239jXrm9M1mlQbyKJ5A152g== + +is-number-object@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-number-object/-/is-number-object-1.1.1.tgz#144b21e95a1bc148205dcc2814a9134ec41b2541" + integrity sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-number@^7.0.0: + version "7.0.0" + resolved "https://registry.yarnpkg.com/is-number/-/is-number-7.0.0.tgz#7535345b896734d5f80c4d06c50955527a14f12b" + integrity sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng== + +is-obj@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/is-obj/-/is-obj-1.0.1.tgz#3e4729ac1f5fde025cd7d83a896dab9f4f67db0f" + integrity sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg== + +is-regex@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/is-regex/-/is-regex-1.2.1.tgz#76d70a3ed10ef9be48eb577887d74205bf0cad22" + integrity sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g== + dependencies: + call-bound "^1.0.2" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + hasown "^2.0.2" + +is-regexp@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-regexp/-/is-regexp-1.0.0.tgz#fd2d883545c46bac5a633e7b9a09e87fa2cb5069" + integrity sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA== + +is-set@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/is-set/-/is-set-2.0.3.tgz#8ab209ea424608141372ded6e0cb200ef1d9d01d" + integrity sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg== + +is-shared-array-buffer@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz#9b67844bd9b7f246ba0708c3a93e34269c774f6f" + integrity sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A== + dependencies: + call-bound "^1.0.3" + +is-stream@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/is-stream/-/is-stream-2.0.1.tgz#fac1e3d53b97ad5a9d0ae9cef2389f5810a5c077" + integrity sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg== + +is-string@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-string/-/is-string-1.1.1.tgz#92ea3f3d5c5b6e039ca8677e5ac8d07ea773cbb9" + integrity sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA== + dependencies: + call-bound "^1.0.3" + has-tostringtag "^1.0.2" + +is-symbol@^1.0.4, is-symbol@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-symbol/-/is-symbol-1.1.1.tgz#f47761279f532e2b05a7024a7506dbbedacd0634" + integrity sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w== + dependencies: + call-bound "^1.0.2" + has-symbols "^1.1.0" + safe-regex-test "^1.1.0" + +is-typed-array@^1.1.13, is-typed-array@^1.1.14, is-typed-array@^1.1.15: + version "1.1.15" + resolved "https://registry.yarnpkg.com/is-typed-array/-/is-typed-array-1.1.15.tgz#4bfb4a45b61cee83a5a46fba778e4e8d59c0ce0b" + integrity sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ== + dependencies: + which-typed-array "^1.1.16" + +is-weakmap@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/is-weakmap/-/is-weakmap-2.0.2.tgz#bf72615d649dfe5f699079c54b83e47d1ae19cfd" + integrity sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w== + +is-weakref@^1.0.2, is-weakref@^1.1.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/is-weakref/-/is-weakref-1.1.1.tgz#eea430182be8d64174bd96bffbc46f21bf3f9293" + integrity sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew== + dependencies: + call-bound "^1.0.3" + +is-weakset@^2.0.3: + version "2.0.4" + resolved "https://registry.yarnpkg.com/is-weakset/-/is-weakset-2.0.4.tgz#c9f5deb0bc1906c6d6f1027f284ddf459249daca" + integrity sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ== + dependencies: + call-bound "^1.0.3" + get-intrinsic "^1.2.6" + +is-what@^4.1.8: + version "4.1.16" + resolved "https://registry.yarnpkg.com/is-what/-/is-what-4.1.16.tgz#1ad860a19da8b4895ad5495da3182ce2acdd7a6f" + integrity sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A== + +isarray@^2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-2.0.5.tgz#8af1e4c1221244cc62459faf38940d4e644a5723" + integrity sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw== + +isexe@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/isexe/-/isexe-2.0.0.tgz#e8fbf374dc556ff8947a10dcb0572d633f2cfa10" + integrity sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw== + +jackspeak@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/jackspeak/-/jackspeak-4.1.0.tgz#c489c079f2b636dc4cbe9b0312a13ff1282e561b" + integrity sha512-9DDdhb5j6cpeitCbvLO7n7J4IxnbM6hoF6O1g4HQ5TfhvvKN8ywDM7668ZhMHRqVmxqhps/F6syWK2KcPxYlkw== + dependencies: + "@isaacs/cliui" "^8.0.2" + +jake@^10.8.5: + version "10.9.2" + resolved "https://registry.yarnpkg.com/jake/-/jake-10.9.2.tgz#6ae487e6a69afec3a5e167628996b59f35ae2b7f" + integrity sha512-2P4SQ0HrLQ+fw6llpLnOaGAvN2Zu6778SJMrCUwns4fOoG9ayrTiZk3VV8sCPkVZF8ab0zksVpS8FDY5pRCNBA== + dependencies: + async "^3.2.3" + chalk "^4.0.2" + filelist "^1.0.4" + minimatch "^3.1.2" + +js-sdsl@4.3.0: + version "4.3.0" + resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.3.0.tgz#aeefe32a451f7af88425b11fdb5f58c90ae1d711" + integrity sha512-mifzlm2+5nZ+lEcLJMoBK0/IH/bDg8XnJfd/Wq6IP+xoCjLZsTOnV2QpxlVbX9bMnkl5PdEjNtBJ9Cj1NjifhQ== + +js-tokens@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-tokens/-/js-tokens-4.0.0.tgz#19203fb59991df98e3a287050d4647cdeaf32499" + integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== + +jsbn@1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/jsbn/-/jsbn-1.1.0.tgz#b01307cb29b618a1ed26ec79e911f803c4da0040" + integrity sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A== + +jsesc@^3.0.2: + version "3.1.0" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.1.0.tgz#74d335a234f67ed19907fdadfac7ccf9d409825d" + integrity sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA== + +jsesc@~3.0.2: + version "3.0.2" + resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-3.0.2.tgz#bb8b09a6597ba426425f2e4a07245c3d00b9343e" + integrity sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g== + +json-schema-traverse@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz#ae7bcb3656ab77a73ba5c49bf654f38e6b6860e2" + integrity sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug== + +json-schema@^0.4.0: + version "0.4.0" + resolved "https://registry.yarnpkg.com/json-schema/-/json-schema-0.4.0.tgz#f7de4cf6efab838ebaeb3236474cbba5a1930ab5" + integrity sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA== + +json5@^2.2.0, json5@^2.2.3: + version "2.2.3" + resolved "https://registry.yarnpkg.com/json5/-/json5-2.2.3.tgz#78cd6f1a19bdc12b73db5ad0c61efd66c1e29283" + integrity sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg== + +jsonfile@^6.0.1: + version "6.1.0" + resolved "https://registry.yarnpkg.com/jsonfile/-/jsonfile-6.1.0.tgz#bc55b2634793c679ec6403094eb13698a6ec0aae" + integrity sha512-5dgndWOriYSm5cnYaJNhalLNDKOqFwyDB/rr1E9ZsGciGvKPs8R2xYGCacuf3z6K1YKDz182fd+fY3cn3pMqXQ== + dependencies: + universalify "^2.0.0" + optionalDependencies: + graceful-fs "^4.1.6" + +jsonpointer@^5.0.0: + version "5.0.1" + resolved "https://registry.yarnpkg.com/jsonpointer/-/jsonpointer-5.0.1.tgz#2110e0af0900fd37467b5907ecd13a7884a1b559" + integrity sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ== + +leven@^3.1.0: + version "3.1.0" + resolved "https://registry.yarnpkg.com/leven/-/leven-3.1.0.tgz#77891de834064cccba82ae7842bb6b14a13ed7f2" + integrity sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A== + +local-pkg@^1.0.0, local-pkg@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/local-pkg/-/local-pkg-1.1.1.tgz#f5fe74a97a3bd3c165788ee08ca9fbe998dc58dd" + integrity sha512-WunYko2W1NcdfAFpuLUoucsgULmgDBRkdxHxWQ7mK0cQqwPiy8E1enjuRBrhLtZkB5iScJ1XIPdhVEFK8aOLSg== + dependencies: + mlly "^1.7.4" + pkg-types "^2.0.1" + quansync "^0.2.8" + +lodash.debounce@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz#82d79bff30a67c4005ffd5e2515300ad9ca4d7af" + integrity sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow== + +lodash.sortby@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/lodash.sortby/-/lodash.sortby-4.7.0.tgz#edd14c824e2cc9c1e0b0a1b42bb5210516a42438" + integrity sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA== + +lodash@^4.17.20: + version "4.17.21" + resolved "https://registry.yarnpkg.com/lodash/-/lodash-4.17.21.tgz#679591c564c3bffaae8454cf0b3df370c3d6911c" + integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== + +lru-cache@^10.4.3: + version "10.4.3" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-10.4.3.tgz#410fc8a17b70e598013df257c2446b7f3383f119" + integrity sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ== + +lru-cache@^11.0.0: + version "11.1.0" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-11.1.0.tgz#afafb060607108132dbc1cf8ae661afb69486117" + integrity sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A== + +lru-cache@^5.1.1: + version "5.1.1" + resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-5.1.1.tgz#1da27e6710271947695daf6848e847f01d84b920" + integrity sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w== + dependencies: + yallist "^3.0.2" + +magic-string-ast@^0.7.0: + version "0.7.1" + resolved "https://registry.yarnpkg.com/magic-string-ast/-/magic-string-ast-0.7.1.tgz#07b044f98d061a71f1e38f08de2fa5fee4051563" + integrity sha512-ub9iytsEbT7Yw/Pd29mSo/cNQpaEu67zR1VVcXDiYjSFwzeBxNdTd0FMnSslLQXiRj8uGPzwsaoefrMD5XAmdw== + dependencies: + magic-string "^0.30.17" + +magic-string@^0.25.0, magic-string@^0.25.7: + version "0.25.9" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.25.9.tgz#de7f9faf91ef8a1c91d02c2e5314c8277dbcdd1c" + integrity sha512-RmF0AsMzgt25qzqqLc1+MbHmhdx0ojF2Fvs4XnOqz2ZOBXzzkEwc/dJQZCYHAn7v1jbVOjAZfK8msRn4BxO4VQ== + dependencies: + sourcemap-codec "^1.4.8" + +magic-string@^0.30.11, magic-string@^0.30.17: + version "0.30.17" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.17.tgz#450a449673d2460e5bbcfba9a61916a1714c7453" + integrity sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA== + dependencies: + "@jridgewell/sourcemap-codec" "^1.5.0" + +math-intrinsics@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/math-intrinsics/-/math-intrinsics-1.1.0.tgz#a0dd74be81e2aa5c2f27e65ce283605ee4e2b7f9" + integrity sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g== + +merge2@^1.3.0: + version "1.4.1" + resolved "https://registry.yarnpkg.com/merge2/-/merge2-1.4.1.tgz#4368892f885e907455a6fd7dc55c0c9d404990ae" + integrity sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg== + +micromatch@^4.0.8: + version "4.0.8" + resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-4.0.8.tgz#d66fa18f3a47076789320b9b1af32bd86d9fa202" + integrity sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA== + dependencies: + braces "^3.0.3" + picomatch "^2.3.1" + +minimatch@^10.0.0: + version "10.0.1" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-10.0.1.tgz#ce0521856b453c86e25f2c4c0d03e6ff7ddc440b" + integrity sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ== + dependencies: + brace-expansion "^2.0.1" + +minimatch@^3.1.1, minimatch@^3.1.2: + version "3.1.2" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" + integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== + dependencies: + brace-expansion "^1.1.7" + +minimatch@^5.0.1: + version "5.1.6" + resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-5.1.6.tgz#1cfcb8cf5522ea69952cd2af95ae09477f122a96" + integrity sha512-lKwV/1brpG6mBUFHtb7NUmtABCb2WZZmm2wNiOA5hAb8VdCS4B3dtMWyvcoViccwAW/COERjXLt0zP1zXUN26g== + dependencies: + brace-expansion "^2.0.1" + +minimist@^1.2.8: + version "1.2.8" + resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" + integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== + +minipass@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/minipass/-/minipass-7.1.2.tgz#93a9626ce5e5e66bd4db86849e7515e92340a707" + integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== + +mitt@^3.0.1: + version "3.0.1" + resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" + integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== + +mlly@^1.7.4: + version "1.7.4" + resolved "https://registry.yarnpkg.com/mlly/-/mlly-1.7.4.tgz#3d7295ea2358ec7a271eaa5d000a0f84febe100f" + integrity sha512-qmdSIPC4bDJXgZTCR7XosJiNKySV7O215tsPtDN9iEO/7q/76b/ijtgRu/+epFXSJhijtTCCGp3DWS549P3xKw== + dependencies: + acorn "^8.14.0" + pathe "^2.0.1" + pkg-types "^1.3.0" + ufo "^1.5.4" + +mqtt-packet@^9.0.2: + version "9.0.2" + resolved "https://registry.yarnpkg.com/mqtt-packet/-/mqtt-packet-9.0.2.tgz#fe6ae2c36fe3f269d11b3fe663b53648f3b3700a" + integrity sha512-MvIY0B8/qjq7bKxdN1eD+nrljoeaai+qjLJgfRn3TiMuz0pamsIWY2bFODPZMSNmabsLANXsLl4EMoWvlaTZWA== + dependencies: + bl "^6.0.8" + debug "^4.3.4" + process-nextick-args "^2.0.1" + +mqtt@^5.10.3: + version "5.12.0" + resolved "https://registry.yarnpkg.com/mqtt/-/mqtt-5.12.0.tgz#47b09a8d7e435d6448ee2bf6f887dd31b1582cda" + integrity sha512-IKx8q4/0P0VfM/skx03CPMBMBgT9BZoVIMTev9rpauJUm36dW2/Mo3xFBZNt3kaS/4wX/Fsn6x/tbofDRpXiLw== + dependencies: + commist "^3.2.0" + concat-stream "^2.0.0" + debug "^4.4.0" + help-me "^5.0.0" + lru-cache "^10.4.3" + minimist "^1.2.8" + mqtt-packet "^9.0.2" + number-allocator "^1.0.14" + readable-stream "^4.7.0" + rfdc "^1.4.1" + socks "^2.8.3" + split2 "^4.2.0" + worker-timers "^7.1.8" + ws "^8.18.0" + +ms@^2.1.3: + version "2.1.3" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" + integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== + +nanoid@^3.3.8: + version "3.3.11" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" + integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== + +node-releases@^2.0.19: + version "2.0.19" + resolved "https://registry.yarnpkg.com/node-releases/-/node-releases-2.0.19.tgz#9e445a52950951ec4d177d843af370b411caf314" + integrity sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw== + +normalize-path@^3.0.0, normalize-path@~3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/normalize-path/-/normalize-path-3.0.0.tgz#0dcd69ff23a1c9b11fd0978316644a0388216a65" + integrity sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA== + +number-allocator@^1.0.14: + version "1.0.14" + resolved "https://registry.yarnpkg.com/number-allocator/-/number-allocator-1.0.14.tgz#1f2e32855498a7740dcc8c78bed54592d930ee4d" + integrity sha512-OrL44UTVAvkKdOdRQZIJpLkAdjXGTRda052sN4sO77bKEzYYqWKMBjQvrJFzqygI99gL6Z4u2xctPW1tB8ErvA== + dependencies: + debug "^4.3.1" + js-sdsl "4.3.0" + +object-inspect@^1.13.3: + version "1.13.4" + resolved "https://registry.yarnpkg.com/object-inspect/-/object-inspect-1.13.4.tgz#8375265e21bc20d0fa582c22e1b13485d6e00213" + integrity sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew== + +object-keys@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/object-keys/-/object-keys-1.1.1.tgz#1c47f272df277f3b1daf061677d9c82e2322c60e" + integrity sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA== + +object.assign@^4.1.7: + version "4.1.7" + resolved "https://registry.yarnpkg.com/object.assign/-/object.assign-4.1.7.tgz#8c14ca1a424c6a561b0bb2a22f66f5049a945d3d" + integrity sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + has-symbols "^1.1.0" + object-keys "^1.1.1" + +once@^1.3.0: + version "1.4.0" + resolved "https://registry.yarnpkg.com/once/-/once-1.4.0.tgz#583b1aa775961d4b113ac17d9c50baef9dd76bd1" + integrity sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w== + dependencies: + wrappy "1" + onscan.js@^1.5.2: version "1.5.2" resolved "https://registry.yarnpkg.com/onscan.js/-/onscan.js-1.5.2.tgz#14ed636e5f4c3f0a78bacbf9a505dad3140ee341" integrity sha512-9oGYy2gXYRjvXO9GYqqVca0VuCTAmWhbmX3egBSBP13rXiMNb+dKPJzKFEeECGqPBpf0m40Zoo+GUQ7eCackdw== + +own-keys@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/own-keys/-/own-keys-1.0.1.tgz#e4006910a2bf913585289676eebd6f390cf51358" + integrity sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg== + dependencies: + get-intrinsic "^1.2.6" + object-keys "^1.1.1" + safe-push-apply "^1.0.0" + +package-json-from-dist@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/package-json-from-dist/-/package-json-from-dist-1.0.1.tgz#4f1471a010827a86f94cfd9b0727e36d267de505" + integrity sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw== + +path-is-absolute@^1.0.0: + version "1.0.1" + resolved "https://registry.yarnpkg.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz#174b9268735534ffbc7ace6bf53a5a9e1b5c5f5f" + integrity sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg== + +path-key@^3.1.0: + version "3.1.1" + resolved "https://registry.yarnpkg.com/path-key/-/path-key-3.1.1.tgz#581f6ade658cbba65a0d3380de7753295054f375" + integrity sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q== + +path-parse@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/path-parse/-/path-parse-1.0.7.tgz#fbc114b60ca42b30d9daf5858e4bd68bbedb6735" + integrity sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw== + +path-scurry@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/path-scurry/-/path-scurry-2.0.0.tgz#9f052289f23ad8bf9397a2a0425e7b8615c58580" + integrity sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg== + dependencies: + lru-cache "^11.0.0" + minipass "^7.1.2" + +pathe@^2.0.1, pathe@^2.0.2, pathe@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/pathe/-/pathe-2.0.3.tgz#3ecbec55421685b70a9da872b2cff3e1cbed1716" + integrity sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w== + +perfect-debounce@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz#9c2e8bc30b169cc984a58b7d5b28049839591d2a" + integrity sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA== + +picocolors@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.1.1.tgz#3d321af3eab939b083c8f929a1d12cda81c26b6b" + integrity sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA== + +picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.2, picomatch@^2.3.1: + version "2.3.1" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" + integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== + +picomatch@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-4.0.2.tgz#77c742931e8f3b8820946c76cd0c1f13730d1dab" + integrity sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg== + +pinia@^3.0.1: + version "3.0.2" + resolved "https://registry.yarnpkg.com/pinia/-/pinia-3.0.2.tgz#0616c2e1b39915f253c7626db3c81b7cdad695da" + integrity sha512-sH2JK3wNY809JOeiiURUR0wehJ9/gd9qFN2Y828jCbxEzKEmEt0pzCXwqiSTfuRsK9vQsOflSdnbdBOGrhtn+g== + dependencies: + "@vue/devtools-api" "^7.7.2" + +pkg-types@^1.3.0: + version "1.3.1" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-1.3.1.tgz#bd7cc70881192777eef5326c19deb46e890917df" + integrity sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ== + dependencies: + confbox "^0.1.8" + mlly "^1.7.4" + pathe "^2.0.1" + +pkg-types@^2.0.1: + version "2.1.0" + resolved "https://registry.yarnpkg.com/pkg-types/-/pkg-types-2.1.0.tgz#70c9e1b9c74b63fdde749876ee0aa007ea9edead" + integrity sha512-wmJwA+8ihJixSoHKxZJRBQG1oY8Yr9pGLzRmSsNms0iNWyHHAlZCa7mmKiFR10YPZuz/2k169JiS/inOjBCZ2A== + dependencies: + confbox "^0.2.1" + exsolve "^1.0.1" + pathe "^2.0.3" + +possible-typed-array-names@^1.0.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz#93e3582bc0e5426586d9d07b79ee40fc841de4ae" + integrity sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg== + +postcss@^8.4.43, postcss@^8.4.48: + version "8.5.3" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.5.3.tgz#1463b6f1c7fb16fe258736cba29a2de35237eafb" + integrity sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A== + dependencies: + nanoid "^3.3.8" + picocolors "^1.1.1" + source-map-js "^1.2.1" + +pretty-bytes@^5.3.0: + version "5.6.0" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-5.6.0.tgz#356256f643804773c82f64723fe78c92c62beaeb" + integrity sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg== + +pretty-bytes@^6.1.1: + version "6.1.1" + resolved "https://registry.yarnpkg.com/pretty-bytes/-/pretty-bytes-6.1.1.tgz#38cd6bb46f47afbf667c202cfc754bffd2016a3b" + integrity sha512-mQUvGU6aUFQ+rNvTIAcZuWGRT9a6f6Yrg9bHs4ImKF+HZCEK+plBvnAZYSIQztknZF2qnzNtr6F8s0+IuptdlQ== + +process-nextick-args@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + +process@^0.11.10: + version "0.11.10" + resolved "https://registry.yarnpkg.com/process/-/process-0.11.10.tgz#7332300e840161bda3e69a1d1d91a7d4bc16f182" + integrity sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A== + +punycode@^2.1.0: + version "2.3.1" + resolved "https://registry.yarnpkg.com/punycode/-/punycode-2.3.1.tgz#027422e2faec0b25e1549c3e1bd8309b9133b6e5" + integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg== + +quansync@^0.2.8: + version "0.2.10" + resolved "https://registry.yarnpkg.com/quansync/-/quansync-0.2.10.tgz#32053cf166fa36511aae95fc49796116f2dc20e1" + integrity sha512-t41VRkMYbkHyCYmOvx/6URnN80H7k4X0lLdBMGsz+maAwrJQYB1djpV6vHrQIBE0WBSGqhtEHrK9U3DWWH8v7A== + +queue-microtask@^1.2.2: + version "1.2.3" + resolved "https://registry.yarnpkg.com/queue-microtask/-/queue-microtask-1.2.3.tgz#4929228bbc724dfac43e0efb058caf7b6cfb6243" + integrity sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== + +randombytes@^2.1.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/randombytes/-/randombytes-2.1.0.tgz#df6f84372f0270dc65cdf6291349ab7a473d4f2a" + integrity sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== + dependencies: + safe-buffer "^5.1.0" + +readable-stream@^3.0.2: + version "3.6.2" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-3.6.2.tgz#56a9b36ea965c00c5a93ef31eb111a0f11056967" + integrity sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA== + dependencies: + inherits "^2.0.3" + string_decoder "^1.1.1" + util-deprecate "^1.0.1" + +readable-stream@^4.2.0, readable-stream@^4.7.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-4.7.0.tgz#cedbd8a1146c13dfff8dab14068028d58c15ac91" + integrity sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg== + dependencies: + abort-controller "^3.0.0" + buffer "^6.0.3" + events "^3.3.0" + process "^0.11.10" + string_decoder "^1.3.0" + +readdirp@^4.0.1: + version "4.1.2" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-4.1.2.tgz#eb85801435fbf2a7ee58f19e0921b068fc69948d" + integrity sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg== + +readdirp@~3.6.0: + version "3.6.0" + resolved "https://registry.yarnpkg.com/readdirp/-/readdirp-3.6.0.tgz#74a370bd857116e245b29cc97340cd431a02a6c7" + integrity sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA== + dependencies: + picomatch "^2.2.1" + +reflect.getprototypeof@^1.0.6, reflect.getprototypeof@^1.0.9: + version "1.0.10" + resolved "https://registry.yarnpkg.com/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz#c629219e78a3316d8b604c765ef68996964e7bf9" + integrity sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-abstract "^1.23.9" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.7" + get-proto "^1.0.1" + which-builtin-type "^1.2.1" + +regenerate-unicode-properties@^10.2.0: + version "10.2.0" + resolved "https://registry.yarnpkg.com/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz#626e39df8c372338ea9b8028d1f99dc3fd9c3db0" + integrity sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA== + dependencies: + regenerate "^1.4.2" + +regenerate@^1.4.2: + version "1.4.2" + resolved "https://registry.yarnpkg.com/regenerate/-/regenerate-1.4.2.tgz#b9346d8827e8f5a32f7ba29637d398b69014848a" + integrity sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A== + +regexp.prototype.flags@^1.5.3: + version "1.5.4" + resolved "https://registry.yarnpkg.com/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz#1ad6c62d44a259007e55b3970e00f746efbcaa19" + integrity sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA== + dependencies: + call-bind "^1.0.8" + define-properties "^1.2.1" + es-errors "^1.3.0" + get-proto "^1.0.1" + gopd "^1.2.0" + set-function-name "^2.0.2" + +regexpu-core@^6.2.0: + version "6.2.0" + resolved "https://registry.yarnpkg.com/regexpu-core/-/regexpu-core-6.2.0.tgz#0e5190d79e542bf294955dccabae04d3c7d53826" + integrity sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA== + dependencies: + regenerate "^1.4.2" + regenerate-unicode-properties "^10.2.0" + regjsgen "^0.8.0" + regjsparser "^0.12.0" + unicode-match-property-ecmascript "^2.0.0" + unicode-match-property-value-ecmascript "^2.1.0" + +regjsgen@^0.8.0: + version "0.8.0" + resolved "https://registry.yarnpkg.com/regjsgen/-/regjsgen-0.8.0.tgz#df23ff26e0c5b300a6470cad160a9d090c3a37ab" + integrity sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q== + +regjsparser@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/regjsparser/-/regjsparser-0.12.0.tgz#0e846df6c6530586429377de56e0475583b088dc" + integrity sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ== + dependencies: + jsesc "~3.0.2" + +require-from-string@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/require-from-string/-/require-from-string-2.0.2.tgz#89a7fdd938261267318eafe14f9c32e598c36909" + integrity sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw== + +resolve@^1.14.2, resolve@^1.22.1: + version "1.22.10" + resolved "https://registry.yarnpkg.com/resolve/-/resolve-1.22.10.tgz#b663e83ffb09bbf2386944736baae803029b8b39" + integrity sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w== + dependencies: + is-core-module "^2.16.0" + path-parse "^1.0.7" + supports-preserve-symlinks-flag "^1.0.0" + +reusify@^1.0.4: + version "1.1.0" + resolved "https://registry.yarnpkg.com/reusify/-/reusify-1.1.0.tgz#0fe13b9522e1473f51b558ee796e08f11f9b489f" + integrity sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw== + +rfdc@^1.4.1: + version "1.4.1" + resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" + integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== + +rollup@^2.43.1: + version "2.79.2" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-2.79.2.tgz#f150e4a5db4b121a21a747d762f701e5e9f49090" + integrity sha512-fS6iqSPZDs3dr/y7Od6y5nha8dW1YnbgtsyotCVvoFGKbERG++CVRFv1meyGDE1SNItQA8BrnCw7ScdAhRJ3XQ== + optionalDependencies: + fsevents "~2.3.2" + +rollup@^4.20.0: + version "4.40.1" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.40.1.tgz#03d6c53ebb6a9c2c060ae686a61e72a2472b366f" + integrity sha512-C5VvvgCCyfyotVITIAv+4efVytl5F7wt+/I2i9q9GZcEXW9BP52YYOXC58igUi+LFZVHukErIIqQSWwv/M3WRw== + dependencies: + "@types/estree" "1.0.7" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.40.1" + "@rollup/rollup-android-arm64" "4.40.1" + "@rollup/rollup-darwin-arm64" "4.40.1" + "@rollup/rollup-darwin-x64" "4.40.1" + "@rollup/rollup-freebsd-arm64" "4.40.1" + "@rollup/rollup-freebsd-x64" "4.40.1" + "@rollup/rollup-linux-arm-gnueabihf" "4.40.1" + "@rollup/rollup-linux-arm-musleabihf" "4.40.1" + "@rollup/rollup-linux-arm64-gnu" "4.40.1" + "@rollup/rollup-linux-arm64-musl" "4.40.1" + "@rollup/rollup-linux-loongarch64-gnu" "4.40.1" + "@rollup/rollup-linux-powerpc64le-gnu" "4.40.1" + "@rollup/rollup-linux-riscv64-gnu" "4.40.1" + "@rollup/rollup-linux-riscv64-musl" "4.40.1" + "@rollup/rollup-linux-s390x-gnu" "4.40.1" + "@rollup/rollup-linux-x64-gnu" "4.40.1" + "@rollup/rollup-linux-x64-musl" "4.40.1" + "@rollup/rollup-win32-arm64-msvc" "4.40.1" + "@rollup/rollup-win32-ia32-msvc" "4.40.1" + "@rollup/rollup-win32-x64-msvc" "4.40.1" + fsevents "~2.3.2" + +run-parallel@^1.1.9: + version "1.2.0" + resolved "https://registry.yarnpkg.com/run-parallel/-/run-parallel-1.2.0.tgz#66d1368da7bdf921eb9d95bd1a9229e7f21a43ee" + integrity sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA== + dependencies: + queue-microtask "^1.2.2" + +safe-array-concat@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/safe-array-concat/-/safe-array-concat-1.1.3.tgz#c9e54ec4f603b0bbb8e7e5007a5ee7aecd1538c3" + integrity sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + get-intrinsic "^1.2.6" + has-symbols "^1.1.0" + isarray "^2.0.5" + +safe-buffer@^5.1.0, safe-buffer@~5.2.0: + version "5.2.1" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.2.1.tgz#1eaf9fa9bdb1fdd4ec75f58f9cdb4e6b7827eec6" + integrity sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ== + +safe-buffer@~5.1.1: + version "5.1.2" + resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" + integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== + +safe-push-apply@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/safe-push-apply/-/safe-push-apply-1.0.0.tgz#01850e981c1602d398c85081f360e4e6d03d27f5" + integrity sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA== + dependencies: + es-errors "^1.3.0" + isarray "^2.0.5" + +safe-regex-test@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/safe-regex-test/-/safe-regex-test-1.1.0.tgz#7f87dfb67a3150782eaaf18583ff5d1711ac10c1" + integrity sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + is-regex "^1.2.1" + +scule@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/scule/-/scule-1.3.0.tgz#6efbd22fd0bb801bdcc585c89266a7d2daa8fbd3" + integrity sha512-6FtHJEvt+pVMIB9IBY+IcCJ6Z5f1iQnytgyfKMhDKgmzYG+TeH/wx1y3l27rshSbLiSanrR9ffZDrEsmjlQF2g== + +semver@^6.3.1: + version "6.3.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.1.tgz#556d2ef8689146e46dcea4bfdd095f3434dffcb4" + integrity sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA== + +serialize-javascript@^6.0.1: + version "6.0.2" + resolved "https://registry.yarnpkg.com/serialize-javascript/-/serialize-javascript-6.0.2.tgz#defa1e055c83bf6d59ea805d8da862254eb6a6c2" + integrity sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g== + dependencies: + randombytes "^2.1.0" + +set-function-length@^1.2.2: + version "1.2.2" + resolved "https://registry.yarnpkg.com/set-function-length/-/set-function-length-1.2.2.tgz#aac72314198eaed975cf77b2c3b6b880695e5449" + integrity sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + function-bind "^1.1.2" + get-intrinsic "^1.2.4" + gopd "^1.0.1" + has-property-descriptors "^1.0.2" + +set-function-name@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/set-function-name/-/set-function-name-2.0.2.tgz#16a705c5a0dc2f5e638ca96d8a8cd4e1c2b90985" + integrity sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ== + dependencies: + define-data-property "^1.1.4" + es-errors "^1.3.0" + functions-have-names "^1.2.3" + has-property-descriptors "^1.0.2" + +set-proto@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/set-proto/-/set-proto-1.0.0.tgz#0760dbcff30b2d7e801fd6e19983e56da337565e" + integrity sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw== + dependencies: + dunder-proto "^1.0.1" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + +shebang-command@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/shebang-command/-/shebang-command-2.0.0.tgz#ccd0af4f8835fbdc265b82461aaf0c36663f34ea" + integrity sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA== + dependencies: + shebang-regex "^3.0.0" + +shebang-regex@^3.0.0: + version "3.0.0" + resolved "https://registry.yarnpkg.com/shebang-regex/-/shebang-regex-3.0.0.tgz#ae16f1644d873ecad843b0307b143362d4c42172" + integrity sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A== + +side-channel-list@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/side-channel-list/-/side-channel-list-1.0.0.tgz#10cb5984263115d3b7a0e336591e290a830af8ad" + integrity sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + +side-channel-map@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/side-channel-map/-/side-channel-map-1.0.1.tgz#d6bb6b37902c6fef5174e5f533fab4c732a26f42" + integrity sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + +side-channel-weakmap@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz#11dda19d5368e40ce9ec2bdc1fb0ecbc0790ecea" + integrity sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A== + dependencies: + call-bound "^1.0.2" + es-errors "^1.3.0" + get-intrinsic "^1.2.5" + object-inspect "^1.13.3" + side-channel-map "^1.0.1" + +side-channel@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.1.0.tgz#c3fcff9c4da932784873335ec9765fa94ff66bc9" + integrity sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw== + dependencies: + es-errors "^1.3.0" + object-inspect "^1.13.3" + side-channel-list "^1.0.0" + side-channel-map "^1.0.1" + side-channel-weakmap "^1.0.2" + +signal-exit@^4.0.1: + version "4.1.0" + resolved "https://registry.yarnpkg.com/signal-exit/-/signal-exit-4.1.0.tgz#952188c1cbd546070e2dd20d0f41c0ae0530cb04" + integrity sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw== + +smart-buffer@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/smart-buffer/-/smart-buffer-4.2.0.tgz#6e1d71fa4f18c05f7d0ff216dd16a481d0e8d9ae" + integrity sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg== + +smob@^1.0.0: + version "1.5.0" + resolved "https://registry.yarnpkg.com/smob/-/smob-1.5.0.tgz#85d79a1403abf128d24d3ebc1cdc5e1a9548d3ab" + integrity sha512-g6T+p7QO8npa+/hNx9ohv1E5pVCmWrVCUzUXJyLdMmftX6ER0oiWY/w9knEonLpnOp6b6FenKnMfR8gqwWdwig== + +socks@^2.8.3: + version "2.8.4" + resolved "https://registry.yarnpkg.com/socks/-/socks-2.8.4.tgz#07109755cdd4da03269bda4725baa061ab56d5cc" + integrity sha512-D3YaD0aRxR3mEcqnidIs7ReYJFVzWdd6fXJYUM8ixcQcJRGTka/b3saV0KflYhyVJXKhb947GndU35SxYNResQ== + dependencies: + ip-address "^9.0.5" + smart-buffer "^4.2.0" + +source-map-js@^1.2.0, source-map-js@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.1.tgz#1ce5650fddd87abc099eda37dcff024c2667ae46" + integrity sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA== + +source-map-support@~0.5.20: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +source-map@^0.8.0-beta.0: + version "0.8.0-beta.0" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.8.0-beta.0.tgz#d4c1bb42c3f7ee925f005927ba10709e0d1d1f11" + integrity sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA== + dependencies: + whatwg-url "^7.0.0" + +sourcemap-codec@^1.4.8: + version "1.4.8" + resolved "https://registry.yarnpkg.com/sourcemap-codec/-/sourcemap-codec-1.4.8.tgz#ea804bd94857402e6992d05a38ef1ae35a9ab4c4" + integrity sha512-9NykojV5Uih4lgo5So5dtw+f0JgJX30KCNI8gwhz2J9A15wD0Ml6tjHKwf6fTSa6fAdVBdZeNOs9eJ71qCk8vA== + +speakingurl@^14.0.1: + version "14.0.1" + resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" + integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== + +split2@^4.2.0: + version "4.2.0" + resolved "https://registry.yarnpkg.com/split2/-/split2-4.2.0.tgz#c9c5920904d148bab0b9f67145f245a86aadbfa4" + integrity sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg== + +sprintf-js@^1.1.3: + version "1.1.3" + resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.3.tgz#4914b903a2f8b685d17fdf78a70e917e872e444a" + integrity sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA== + +"string-width-cjs@npm:string-width@^4.2.0": + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^4.1.0: + version "4.2.3" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010" + integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g== + dependencies: + emoji-regex "^8.0.0" + is-fullwidth-code-point "^3.0.0" + strip-ansi "^6.0.1" + +string-width@^5.0.1, string-width@^5.1.2: + version "5.1.2" + resolved "https://registry.yarnpkg.com/string-width/-/string-width-5.1.2.tgz#14f8daec6d81e7221d2a357e668cab73bdbca794" + integrity sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA== + dependencies: + eastasianwidth "^0.2.0" + emoji-regex "^9.2.2" + strip-ansi "^7.0.1" + +string.prototype.matchall@^4.0.6: + version "4.0.12" + resolved "https://registry.yarnpkg.com/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz#6c88740e49ad4956b1332a911e949583a275d4c0" + integrity sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.3" + define-properties "^1.2.1" + es-abstract "^1.23.6" + es-errors "^1.3.0" + es-object-atoms "^1.0.0" + get-intrinsic "^1.2.6" + gopd "^1.2.0" + has-symbols "^1.1.0" + internal-slot "^1.1.0" + regexp.prototype.flags "^1.5.3" + set-function-name "^2.0.2" + side-channel "^1.1.0" + +string.prototype.trim@^1.2.10: + version "1.2.10" + resolved "https://registry.yarnpkg.com/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz#40b2dd5ee94c959b4dcfb1d65ce72e90da480c81" + integrity sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-data-property "^1.1.4" + define-properties "^1.2.1" + es-abstract "^1.23.5" + es-object-atoms "^1.0.0" + has-property-descriptors "^1.0.2" + +string.prototype.trimend@^1.0.9: + version "1.0.9" + resolved "https://registry.yarnpkg.com/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz#62e2731272cd285041b36596054e9f66569b6942" + integrity sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ== + dependencies: + call-bind "^1.0.8" + call-bound "^1.0.2" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string.prototype.trimstart@^1.0.8: + version "1.0.8" + resolved "https://registry.yarnpkg.com/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz#7ee834dda8c7c17eff3118472bb35bfedaa34dde" + integrity sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg== + dependencies: + call-bind "^1.0.7" + define-properties "^1.2.1" + es-object-atoms "^1.0.0" + +string_decoder@^1.1.1, string_decoder@^1.3.0: + version "1.3.0" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.3.0.tgz#42f114594a46cf1a8e30b0a84f56c78c3edac21e" + integrity sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA== + dependencies: + safe-buffer "~5.2.0" + +stringify-object@^3.3.0: + version "3.3.0" + resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" + integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== + dependencies: + get-own-enumerable-property-symbols "^3.0.0" + is-obj "^1.0.1" + is-regexp "^1.0.0" + +"strip-ansi-cjs@npm:strip-ansi@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^6.0.0, strip-ansi@^6.0.1: + version "6.0.1" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" + integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A== + dependencies: + ansi-regex "^5.0.1" + +strip-ansi@^7.0.1: + version "7.1.0" + resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-7.1.0.tgz#d5b6568ca689d8561370b0707685d22434faff45" + integrity sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ== + dependencies: + ansi-regex "^6.0.1" + +strip-comments@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/strip-comments/-/strip-comments-2.0.1.tgz#4ad11c3fbcac177a67a40ac224ca339ca1c1ba9b" + integrity sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw== + +superjson@^2.2.2: + version "2.2.2" + resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.2.tgz#9d52bf0bf6b5751a3c3472f1292e714782ba3173" + integrity sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q== + dependencies: + copy-anything "^3.0.2" + +supports-color@^7.1.0: + version "7.2.0" + resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-7.2.0.tgz#1b7dcdcb32b8138801b3e478ba6a51caa89648da" + integrity sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw== + dependencies: + has-flag "^4.0.0" + +supports-preserve-symlinks-flag@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz#6eda4bd344a3c94aea376d4cc31bc77311039e09" + integrity sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w== + +temp-dir@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/temp-dir/-/temp-dir-2.0.0.tgz#bde92b05bdfeb1516e804c9c00ad45177f31321e" + integrity sha512-aoBAniQmmwtcKp/7BzsH8Cxzv8OL736p7v1ihGb5e9DJ9kTwGWHrQrVB5+lfVDzfGrdRzXch+ig7LHaY1JTOrg== + +tempy@^0.6.0: + version "0.6.0" + resolved "https://registry.yarnpkg.com/tempy/-/tempy-0.6.0.tgz#65e2c35abc06f1124a97f387b08303442bde59f3" + integrity sha512-G13vtMYPT/J8A4X2SjdtBTphZlrp1gKv6hZiOjw14RCWg6GbHuQBGtjlx75xLbYV/wEc0D7G5K4rxKP/cXk8Bw== + dependencies: + is-stream "^2.0.0" + temp-dir "^2.0.0" + type-fest "^0.16.0" + unique-string "^2.0.0" + +terser@^5.17.4: + version "5.39.0" + resolved "https://registry.yarnpkg.com/terser/-/terser-5.39.0.tgz#0e82033ed57b3ddf1f96708d123cca717d86ca3a" + integrity sha512-LBAhFyLho16harJoWMg/nZsQYgTrg5jXOn2nCYjRUcZZEdE3qa2zb8QEDRUGVZBW4rlazf2fxkg8tztybTaqWw== + dependencies: + "@jridgewell/source-map" "^0.3.3" + acorn "^8.8.2" + commander "^2.20.0" + source-map-support "~0.5.20" + +tinyglobby@^0.2.10, tinyglobby@^0.2.12: + version "0.2.13" + resolved "https://registry.yarnpkg.com/tinyglobby/-/tinyglobby-0.2.13.tgz#a0e46515ce6cbcd65331537e57484af5a7b2ff7e" + integrity sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw== + dependencies: + fdir "^6.4.4" + picomatch "^4.0.2" + +to-regex-range@^5.0.1: + version "5.0.1" + resolved "https://registry.yarnpkg.com/to-regex-range/-/to-regex-range-5.0.1.tgz#1648c44aae7c8d988a326018ed72f5b4dd0392e4" + integrity sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ== + dependencies: + is-number "^7.0.0" + +tr46@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-1.0.1.tgz#a8b13fd6bfd2489519674ccde55ba3693b706d09" + integrity sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA== + dependencies: + punycode "^2.1.0" + +tslib@^2.6.2: + version "2.8.1" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-2.8.1.tgz#612efe4ed235d567e8aba5f2a5fab70280ade83f" + integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== + +type-fest@^0.16.0: + version "0.16.0" + resolved "https://registry.yarnpkg.com/type-fest/-/type-fest-0.16.0.tgz#3240b891a78b0deae910dbeb86553e552a148860" + integrity sha512-eaBzG6MxNzEn9kiwvtre90cXaNLkmadMWa1zQMs3XORCXNbsH/OewwbxC5ia9dCxIxnTAsSxXJaa/p5y8DlvJg== + +typed-array-buffer@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz#a72395450a4869ec033fd549371b47af3a2ee536" + integrity sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw== + dependencies: + call-bound "^1.0.3" + es-errors "^1.3.0" + is-typed-array "^1.1.14" + +typed-array-byte-length@^1.0.3: + version "1.0.3" + resolved "https://registry.yarnpkg.com/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz#8407a04f7d78684f3d252aa1a143d2b77b4160ce" + integrity sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg== + dependencies: + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.14" + +typed-array-byte-offset@^1.0.4: + version "1.0.4" + resolved "https://registry.yarnpkg.com/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz#ae3698b8ec91a8ab945016108aef00d5bff12355" + integrity sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + for-each "^0.3.3" + gopd "^1.2.0" + has-proto "^1.2.0" + is-typed-array "^1.1.15" + reflect.getprototypeof "^1.0.9" + +typed-array-length@^1.0.7: + version "1.0.7" + resolved "https://registry.yarnpkg.com/typed-array-length/-/typed-array-length-1.0.7.tgz#ee4deff984b64be1e118b0de8c9c877d5ce73d3d" + integrity sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg== + dependencies: + call-bind "^1.0.7" + for-each "^0.3.3" + gopd "^1.0.1" + is-typed-array "^1.1.13" + possible-typed-array-names "^1.0.0" + reflect.getprototypeof "^1.0.6" + +typedarray@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777" + integrity sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA== + +typescript@^5.8.2: + version "5.8.3" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-5.8.3.tgz#92f8a3e5e3cf497356f4178c34cd65a7f5e8440e" + integrity sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ== + +ufo@^1.5.4: + version "1.6.1" + resolved "https://registry.yarnpkg.com/ufo/-/ufo-1.6.1.tgz#ac2db1d54614d1b22c1d603e3aef44a85d8f146b" + integrity sha512-9a4/uxlTWJ4+a5i0ooc1rU7C7YOw3wT+UGqdeNNHWnOF9qcMBgLRS+4IYUqbczewFx4mLEig6gawh7X6mFlEkA== + +unbox-primitive@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/unbox-primitive/-/unbox-primitive-1.1.0.tgz#8d9d2c9edeea8460c7f35033a88867944934d1e2" + integrity sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw== + dependencies: + call-bound "^1.0.3" + has-bigints "^1.0.2" + has-symbols "^1.1.0" + which-boxed-primitive "^1.1.1" + +undici-types@~6.19.2: + version "6.19.8" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.19.8.tgz#35111c9d1437ab83a7cdc0abae2f26d88eda0a02" + integrity sha512-ve2KP6f/JnbPBFyobGHuerC9g1FYGn/F8n1LWTwNxCEzd6IfqTwUQcNXgEtmmQ6DlRrC1hrSrBnCZPokRrDHjw== + +undici-types@~6.21.0: + version "6.21.0" + resolved "https://registry.yarnpkg.com/undici-types/-/undici-types-6.21.0.tgz#691d00af3909be93a7faa13be61b3a5b50ef12cb" + integrity sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ== + +unicode-canonical-property-names-ecmascript@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz#cb3173fe47ca743e228216e4a3ddc4c84d628cc2" + integrity sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg== + +unicode-match-property-ecmascript@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz#54fd16e0ecb167cf04cf1f756bdcc92eba7976c3" + integrity sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q== + dependencies: + unicode-canonical-property-names-ecmascript "^2.0.0" + unicode-property-aliases-ecmascript "^2.0.0" + +unicode-match-property-value-ecmascript@^2.1.0: + version "2.2.0" + resolved "https://registry.yarnpkg.com/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz#a0401aee72714598f739b68b104e4fe3a0cb3c71" + integrity sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg== + +unicode-property-aliases-ecmascript@^2.0.0: + version "2.1.0" + resolved "https://registry.yarnpkg.com/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz#43d41e3be698bd493ef911077c9b131f827e8ccd" + integrity sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w== + +unique-string@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/unique-string/-/unique-string-2.0.0.tgz#39c6451f81afb2749de2b233e3f7c5e8843bd89d" + integrity sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg== + dependencies: + crypto-random-string "^2.0.0" + +universalify@^2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.1.tgz#168efc2180964e6386d061e094df61afe239b18d" + integrity sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw== + +unplugin-utils@^0.2.3, unplugin-utils@^0.2.4: + version "0.2.4" + resolved "https://registry.yarnpkg.com/unplugin-utils/-/unplugin-utils-0.2.4.tgz#56e4029a6906645a10644f8befc404b06d5d24d0" + integrity sha512-8U/MtpkPkkk3Atewj1+RcKIjb5WBimZ/WSLhhR3w6SsIj8XJuKTacSP8g+2JhfSGw0Cb125Y+2zA/IzJZDVbhA== + dependencies: + pathe "^2.0.2" + picomatch "^4.0.2" + +unplugin-vue-components@^28.4.1: + version "28.5.0" + resolved "https://registry.yarnpkg.com/unplugin-vue-components/-/unplugin-vue-components-28.5.0.tgz#33585a24c98939d1abe56bd69217bc7187ba329f" + integrity sha512-o7fMKU/uI8NiP+E0W62zoduuguWqB0obTfHFtbr1AP2uo2lhUPnPttWUE92yesdiYfo9/0hxIrj38FMc1eaySg== + dependencies: + chokidar "^3.6.0" + debug "^4.4.0" + local-pkg "^1.1.1" + magic-string "^0.30.17" + mlly "^1.7.4" + tinyglobby "^0.2.12" + unplugin "^2.3.2" + unplugin-utils "^0.2.4" + +unplugin-vue-router@^0.12.0: + version "0.12.0" + resolved "https://registry.yarnpkg.com/unplugin-vue-router/-/unplugin-vue-router-0.12.0.tgz#416a698871acd054d3479841ba97ea34473f3bfb" + integrity sha512-xjgheKU0MegvXQcy62GVea0LjyOdMxN0/QH+ijN29W62ZlMhG7o7K+0AYqfpprvPwpWtuRjiyC5jnV2SxWye2w== + dependencies: + "@babel/types" "^7.26.8" + "@vue-macros/common" "^1.16.1" + ast-walker-scope "^0.6.2" + chokidar "^4.0.3" + fast-glob "^3.3.3" + json5 "^2.2.3" + local-pkg "^1.0.0" + magic-string "^0.30.17" + micromatch "^4.0.8" + mlly "^1.7.4" + pathe "^2.0.2" + scule "^1.3.0" + unplugin "^2.2.0" + unplugin-utils "^0.2.3" + yaml "^2.7.0" + +unplugin@^2.2.0, unplugin@^2.3.2: + version "2.3.2" + resolved "https://registry.yarnpkg.com/unplugin/-/unplugin-2.3.2.tgz#36c93a1662b70c97a2e2fc45c0e78fa09f7a4984" + integrity sha512-3n7YA46rROb3zSj8fFxtxC/PqoyvYQ0llwz9wtUPUutr9ig09C8gGo5CWCwHrUzlqC1LLR43kxp5vEIyH1ac1w== + dependencies: + acorn "^8.14.1" + picomatch "^4.0.2" + webpack-virtual-modules "^0.6.2" + +upath@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/upath/-/upath-1.2.0.tgz#8f66dbcd55a883acdae4408af8b035a5044c1894" + integrity sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg== + +update-browserslist-db@^1.1.1: + version "1.1.3" + resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz#348377dd245216f9e7060ff50b15a1b740b75420" + integrity sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw== + dependencies: + escalade "^3.2.0" + picocolors "^1.1.1" + +util-deprecate@^1.0.1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" + integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== + +vite-plugin-pwa@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/vite-plugin-pwa/-/vite-plugin-pwa-1.0.0.tgz#bd0ee81e71851bd45cae2f4aec90e52e9833152d" + integrity sha512-X77jo0AOd5OcxmWj3WnVti8n7Kw2tBgV1c8MCXFclrSlDV23ePzv2eTDIALXI2Qo6nJ5pZJeZAuX0AawvRfoeA== + dependencies: + debug "^4.3.6" + pretty-bytes "^6.1.1" + tinyglobby "^0.2.10" + workbox-build "^7.3.0" + workbox-window "^7.3.0" + +vite@^5.4.16: + version "5.4.19" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.4.19.tgz#20efd060410044b3ed555049418a5e7d1998f959" + integrity sha512-qO3aKv3HoQC8QKiNSTuUM1l9o/XX3+c+VTgLHbJWHZGeTPVAg2XwazI9UWzoxjIJCGCV2zU60uqMzjeLZuULqA== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.43" + rollup "^4.20.0" + optionalDependencies: + fsevents "~2.3.3" + +vue-router@^4.5.0: + version "4.5.1" + resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-4.5.1.tgz#47bffe2d3a5479d2886a9a244547a853aa0abf69" + integrity sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw== + dependencies: + "@vue/devtools-api" "^6.6.4" + +vue-toast-notification@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/vue-toast-notification/-/vue-toast-notification-3.1.3.tgz#cdaef11cbe4045f3ea9d3ea5c814fa0333610dd1" + integrity sha512-XNyWqwLIGBFfX5G9sK+clq3N3IPlhDjzNdbZaXkEElcotPlWs0wWZailk1vqhdtLYT/93Y4FHAVuzyatLmPZRA== + +vue@^3.5.13: + version "3.5.13" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.13.tgz#9f760a1a982b09c0c04a867903fc339c9f29ec0a" + integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ== + dependencies: + "@vue/compiler-dom" "3.5.13" + "@vue/compiler-sfc" "3.5.13" + "@vue/runtime-dom" "3.5.13" + "@vue/server-renderer" "3.5.13" + "@vue/shared" "3.5.13" + +webidl-conversions@^4.0.2: + version "4.0.2" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-4.0.2.tgz#a855980b1f0b6b359ba1d5d9fb39ae941faa63ad" + integrity sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg== + +webpack-virtual-modules@^0.6.2: + version "0.6.2" + resolved "https://registry.yarnpkg.com/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz#057faa9065c8acf48f24cb57ac0e77739ab9a7e8" + integrity sha512-66/V2i5hQanC51vBQKPH4aI8NMAcBW59FVBs+rC7eGHupMyfn34q7rZIE+ETlJ+XTevqfUhVVBgSUNSW2flEUQ== + +whatwg-url@^7.0.0: + version "7.1.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-7.1.0.tgz#c2c492f1eca612988efd3d2266be1b9fc6170d06" + integrity sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg== + dependencies: + lodash.sortby "^4.7.0" + tr46 "^1.0.1" + webidl-conversions "^4.0.2" + +which-boxed-primitive@^1.1.0, which-boxed-primitive@^1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz#d76ec27df7fa165f18d5808374a5fe23c29b176e" + integrity sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA== + dependencies: + is-bigint "^1.1.0" + is-boolean-object "^1.2.1" + is-number-object "^1.1.1" + is-string "^1.1.1" + is-symbol "^1.1.1" + +which-builtin-type@^1.2.1: + version "1.2.1" + resolved "https://registry.yarnpkg.com/which-builtin-type/-/which-builtin-type-1.2.1.tgz#89183da1b4907ab089a6b02029cc5d8d6574270e" + integrity sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q== + dependencies: + call-bound "^1.0.2" + function.prototype.name "^1.1.6" + has-tostringtag "^1.0.2" + is-async-function "^2.0.0" + is-date-object "^1.1.0" + is-finalizationregistry "^1.1.0" + is-generator-function "^1.0.10" + is-regex "^1.2.1" + is-weakref "^1.0.2" + isarray "^2.0.5" + which-boxed-primitive "^1.1.0" + which-collection "^1.0.2" + which-typed-array "^1.1.16" + +which-collection@^1.0.2: + version "1.0.2" + resolved "https://registry.yarnpkg.com/which-collection/-/which-collection-1.0.2.tgz#627ef76243920a107e7ce8e96191debe4b16c2a0" + integrity sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw== + dependencies: + is-map "^2.0.3" + is-set "^2.0.3" + is-weakmap "^2.0.2" + is-weakset "^2.0.3" + +which-typed-array@^1.1.16, which-typed-array@^1.1.18: + version "1.1.19" + resolved "https://registry.yarnpkg.com/which-typed-array/-/which-typed-array-1.1.19.tgz#df03842e870b6b88e117524a4b364b6fc689f956" + integrity sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw== + dependencies: + available-typed-arrays "^1.0.7" + call-bind "^1.0.8" + call-bound "^1.0.4" + for-each "^0.3.5" + get-proto "^1.0.1" + gopd "^1.2.0" + has-tostringtag "^1.0.2" + +which@^2.0.1: + version "2.0.2" + resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1" + integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA== + dependencies: + isexe "^2.0.0" + +workbox-background-sync@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-background-sync/-/workbox-background-sync-7.3.0.tgz#b6340731a8d5b42b9e75a8a87c8806928e6e6303" + integrity sha512-PCSk3eK7Mxeuyatb22pcSx9dlgWNv3+M8PqPaYDokks8Y5/FX4soaOqj3yhAZr5k6Q5JWTOMYgaJBpbw11G9Eg== + dependencies: + idb "^7.0.1" + workbox-core "7.3.0" + +workbox-broadcast-update@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-broadcast-update/-/workbox-broadcast-update-7.3.0.tgz#bff86b91795c4b9fa46a758d1a7a151828623280" + integrity sha512-T9/F5VEdJVhwmrIAE+E/kq5at2OY6+OXXgOWQevnubal6sO92Gjo24v6dCVwQiclAF5NS3hlmsifRrpQzZCdUA== + dependencies: + workbox-core "7.3.0" + +workbox-build@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-build/-/workbox-build-7.3.0.tgz#ab688f3241b32862236aeeb62b240195f1fe4b62" + integrity sha512-JGL6vZTPlxnlqZRhR/K/msqg3wKP+m0wfEUVosK7gsYzSgeIxvZLi1ViJJzVL7CEeI8r7rGFV973RiEqkP3lWQ== + dependencies: + "@apideck/better-ajv-errors" "^0.3.1" + "@babel/core" "^7.24.4" + "@babel/preset-env" "^7.11.0" + "@babel/runtime" "^7.11.2" + "@rollup/plugin-babel" "^5.2.0" + "@rollup/plugin-node-resolve" "^15.2.3" + "@rollup/plugin-replace" "^2.4.1" + "@rollup/plugin-terser" "^0.4.3" + "@surma/rollup-plugin-off-main-thread" "^2.2.3" + ajv "^8.6.0" + common-tags "^1.8.0" + fast-json-stable-stringify "^2.1.0" + fs-extra "^9.0.1" + glob "^7.1.6" + lodash "^4.17.20" + pretty-bytes "^5.3.0" + rollup "^2.43.1" + source-map "^0.8.0-beta.0" + stringify-object "^3.3.0" + strip-comments "^2.0.1" + tempy "^0.6.0" + upath "^1.2.0" + workbox-background-sync "7.3.0" + workbox-broadcast-update "7.3.0" + workbox-cacheable-response "7.3.0" + workbox-core "7.3.0" + workbox-expiration "7.3.0" + workbox-google-analytics "7.3.0" + workbox-navigation-preload "7.3.0" + workbox-precaching "7.3.0" + workbox-range-requests "7.3.0" + workbox-recipes "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + workbox-streams "7.3.0" + workbox-sw "7.3.0" + workbox-window "7.3.0" + +workbox-cacheable-response@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-cacheable-response/-/workbox-cacheable-response-7.3.0.tgz#557b0f5fdfceb22fe243e3f19807c76a0ae646e3" + integrity sha512-eAFERIg6J2LuyELhLlmeRcJFa5e16Mj8kL2yCDbhWE+HUun9skRQrGIFVUagqWj4DMaaPSMWfAolM7XZZxNmxA== + dependencies: + workbox-core "7.3.0" + +workbox-core@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-core/-/workbox-core-7.3.0.tgz#f24fb92041a0b7482fe2dd856544aaa9fa105248" + integrity sha512-Z+mYrErfh4t3zi7NVTvOuACB0A/jA3bgxUN3PwtAVHvfEsZxV9Iju580VEETug3zYJRc0Dmii/aixI/Uxj8fmw== + +workbox-expiration@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-expiration/-/workbox-expiration-7.3.0.tgz#2c1ee1fdada34aa7e7474f706d5429c914bd10d2" + integrity sha512-lpnSSLp2BM+K6bgFCWc5bS1LR5pAwDWbcKt1iL87/eTSJRdLdAwGQznZE+1czLgn/X05YChsrEegTNxjM067vQ== + dependencies: + idb "^7.0.1" + workbox-core "7.3.0" + +workbox-google-analytics@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-google-analytics/-/workbox-google-analytics-7.3.0.tgz#3c4d4956c0a9800dfb587d82ec8bc0f9cf963791" + integrity sha512-ii/tSfFdhjLHZ2BrYgFNTrb/yk04pw2hasgbM70jpZfLk0vdJAXgaiMAWsoE+wfJDNWoZmBYY0hMVI0v5wWDbg== + dependencies: + workbox-background-sync "7.3.0" + workbox-core "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-navigation-preload@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-navigation-preload/-/workbox-navigation-preload-7.3.0.tgz#9d54693b9179d5175e66af5ef9a92d1b7cf3e605" + integrity sha512-fTJzogmFaTv4bShZ6aA7Bfj4Cewaq5rp30qcxl2iYM45YD79rKIhvzNHiFj1P+u5ZZldroqhASXwwoyusnr2cg== + dependencies: + workbox-core "7.3.0" + +workbox-precaching@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-precaching/-/workbox-precaching-7.3.0.tgz#a84663d69efdb334f25c04dba0a72ed3391c4da8" + integrity sha512-ckp/3t0msgXclVAYaNndAGeAoWQUv7Rwc4fdhWL69CCAb2UHo3Cef0KIUctqfQj1p8h6aGyz3w8Cy3Ihq9OmIw== + dependencies: + workbox-core "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-range-requests@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-range-requests/-/workbox-range-requests-7.3.0.tgz#1b3d5c235a0ff5271418c3a7183281dc131ccd0d" + integrity sha512-EyFmM1KpDzzAouNF3+EWa15yDEenwxoeXu9bgxOEYnFfCxns7eAxA9WSSaVd8kujFFt3eIbShNqa4hLQNFvmVQ== + dependencies: + workbox-core "7.3.0" + +workbox-recipes@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-recipes/-/workbox-recipes-7.3.0.tgz#fa407101e8ce52850dfba8e17a5afccb733a3942" + integrity sha512-BJro/MpuW35I/zjZQBcoxsctgeB+kyb2JAP5EB3EYzePg8wDGoQuUdyYQS+CheTb+GhqJeWmVs3QxLI8EBP1sg== + dependencies: + workbox-cacheable-response "7.3.0" + workbox-core "7.3.0" + workbox-expiration "7.3.0" + workbox-precaching "7.3.0" + workbox-routing "7.3.0" + workbox-strategies "7.3.0" + +workbox-routing@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-routing/-/workbox-routing-7.3.0.tgz#fc86296bc1155c112ee2c16b3180853586c30208" + integrity sha512-ZUlysUVn5ZUzMOmQN3bqu+gK98vNfgX/gSTZ127izJg/pMMy4LryAthnYtjuqcjkN4HEAx1mdgxNiKJMZQM76A== + dependencies: + workbox-core "7.3.0" + +workbox-strategies@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-strategies/-/workbox-strategies-7.3.0.tgz#bb1530f205806895aacdea3639e6cf6bfb3a6cb0" + integrity sha512-tmZydug+qzDFATwX7QiEL5Hdf7FrkhjaF9db1CbB39sDmEZJg3l9ayDvPxy8Y18C3Y66Nrr9kkN1f/RlkDgllg== + dependencies: + workbox-core "7.3.0" + +workbox-streams@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-streams/-/workbox-streams-7.3.0.tgz#a4c0ae51b66121a2aa6f89229e237aca6dc27eb5" + integrity sha512-SZnXucyg8x2Y61VGtDjKPO5EgPUG5NDn/v86WYHX+9ZqvAsGOytP0Jxp1bl663YUuMoXSAtsGLL+byHzEuMRpw== + dependencies: + workbox-core "7.3.0" + workbox-routing "7.3.0" + +workbox-sw@7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-sw/-/workbox-sw-7.3.0.tgz#39215017e868d7cfe6835b2961f55369d89b3e73" + integrity sha512-aCUyoAZU9IZtH05mn0ACUpyHzPs0lMeJimAYkQkBsOWiqaJLgusfDCR+yllkPkFRxWpZKF8vSvgHYeG7LwhlmA== + +workbox-window@7.3.0, workbox-window@^7.3.0: + version "7.3.0" + resolved "https://registry.yarnpkg.com/workbox-window/-/workbox-window-7.3.0.tgz#e71bb0b4d880d2295c96bf1ccadb6cea0df51c07" + integrity sha512-qW8PDy16OV1UBaUNGlTVcepzrlzyzNW/ZJvFQQs2j2TzGsg6IKjcpZC1RSquqQnTOafl5pCj5bGfAHlCjOOjdA== + dependencies: + "@types/trusted-types" "^2.0.2" + workbox-core "7.3.0" + +worker-timers-broker@^6.1.8: + version "6.1.8" + resolved "https://registry.yarnpkg.com/worker-timers-broker/-/worker-timers-broker-6.1.8.tgz#08f64e5931b77fadc55f0c7388c077a7dd17e4c7" + integrity sha512-FUCJu9jlK3A8WqLTKXM9E6kAmI/dR1vAJ8dHYLMisLNB/n3GuaFIjJ7pn16ZcD1zCOf7P6H62lWIEBi+yz/zQQ== + dependencies: + "@babel/runtime" "^7.24.5" + fast-unique-numbers "^8.0.13" + tslib "^2.6.2" + worker-timers-worker "^7.0.71" + +worker-timers-worker@^7.0.71: + version "7.0.71" + resolved "https://registry.yarnpkg.com/worker-timers-worker/-/worker-timers-worker-7.0.71.tgz#f96138bafbcfaabea116603ce23956e05e76db6a" + integrity sha512-ks/5YKwZsto1c2vmljroppOKCivB/ma97g9y77MAAz2TBBjPPgpoOiS1qYQKIgvGTr2QYPT3XhJWIB6Rj2MVPQ== + dependencies: + "@babel/runtime" "^7.24.5" + tslib "^2.6.2" + +worker-timers@^7.1.8: + version "7.1.8" + resolved "https://registry.yarnpkg.com/worker-timers/-/worker-timers-7.1.8.tgz#f53072c396ac4264fd3027914f4ab793c92d90be" + integrity sha512-R54psRKYVLuzff7c1OTFcq/4Hue5Vlz4bFtNEIarpSiCYhpifHU3aIQI29S84o1j87ePCYqbmEJPqwBTf+3sfw== + dependencies: + "@babel/runtime" "^7.24.5" + tslib "^2.6.2" + worker-timers-broker "^6.1.8" + worker-timers-worker "^7.0.71" + +"wrap-ansi-cjs@npm:wrap-ansi@^7.0.0": + version "7.0.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-7.0.0.tgz#67e145cff510a6a6984bdf1152911d69d2eb9e43" + integrity sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q== + dependencies: + ansi-styles "^4.0.0" + string-width "^4.1.0" + strip-ansi "^6.0.0" + +wrap-ansi@^8.1.0: + version "8.1.0" + resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-8.1.0.tgz#56dc22368ee570face1b49819975d9b9a5ead214" + integrity sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ== + dependencies: + ansi-styles "^6.1.0" + string-width "^5.0.1" + strip-ansi "^7.0.1" + +wrappy@1: + version "1.0.2" + resolved "https://registry.yarnpkg.com/wrappy/-/wrappy-1.0.2.tgz#b5243d8f3ec1aa35f1364605bc0d1036e30ab69f" + integrity sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ== + +ws@^8.18.0: + version "8.18.1" + resolved "https://registry.yarnpkg.com/ws/-/ws-8.18.1.tgz#ea131d3784e1dfdff91adb0a4a116b127515e3cb" + integrity sha512-RKW2aJZMXeMxVpnZ6bck+RswznaxmzdULiBr6KY7XkTnW8uvt0iT9H5DkHUChXrc+uurzwa0rVI16n/Xzjdz1w== + +yallist@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/yallist/-/yallist-3.1.1.tgz#dbb7daf9bfd8bac9ab45ebf602b8cbad0d5d08fd" + integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g== + +yaml@^2.7.0: + version "2.7.1" + resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.7.1.tgz#44a247d1b88523855679ac7fa7cda6ed7e135cf6" + integrity sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==