diff --git a/.github/helper/install.sh b/.github/helper/install.sh index dc15254..cb2becc 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -50,9 +50,11 @@ sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile bench get-app hrms --branch "${BRANCH_NAME}" --skip-assets --overwrite bench get-app erpnext --branch "${BRANCH_NAME}" --skip-assets --overwrite +bench get-app payments --branch "${BRANCH_NAME}" --skip-assets --overwrite +bench get-app webshop --branch "${BRANCH_NAME}" --skip-assets --overwrite bench get-app inventory_tools "${GITHUB_WORKSPACE}" --skip-assets -printf '%s\n' 'frappe' 'erpnext' 'hrms' 'inventory_tools' > ~/frappe-bench/sites/apps.txt +printf '%s\n' 'frappe' 'erpnext' 'hrms' 'payments' 'webshop' 'inventory_tools' > ~/frappe-bench/sites/apps.txt bench setup requirements --python bench use test_site @@ -66,5 +68,4 @@ echo "SITE LIST-APPS:" bench list-apps bench start &> bench_run_logs.txt & -CI=Yes & -bench execute 'inventory_tools.tests.setup.before_test' +CI=Yes diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index d702d91..ca94bb4 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -30,7 +30,7 @@ jobs: run: pip install mypy - name: Install mypy types - run: mypy ./inventory_tools/. --install-types + run: mypy ./inventory_tools/. --install-types --non-interactive - name: Run mypy uses: sasanquaneuf/mypy-github-action@releases/v1 diff --git a/.github/workflows/overrides.yaml b/.github/workflows/overrides.yaml index 9e90f0c..472e415 100644 --- a/.github/workflows/overrides.yaml +++ b/.github/workflows/overrides.yaml @@ -2,6 +2,9 @@ name: Track Overrides on: pull_request: + branches: + - version-14 + - version-15 jobs: track_overrides: diff --git a/.github/workflows/pytest.yaml b/.github/workflows/pytest.yaml index ba1dc5a..4d260a3 100644 --- a/.github/workflows/pytest.yaml +++ b/.github/workflows/pytest.yaml @@ -87,6 +87,11 @@ jobs: MYSQL_PWD: 'admin' run: bash ${{ github.workspace }}/.github/helper/install.sh + - name: Install Test Fixtures + working-directory: /home/runner/frappe-bench + run: | + bench execute 'inventory_tools.tests.setup.before_test' + - name: Run Tests working-directory: /home/runner/frappe-bench run: | diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6250678..c3464db 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,7 +20,7 @@ repos: - id: debug-statements - repo: https://github.com/asottile/pyupgrade - rev: v2.34.0 + rev: v3.17.0 hooks: - id: pyupgrade args: ['--py310-plus'] @@ -31,22 +31,16 @@ repos: - id: black - repo: https://github.com/PyCQA/flake8 - rev: 5.0.4 + rev: 7.1.1 hooks: - id: flake8 additional_dependencies: ['flake8-bugbear'] - - repo: https://github.com/pre-commit/mirrors-mypy - rev: v1.5.1 - hooks: - - id: mypy - exclude: ^tests/ - args: [--ignore-missing-imports] - - repo: https://github.com/agritheory/test_utils - rev: v0.14.0 + rev: v0.15.0 hooks: - id: update_pre_commit_config + - id: mypy - id: validate_copyright files: '\.(js|ts|py|md)$' args: ['--app', 'inventory_tools'] diff --git a/CHANGELOG.md b/CHANGELOG.md index 68ac4f8..1a2af05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,709 +1,712 @@ -# CHANGELOG - - -## v14.6.0 (2024-05-13) - -### Chore - -* chore: update quotation demand docs (#77) ([`f168079`](https://github.com/agritheory/inventory_tools/commit/f168079e29d99056d4c5eb2321d84957cff84f3b)) - -### Feature - -* feat: add inventory tools settings to boot (#81) ([`7ac2e15`](https://github.com/agritheory/inventory_tools/commit/7ac2e15aa74676ba5741887231814cdf9e675755)) - - -## v14.5.0 (2024-04-29) - -### Feature - -* feat: allow user to bulk upload items with quantity to shopping cart (#70) - -* feat: allow user to bulk upload items with quantity to shoping cart - -* nit: variable and syntactical changes - -* fix: removed line by line order placing, optimised code, added enqueue - -* fix: minor cleanup - ---------- - -Co-authored-by: Rohan Bansal <rohan@agritheory.dev> ([`748a9bc`](https://github.com/agritheory/inventory_tools/commit/748a9bc85034094dcd60544fe2b822b67322fb44)) - -### Unknown - -* Aggregate and/or Split Quotations into multiple Sales Orders (#65) - -* feat: Quotation Demand - -* feat: hook - -* feat: settings for quotation demand - -* feat: fixes, SO validate_warehouse - -* feat: quotation demand tests - -* feat: documentation - -* feat: improvements - -* feat: improve tests - -* feat: total selected, price, draft so - -* feat: tests ([`6eceeb5`](https://github.com/agritheory/inventory_tools/commit/6eceeb5623856d659db1ffc3dd7a3ce352cd8000)) - - -## v14.4.0 (2024-04-25) - -### Ci - -* ci: add black to ci (#61) - -* ci: add black to ci - -* chore: black ([`db5a742`](https://github.com/agritheory/inventory_tools/commit/db5a7427f068d7005f8f07125d81f1430c49eb9b)) - -### Feature - -* feat: aggregate POs (#71) ([`5ff8fee`](https://github.com/agritheory/inventory_tools/commit/5ff8fee2ef356990e2dcda0d2db76d1b856a0685)) - -### Unknown - -* Material Demand Test (#64) - -* wip: material demand tests - -* test: material demand aggregation tests and fixes with and without warehouse - -* test: material demand tests and bug fixes - -* chore: add mypy types isntall - -* fix: remove duplicate function - -* tests: add pytest runner file - -* tests: add helper files - -* tests: correct app name - -* ci: fix site_config - -* ci: update action version to use node 20 - -* ci: hrms get-app - -* ci: add base branch sensitvie get-app - -* ci: add hrms to install - -* ci: add node 20 versions - -* ci: remove hrms from install-apps - -* ci: get-app for hrms - -* ci: update prettier and changed-files actions - -* ci: ad dhrms to apps.txt - -* ci: get-app for hrms again - -* ci: get-app for hrms only - -* ci: overrite erpnext install (hrms is tryign to install develop branch) - -* ci: add app name - -* ci: add repo to required apps - -* ci: try install hrms first with overwrite - -* ci: remove "install_apps" key from site_config - -* ci: remove required apps - -* feat: use query builder - -* ci: remove resolve deps - -* ci: fix install apps - -* test: fix manufacturing capacity test and report - -* test: cleanup uom test - -* test: fix error message assert - -* test: add ordering - -* fix: test - -* fix: use frappe function - -* fix: use frappe function - ---------- - -Co-authored-by: fproldan <franciscoproldan@gmail.com> ([`6f4f63f`](https://github.com/agritheory/inventory_tools/commit/6f4f63f22f3f7278e20c6aba9e5ef6a021ae03b2)) - -* Manufacturing capacity report (#57) - -* ci: update app in str(file path) code to more exact matching - -* chore: remove unused code - -* feat: add manufacturing capacity report - -* tests: start manufacturing capacity report tests - -* chore: uncomment formatting code - -* docs: add manufacturing capacity report documentation - -* fix: div by zero in parts can build calc - -* docs: update for calculation differences - -* fix: in stock qty to zero if none from query ([`2936cde`](https://github.com/agritheory/inventory_tools/commit/2936cde9aac6583edeb383d94d74142461cf243a)) - - -## v14.3.0 (2024-03-01) - -### Feature - -* feat: Alternative workstation in job card and operation (#56) - -* feat: New field and link filters to select alternative workstation - -* fix: add alternativ workstation in fixture - -* fix:change alternative workstation in fixture - -* change to error key function - -* fix: set validation on workstation if not operation - -* changes field name in testcase - -* fix: change a class base function to saperate function - -* added a searchfield in query - -* added a searchfield in query - -* added a searchfield in query - -* added a searchfield in query - -* added a searchfield in query - -* feat: validation to not allow default workstation in alternative - -* added comment on function ([`0788d3c`](https://github.com/agritheory/inventory_tools/commit/0788d3caa67ad7820a5a3cdfdc950c85bd6f0cd7)) - -### Unknown - -* Prompt Material Transfer upon Completion of Manufacturing Stock Entry (#51) - -* feat: wip stock entry next action - -* feat: improve message - -* fix: on_submit hook - -* fix: parameter hardcoded - -* feat: change wording ([`d1e126b`](https://github.com/agritheory/inventory_tools/commit/d1e126b8dbaf09370f6d4c173e24af33ac994bd7)) - - -## v14.2.0 (2024-02-01) - -### Feature - -* feat: manufacturing over/under production - -* feat: WIP Manufacturing Over/Under Production - -* feat: WIP Manufacturing Over/Under Production - -* feat: WIP Manufacturing Over/Under Production - -* fix: unused import - -* feat: override onload for work order - -* feat: oveeride get_pending_raw_materials - -* fix: allowed_qty in job card - -* fix: indentation - -* fix: import - -* fixes - -* wip: tests - -* feat: test_get_allowance_percentage - -* feat: test test_validate_finished_goods - -* fix: validate_job_card and test - -* fix: test ([`baeb469`](https://github.com/agritheory/inventory_tools/commit/baeb4690d42429a062595eb9058ddd2770b190c9)) - -### Unknown - -* Make Creation of Job Card(s) on Submit of Work Order configurable (#49) - -* feat: configurable creation of job card - -* feat: configurable creation of job card ([`5bf9486`](https://github.com/agritheory/inventory_tools/commit/5bf94860c58d81faccb1106828f16121c081871a)) - - -## v14.1.3 (2024-01-04) - -### Fix - -* fix: validate customizations (#35) - -* fix: validate customizations - -* fix: only install inventory tools customizations ([`e1d86a0`](https://github.com/agritheory/inventory_tools/commit/e1d86a0739e56f3b987b599c275644ee5c29fc0a)) - - -## v14.1.2 (2023-09-12) - -### Chore - -* chore: remove console.log ([`f82b590`](https://github.com/agritheory/inventory_tools/commit/f82b590d061608818170092f07e3da8d9153b756)) - -### Ci - -* ci: update release action user and email (#32) ([`4264bdd`](https://github.com/agritheory/inventory_tools/commit/4264bdde25c0bff1390923e7f18fce7c353844db)) - -### Fix - -* fix: make uom enforcement respond better to toggle on/off ([`3734cb4`](https://github.com/agritheory/inventory_tools/commit/3734cb42e880168bb4c4a71c67820da63857e0bf)) - -### Unknown - -* Merge pull request #33 from agritheory/uom_enforcement_fix - -fix: make uom enforcement respond better to toggle on/off ([`13ad883`](https://github.com/agritheory/inventory_tools/commit/13ad8832a346799563e0f7ae2f1b20c457d10d5c)) - - -## v14.1.1 (2023-08-30) - -### Fix - -* fix: add is_subcontracted check for additional validation/submit/cancel code ([`8182dac`](https://github.com/agritheory/inventory_tools/commit/8182dacfd35d2a2ee4c5ee94211cbfc7ac00abfd)) - -### Unknown - -* Merge pull request #30 from agritheory/fix_subc_validation - -fix: add is_subcontracted check for additional validation/submit/cancel code ([`c4e7b83`](https://github.com/agritheory/inventory_tools/commit/c4e7b83fd1700d08e8f711e10f1a77c81da0de32)) - - -## v14.1.0 (2023-08-24) - -### Chore - -* chore: update test data for erpnext codebase changes (#24) ([`b7dfc02`](https://github.com/agritheory/inventory_tools/commit/b7dfc02e228c4ebdca86000dac65d1a903640b15)) - -### Ci - -* ci: update remote name ([`021b8a4`](https://github.com/agritheory/inventory_tools/commit/021b8a47355cb9028bac7ac8e60d224247d5b0e8)) - -* ci: update version number ([`9d82150`](https://github.com/agritheory/inventory_tools/commit/9d821506af9d6eb72e3bfb1345ac652aa86896dc)) - -* ci: add python semantic release ([`703803d`](https://github.com/agritheory/inventory_tools/commit/703803df7949fb0cb43699482425ec31595dd1b9)) - -### Documentation - -* docs: update material demand section for expanded functionality ([`1af88f6`](https://github.com/agritheory/inventory_tools/commit/1af88f6d06fdc8e6908381f9dd9011fe470dd9d7)) - -### Feature - -* feat: select email template ([`02196e3`](https://github.com/agritheory/inventory_tools/commit/02196e37c7234982c5dafda9e814117d374565a9)) - -### Fix - -* fix: blank email template for PO; skip supplier-only rows for RFQ ([`2a0e8cd`](https://github.com/agritheory/inventory_tools/commit/2a0e8cd6c6aefdd1a0d7b978652f97ddc755523f)) - -* fix: fix JS after adding draft PO column (#26) ([`e0d3229`](https://github.com/agritheory/inventory_tools/commit/e0d322950e87dcf09f13441859e273a0e44a1192)) - -### Unknown - -* Merge pull request #23 from agritheory/issue_19 - -Allow Creation of RFQ from Material Demand report ([`8a0e350`](https://github.com/agritheory/inventory_tools/commit/8a0e35024968ce5a0f6da05e71f62c091bf2b38f)) - -* Work Order Subcontracting (#13) - -* tests: update test data for additional manufacturing workflow - -* feat: work order subcontracting validations - -* tests: add valuation rate for subcontracted item - -* feat: start wo subcontracting feat - -* feat: make subcontracting section visible by check and settings - -* feat: add ste detail paid field and new pi cols - -* feat: setup hooks and custom po - -* tests: update data for default supplier and price lists - -* feat: include doctype js - -* feat: add work order customizations - -* feat: add purchase order customizations - -* feat: update purchase invoice customizations - -* feat: remove unused code blocks - -* fix: add module to json - -* fix: update custom doc path - -* feat: consolidate custom PI code and modularize class functions - -* feat: combine and refactor PO code - -* feat: update server function paths - -* feat: add BOM field default - -* feat: update to use BOM field vs Item for is_subcontracted - -* feat: code cleanup and refactoring for BOM field - -* feat: update for uom conversion and new svc item - -* fix: move UOM conversions to item - -* add todos in JS - -* feat: rewire item adjustments for conversion factor - -* wip: integrate with production plan - -* feat: add supplier field in WO, allow selection of supplier in dialog - -* chore: add comment explaining precision code - -* wip: subcontractor workflow - -* feat: subcontracting workflow with correct warehouses - -* feat: show/hide subcontracting columns - -* feat: colorize fetach stock entries button - -* fix: text artifacts, new PO errors - -* feat: fetch supplier warehouse, added connectiosn to PI and PO from WO - -* feat: add filters and looks to both PI and PO - -* fix: monkey patch validate_item_details - -* feat: remove buttons and update stockfield in WO subc workflow - -* Enforce UOMs to those that exist in the Item's conversion detail (#27) - -* wip: uom restricted query - -* feat: refactor UOM enforcement validation to be hookable - -* docs: add docs for UOM enforcement - -* tests: fix test logger problem, add xfail uom test - -* Warehouse path (#25) - -* wip: warehouse path - -* wip: warehouse path - -* wip: warehouse path feature - -* feat: warehouse path builder - -* feat: undo query when not configured; setup tweaks - -* chore: update test data for erpnext codebase changes (#24) - -* wip: warehouse path feature - -* wip: test setup - -* chore: update yarn - -* tests: trying to defaeat logger problem - -* test: fix conftest logger issue - -* docs: add docs for warehouse path - -* chore: union types for whitelisted function - ---------- - -Co-authored-by: Heather Kusmierz <heather.kusmierz@gmail.com> - -* tests: test cadence (#28) - -* fix: no cancelled PO in se query, code clean up - -* chore: add comment to explain monkey patch rationale - ---------- - -Co-authored-by: Tyler Matteson <tyler@agritheory.com> ([`ac11c1d`](https://github.com/agritheory/inventory_tools/commit/ac11c1df4daad2189916ca9841480aa1796e42e3)) - -* Merge branch 'version-14' into issue_19 ([`ee0a27f`](https://github.com/agritheory/inventory_tools/commit/ee0a27f0f03a8e3c9e9fa54011d02e44554b8bbb)) - -* Documentation (#29) - -* docs: add index page - -* docs: add screen shots and workflow - -* docs: add screen shots, text edits - -* docs: add example data page - -* docs: add placeholder pages - -* docs: add subcontracting via WO section - -* docs: edits, conform text conventions ([`2fc980d`](https://github.com/agritheory/inventory_tools/commit/2fc980d41105759cffa33e610b779b6d464cf24c)) - -* tests: test cadence (#28) ([`6b5bd47`](https://github.com/agritheory/inventory_tools/commit/6b5bd47089fb2f8a09159635e618425743cc9dff)) - -* Warehouse path (#25) - -* wip: warehouse path - -* wip: warehouse path - -* wip: warehouse path feature - -* feat: warehouse path builder - -* feat: undo query when not configured; setup tweaks - -* chore: update test data for erpnext codebase changes (#24) - -* wip: warehouse path feature - -* wip: test setup - -* chore: update yarn - -* tests: trying to defaeat logger problem - -* test: fix conftest logger issue - -* docs: add docs for warehouse path - -* chore: union types for whitelisted function - ---------- - -Co-authored-by: Heather Kusmierz <heather.kusmierz@gmail.com> ([`370dd6f`](https://github.com/agritheory/inventory_tools/commit/370dd6f9789156ebcbbe7b111d891a74731a477b)) - -* Enforce UOMs to those that exist in the Item's conversion detail (#27) - -* wip: uom restricted query - -* feat: refactor UOM enforcement validation to be hookable - -* docs: add docs for UOM enforcement - -* tests: fix test logger problem, add xfail uom test ([`d4c145a`](https://github.com/agritheory/inventory_tools/commit/d4c145a94d8402fa441619289b4cc6438b7d5c45)) - -* Documentation (#29) - -* docs: add index page - -* docs: add screen shots and workflow - -* docs: add screen shots, text edits - -* docs: add example data page - -* docs: add placeholder pages - -* docs: add subcontracting via WO section - -* docs: edits, conform text conventions ([`198e110`](https://github.com/agritheory/inventory_tools/commit/198e110ab0c5461b9c46925fc05af351151abd38)) - -* tests: test cadence (#28) ([`b91e024`](https://github.com/agritheory/inventory_tools/commit/b91e0246f6e384f40685af40d3a249daa9d03a8c)) - -* Warehouse path (#25) - -* wip: warehouse path - -* wip: warehouse path - -* wip: warehouse path feature - -* feat: warehouse path builder - -* feat: undo query when not configured; setup tweaks - -* chore: update test data for erpnext codebase changes (#24) - -* wip: warehouse path feature - -* wip: test setup - -* chore: update yarn - -* tests: trying to defaeat logger problem - -* test: fix conftest logger issue - -* docs: add docs for warehouse path - -* chore: union types for whitelisted function - ---------- - -Co-authored-by: Heather Kusmierz <heather.kusmierz@gmail.com> ([`e3fb9c7`](https://github.com/agritheory/inventory_tools/commit/e3fb9c7f2a8ed3c42f4cb47078086bcdd60f91c7)) - -* Enforce UOMs to those that exist in the Item's conversion detail (#27) - -* wip: uom restricted query - -* feat: refactor UOM enforcement validation to be hookable - -* docs: add docs for UOM enforcement - -* tests: fix test logger problem, add xfail uom test ([`65d42e1`](https://github.com/agritheory/inventory_tools/commit/65d42e126d81f3ed1b98178f7b6c68c6a070986e)) - - -## v14.0.1 (2023-08-10) - -### Chore - -* chore: update test data for erpnext codebase changes (#24) ([`48160aa`](https://github.com/agritheory/inventory_tools/commit/48160aa27f5d0deb3be5e6e55f16d35fd92ae086)) - -### Ci - -* ci: update remote name ([`218eb06`](https://github.com/agritheory/inventory_tools/commit/218eb06a6a4a2ac3f5b9ee214a130e2c8ba30d29)) - -* ci: update version number ([`6e0c194`](https://github.com/agritheory/inventory_tools/commit/6e0c194235a19011b0c7db5adb8ed7e5954ba5eb)) - -* ci: add python semantic release ([`3382787`](https://github.com/agritheory/inventory_tools/commit/3382787d4726d7483a7243eafff03eb775d0ac3e)) - -### Feature - -* feat: based on item option ([`cc90229`](https://github.com/agritheory/inventory_tools/commit/cc90229b9ccf27cb5a872b24109dc7071f146802)) - -* feat: wip, make rfqs ([`c5a8867`](https://github.com/agritheory/inventory_tools/commit/c5a88673c3d57480a10ec68091e04a75488e18cb)) - -* feat: wip material demand options ([`bedd3d4`](https://github.com/agritheory/inventory_tools/commit/bedd3d42f299c73c1c1fa80c3a4928316d1428a2)) - -* feat: requires_rfq custom field, creation options in report ([`cd4ec42`](https://github.com/agritheory/inventory_tools/commit/cd4ec42e15fdbfa2cc24dc41b16562e74daf6124)) - -### Fix - -* fix: fix JS after adding draft PO column (#26) ([`42aefa9`](https://github.com/agritheory/inventory_tools/commit/42aefa9abab60962f923870a151d7bacbceba141)) - -### Unknown - -* Merge pull request #22 from agritheory/ci_fix - -ci: update remote name ([`946657b`](https://github.com/agritheory/inventory_tools/commit/946657b179d2a8ba6934c2df7ca5d83f9bb04f29)) - -* Merge pull request #21 from agritheory/py_sem_rel_14 - -ci: add python semantic release ([`13b41fa`](https://github.com/agritheory/inventory_tools/commit/13b41fad052f9c45e017b6fbac6897bdf38b7883)) - - -## v14.0.0 (2023-07-21) - -### Documentation - -* docs: wip material demand docs ([`6116a1b`](https://github.com/agritheory/inventory_tools/commit/6116a1b30b77565ff0470af32f8f59aa7a949786)) - -### Feature - -* feat: add column for draft PO amount ([`59d837b`](https://github.com/agritheory/inventory_tools/commit/59d837b1d3dbf4798d08618d033f732cad76cf1f)) - -* feat: create inventory tools settings when company is created ([`0121499`](https://github.com/agritheory/inventory_tools/commit/0121499bd99fa9ff5126b7432dd1d9a1d2816dd4)) - -* feat: create inventory tools settings when company is created ([`edff215`](https://github.com/agritheory/inventory_tools/commit/edff215f58ce8163da37436af893e1395164520c)) - -* feat: material demand PO creation ([`794f735`](https://github.com/agritheory/inventory_tools/commit/794f7352b27eb000ca96ece71c8081b69f519751)) - -* feat: add setting doctype ([`25a75de`](https://github.com/agritheory/inventory_tools/commit/25a75de2f1415ed78634d45c255438f1ed7a3ad1)) - -* feat: Initialize App ([`9e932fe`](https://github.com/agritheory/inventory_tools/commit/9e932fe49e70d5f2507d5900839c32f063a20898)) - -### Fix - -* fix: purchase order custom filed missing, carry price list from report to PO ([`4fe5ac8`](https://github.com/agritheory/inventory_tools/commit/4fe5ac84f52d656db3017af5e5a2a08111b2a729)) - -* fix: add back price list filter, fix schema ([`ceda857`](https://github.com/agritheory/inventory_tools/commit/ceda85797cc62a2d0e70e3a1135a88492f5c2cc0)) - -* fix: rebased v14 conflicts ([`b1614cb`](https://github.com/agritheory/inventory_tools/commit/b1614cb28a3f57d31c2056bf4e4506fd94a6e997)) - -* fix: supplier level de-selection and filtering ([`94140d0`](https://github.com/agritheory/inventory_tools/commit/94140d042a2e3fec79a56f0cacb86725dc2539b7)) - -* fix: module import name ([`ae290f8`](https://github.com/agritheory/inventory_tools/commit/ae290f8a392d9c5b3ea941244e68c8f1099728ef)) - -### Unknown - -* Merge pull request #20 from agritheory/material_demand - -Material Demand report fixes ([`7eb3c87`](https://github.com/agritheory/inventory_tools/commit/7eb3c877932a4d072ff56af23ad77958aba2fc36)) - -* Merge pull request #15 from agritheory/material_demand - -Material Demand ([`486fde6`](https://github.com/agritheory/inventory_tools/commit/486fde69cfdfa8810d93445ef4b32eb0753e799c)) - -* Merge branch 'version-14' into material_demand ([`9275dbf`](https://github.com/agritheory/inventory_tools/commit/9275dbf04f30a7391a0c003fff77cc5b105bd4cb)) - -* wip: material demand report improvements ([`d167804`](https://github.com/agritheory/inventory_tools/commit/d167804e90e10fc8299a49f6f0b90a512e93b5a3)) - -* wip: material demand ([`067b0d7`](https://github.com/agritheory/inventory_tools/commit/067b0d71dc372c35ac988556b20d13b9ab95f009)) - -* Merge pull request #16 from agritheory/settings_hook - -feat: create inventory tools settings when company is created ([`acc2b9c`](https://github.com/agritheory/inventory_tools/commit/acc2b9c640e1cb9eadf2e96060518a323ce34990)) - -* wip: material demand - -selection helpers look good except for supplier-level deselect, which toggles everything backwards ([`de7e09c`](https://github.com/agritheory/inventory_tools/commit/de7e09c4e04456fcd5d23fdbfa30c3a8a1933c28)) - -* wip: warehouse path ([`3ef9d3e`](https://github.com/agritheory/inventory_tools/commit/3ef9d3e7a30a98339f7d09c5ecba2e666e877b49)) - -* wip: material demand report improvements ([`c20379a`](https://github.com/agritheory/inventory_tools/commit/c20379aae9659fc1280f3c884bc888cce765116d)) - -* wip: material demand ([`3f50d5f`](https://github.com/agritheory/inventory_tools/commit/3f50d5f96b5b0a57993320e4f3c56034a490cd9a)) - -* wip: material demand ([`0cb8694`](https://github.com/agritheory/inventory_tools/commit/0cb869487fc576470f5ddb7f1b8f3b23f4cc1a57)) - -* Merge pull request #7 from agritheory/settings_doctype - -feat: add setting doctype ([`5285a99`](https://github.com/agritheory/inventory_tools/commit/5285a9967fc3615f16c7d4b06ac6f55589966975)) - -* Merge pull request #6 from agritheory/test_data_fixes - -fix: module import name ([`6edf7fb`](https://github.com/agritheory/inventory_tools/commit/6edf7fb0127648c90b0e3c203473681b8b964b63)) - -* initial commit ([`a09e1ed`](https://github.com/agritheory/inventory_tools/commit/a09e1ed6724ea49e39d5e208e6031283bf282f97)) - + + +# CHANGELOG + + +## v14.6.0 (2024-05-13) + +### Chore + +* chore: update quotation demand docs (#77) ([`f168079`](https://github.com/agritheory/inventory_tools/commit/f168079e29d99056d4c5eb2321d84957cff84f3b)) + +### Feature + +* feat: add inventory tools settings to boot (#81) ([`7ac2e15`](https://github.com/agritheory/inventory_tools/commit/7ac2e15aa74676ba5741887231814cdf9e675755)) + + +## v14.5.0 (2024-04-29) + +### Feature + +* feat: allow user to bulk upload items with quantity to shopping cart (#70) + +* feat: allow user to bulk upload items with quantity to shoping cart + +* nit: variable and syntactical changes + +* fix: removed line by line order placing, optimised code, added enqueue + +* fix: minor cleanup + +--------- + +Co-authored-by: Rohan Bansal <rohan@agritheory.dev> ([`748a9bc`](https://github.com/agritheory/inventory_tools/commit/748a9bc85034094dcd60544fe2b822b67322fb44)) + +### Unknown + +* Aggregate and/or Split Quotations into multiple Sales Orders (#65) + +* feat: Quotation Demand + +* feat: hook + +* feat: settings for quotation demand + +* feat: fixes, SO validate_warehouse + +* feat: quotation demand tests + +* feat: documentation + +* feat: improvements + +* feat: improve tests + +* feat: total selected, price, draft so + +* feat: tests ([`6eceeb5`](https://github.com/agritheory/inventory_tools/commit/6eceeb5623856d659db1ffc3dd7a3ce352cd8000)) + + +## v14.4.0 (2024-04-25) + +### Ci + +* ci: add black to ci (#61) + +* ci: add black to ci + +* chore: black ([`db5a742`](https://github.com/agritheory/inventory_tools/commit/db5a7427f068d7005f8f07125d81f1430c49eb9b)) + +### Feature + +* feat: aggregate POs (#71) ([`5ff8fee`](https://github.com/agritheory/inventory_tools/commit/5ff8fee2ef356990e2dcda0d2db76d1b856a0685)) + +### Unknown + +* Material Demand Test (#64) + +* wip: material demand tests + +* test: material demand aggregation tests and fixes with and without warehouse + +* test: material demand tests and bug fixes + +* chore: add mypy types isntall + +* fix: remove duplicate function + +* tests: add pytest runner file + +* tests: add helper files + +* tests: correct app name + +* ci: fix site_config + +* ci: update action version to use node 20 + +* ci: hrms get-app + +* ci: add base branch sensitvie get-app + +* ci: add hrms to install + +* ci: add node 20 versions + +* ci: remove hrms from install-apps + +* ci: get-app for hrms + +* ci: update prettier and changed-files actions + +* ci: ad dhrms to apps.txt + +* ci: get-app for hrms again + +* ci: get-app for hrms only + +* ci: overrite erpnext install (hrms is tryign to install develop branch) + +* ci: add app name + +* ci: add repo to required apps + +* ci: try install hrms first with overwrite + +* ci: remove "install_apps" key from site_config + +* ci: remove required apps + +* feat: use query builder + +* ci: remove resolve deps + +* ci: fix install apps + +* test: fix manufacturing capacity test and report + +* test: cleanup uom test + +* test: fix error message assert + +* test: add ordering + +* fix: test + +* fix: use frappe function + +* fix: use frappe function + +--------- + +Co-authored-by: fproldan <franciscoproldan@gmail.com> ([`6f4f63f`](https://github.com/agritheory/inventory_tools/commit/6f4f63f22f3f7278e20c6aba9e5ef6a021ae03b2)) + +* Manufacturing capacity report (#57) + +* ci: update app in str(file path) code to more exact matching + +* chore: remove unused code + +* feat: add manufacturing capacity report + +* tests: start manufacturing capacity report tests + +* chore: uncomment formatting code + +* docs: add manufacturing capacity report documentation + +* fix: div by zero in parts can build calc + +* docs: update for calculation differences + +* fix: in stock qty to zero if none from query ([`2936cde`](https://github.com/agritheory/inventory_tools/commit/2936cde9aac6583edeb383d94d74142461cf243a)) + + +## v14.3.0 (2024-03-01) + +### Feature + +* feat: Alternative workstation in job card and operation (#56) + +* feat: New field and link filters to select alternative workstation + +* fix: add alternativ workstation in fixture + +* fix:change alternative workstation in fixture + +* change to error key function + +* fix: set validation on workstation if not operation + +* changes field name in testcase + +* fix: change a class base function to saperate function + +* added a searchfield in query + +* added a searchfield in query + +* added a searchfield in query + +* added a searchfield in query + +* added a searchfield in query + +* feat: validation to not allow default workstation in alternative + +* added comment on function ([`0788d3c`](https://github.com/agritheory/inventory_tools/commit/0788d3caa67ad7820a5a3cdfdc950c85bd6f0cd7)) + +### Unknown + +* Prompt Material Transfer upon Completion of Manufacturing Stock Entry (#51) + +* feat: wip stock entry next action + +* feat: improve message + +* fix: on_submit hook + +* fix: parameter hardcoded + +* feat: change wording ([`d1e126b`](https://github.com/agritheory/inventory_tools/commit/d1e126b8dbaf09370f6d4c173e24af33ac994bd7)) + + +## v14.2.0 (2024-02-01) + +### Feature + +* feat: manufacturing over/under production + +* feat: WIP Manufacturing Over/Under Production + +* feat: WIP Manufacturing Over/Under Production + +* feat: WIP Manufacturing Over/Under Production + +* fix: unused import + +* feat: override onload for work order + +* feat: oveeride get_pending_raw_materials + +* fix: allowed_qty in job card + +* fix: indentation + +* fix: import + +* fixes + +* wip: tests + +* feat: test_get_allowance_percentage + +* feat: test test_validate_finished_goods + +* fix: validate_job_card and test + +* fix: test ([`baeb469`](https://github.com/agritheory/inventory_tools/commit/baeb4690d42429a062595eb9058ddd2770b190c9)) + +### Unknown + +* Make Creation of Job Card(s) on Submit of Work Order configurable (#49) + +* feat: configurable creation of job card + +* feat: configurable creation of job card ([`5bf9486`](https://github.com/agritheory/inventory_tools/commit/5bf94860c58d81faccb1106828f16121c081871a)) + + +## v14.1.3 (2024-01-04) + +### Fix + +* fix: validate customizations (#35) + +* fix: validate customizations + +* fix: only install inventory tools customizations ([`e1d86a0`](https://github.com/agritheory/inventory_tools/commit/e1d86a0739e56f3b987b599c275644ee5c29fc0a)) + + +## v14.1.2 (2023-09-12) + +### Chore + +* chore: remove console.log ([`f82b590`](https://github.com/agritheory/inventory_tools/commit/f82b590d061608818170092f07e3da8d9153b756)) + +### Ci + +* ci: update release action user and email (#32) ([`4264bdd`](https://github.com/agritheory/inventory_tools/commit/4264bdde25c0bff1390923e7f18fce7c353844db)) + +### Fix + +* fix: make uom enforcement respond better to toggle on/off ([`3734cb4`](https://github.com/agritheory/inventory_tools/commit/3734cb42e880168bb4c4a71c67820da63857e0bf)) + +### Unknown + +* Merge pull request #33 from agritheory/uom_enforcement_fix + +fix: make uom enforcement respond better to toggle on/off ([`13ad883`](https://github.com/agritheory/inventory_tools/commit/13ad8832a346799563e0f7ae2f1b20c457d10d5c)) + + +## v14.1.1 (2023-08-30) + +### Fix + +* fix: add is_subcontracted check for additional validation/submit/cancel code ([`8182dac`](https://github.com/agritheory/inventory_tools/commit/8182dacfd35d2a2ee4c5ee94211cbfc7ac00abfd)) + +### Unknown + +* Merge pull request #30 from agritheory/fix_subc_validation + +fix: add is_subcontracted check for additional validation/submit/cancel code ([`c4e7b83`](https://github.com/agritheory/inventory_tools/commit/c4e7b83fd1700d08e8f711e10f1a77c81da0de32)) + + +## v14.1.0 (2023-08-24) + +### Chore + +* chore: update test data for erpnext codebase changes (#24) ([`b7dfc02`](https://github.com/agritheory/inventory_tools/commit/b7dfc02e228c4ebdca86000dac65d1a903640b15)) + +### Ci + +* ci: update remote name ([`021b8a4`](https://github.com/agritheory/inventory_tools/commit/021b8a47355cb9028bac7ac8e60d224247d5b0e8)) + +* ci: update version number ([`9d82150`](https://github.com/agritheory/inventory_tools/commit/9d821506af9d6eb72e3bfb1345ac652aa86896dc)) + +* ci: add python semantic release ([`703803d`](https://github.com/agritheory/inventory_tools/commit/703803df7949fb0cb43699482425ec31595dd1b9)) + +### Documentation + +* docs: update material demand section for expanded functionality ([`1af88f6`](https://github.com/agritheory/inventory_tools/commit/1af88f6d06fdc8e6908381f9dd9011fe470dd9d7)) + +### Feature + +* feat: select email template ([`02196e3`](https://github.com/agritheory/inventory_tools/commit/02196e37c7234982c5dafda9e814117d374565a9)) + +### Fix + +* fix: blank email template for PO; skip supplier-only rows for RFQ ([`2a0e8cd`](https://github.com/agritheory/inventory_tools/commit/2a0e8cd6c6aefdd1a0d7b978652f97ddc755523f)) + +* fix: fix JS after adding draft PO column (#26) ([`e0d3229`](https://github.com/agritheory/inventory_tools/commit/e0d322950e87dcf09f13441859e273a0e44a1192)) + +### Unknown + +* Merge pull request #23 from agritheory/issue_19 + +Allow Creation of RFQ from Material Demand report ([`8a0e350`](https://github.com/agritheory/inventory_tools/commit/8a0e35024968ce5a0f6da05e71f62c091bf2b38f)) + +* Work Order Subcontracting (#13) + +* tests: update test data for additional manufacturing workflow + +* feat: work order subcontracting validations + +* tests: add valuation rate for subcontracted item + +* feat: start wo subcontracting feat + +* feat: make subcontracting section visible by check and settings + +* feat: add ste detail paid field and new pi cols + +* feat: setup hooks and custom po + +* tests: update data for default supplier and price lists + +* feat: include doctype js + +* feat: add work order customizations + +* feat: add purchase order customizations + +* feat: update purchase invoice customizations + +* feat: remove unused code blocks + +* fix: add module to json + +* fix: update custom doc path + +* feat: consolidate custom PI code and modularize class functions + +* feat: combine and refactor PO code + +* feat: update server function paths + +* feat: add BOM field default + +* feat: update to use BOM field vs Item for is_subcontracted + +* feat: code cleanup and refactoring for BOM field + +* feat: update for uom conversion and new svc item + +* fix: move UOM conversions to item + +* add todos in JS + +* feat: rewire item adjustments for conversion factor + +* wip: integrate with production plan + +* feat: add supplier field in WO, allow selection of supplier in dialog + +* chore: add comment explaining precision code + +* wip: subcontractor workflow + +* feat: subcontracting workflow with correct warehouses + +* feat: show/hide subcontracting columns + +* feat: colorize fetach stock entries button + +* fix: text artifacts, new PO errors + +* feat: fetch supplier warehouse, added connectiosn to PI and PO from WO + +* feat: add filters and looks to both PI and PO + +* fix: monkey patch validate_item_details + +* feat: remove buttons and update stockfield in WO subc workflow + +* Enforce UOMs to those that exist in the Item's conversion detail (#27) + +* wip: uom restricted query + +* feat: refactor UOM enforcement validation to be hookable + +* docs: add docs for UOM enforcement + +* tests: fix test logger problem, add xfail uom test + +* Warehouse path (#25) + +* wip: warehouse path + +* wip: warehouse path + +* wip: warehouse path feature + +* feat: warehouse path builder + +* feat: undo query when not configured; setup tweaks + +* chore: update test data for erpnext codebase changes (#24) + +* wip: warehouse path feature + +* wip: test setup + +* chore: update yarn + +* tests: trying to defaeat logger problem + +* test: fix conftest logger issue + +* docs: add docs for warehouse path + +* chore: union types for whitelisted function + +--------- + +Co-authored-by: Heather Kusmierz <heather.kusmierz@gmail.com> + +* tests: test cadence (#28) + +* fix: no cancelled PO in se query, code clean up + +* chore: add comment to explain monkey patch rationale + +--------- + +Co-authored-by: Tyler Matteson <tyler@agritheory.com> ([`ac11c1d`](https://github.com/agritheory/inventory_tools/commit/ac11c1df4daad2189916ca9841480aa1796e42e3)) + +* Merge branch 'version-14' into issue_19 ([`ee0a27f`](https://github.com/agritheory/inventory_tools/commit/ee0a27f0f03a8e3c9e9fa54011d02e44554b8bbb)) + +* Documentation (#29) + +* docs: add index page + +* docs: add screen shots and workflow + +* docs: add screen shots, text edits + +* docs: add example data page + +* docs: add placeholder pages + +* docs: add subcontracting via WO section + +* docs: edits, conform text conventions ([`2fc980d`](https://github.com/agritheory/inventory_tools/commit/2fc980d41105759cffa33e610b779b6d464cf24c)) + +* tests: test cadence (#28) ([`6b5bd47`](https://github.com/agritheory/inventory_tools/commit/6b5bd47089fb2f8a09159635e618425743cc9dff)) + +* Warehouse path (#25) + +* wip: warehouse path + +* wip: warehouse path + +* wip: warehouse path feature + +* feat: warehouse path builder + +* feat: undo query when not configured; setup tweaks + +* chore: update test data for erpnext codebase changes (#24) + +* wip: warehouse path feature + +* wip: test setup + +* chore: update yarn + +* tests: trying to defaeat logger problem + +* test: fix conftest logger issue + +* docs: add docs for warehouse path + +* chore: union types for whitelisted function + +--------- + +Co-authored-by: Heather Kusmierz <heather.kusmierz@gmail.com> ([`370dd6f`](https://github.com/agritheory/inventory_tools/commit/370dd6f9789156ebcbbe7b111d891a74731a477b)) + +* Enforce UOMs to those that exist in the Item's conversion detail (#27) + +* wip: uom restricted query + +* feat: refactor UOM enforcement validation to be hookable + +* docs: add docs for UOM enforcement + +* tests: fix test logger problem, add xfail uom test ([`d4c145a`](https://github.com/agritheory/inventory_tools/commit/d4c145a94d8402fa441619289b4cc6438b7d5c45)) + +* Documentation (#29) + +* docs: add index page + +* docs: add screen shots and workflow + +* docs: add screen shots, text edits + +* docs: add example data page + +* docs: add placeholder pages + +* docs: add subcontracting via WO section + +* docs: edits, conform text conventions ([`198e110`](https://github.com/agritheory/inventory_tools/commit/198e110ab0c5461b9c46925fc05af351151abd38)) + +* tests: test cadence (#28) ([`b91e024`](https://github.com/agritheory/inventory_tools/commit/b91e0246f6e384f40685af40d3a249daa9d03a8c)) + +* Warehouse path (#25) + +* wip: warehouse path + +* wip: warehouse path + +* wip: warehouse path feature + +* feat: warehouse path builder + +* feat: undo query when not configured; setup tweaks + +* chore: update test data for erpnext codebase changes (#24) + +* wip: warehouse path feature + +* wip: test setup + +* chore: update yarn + +* tests: trying to defaeat logger problem + +* test: fix conftest logger issue + +* docs: add docs for warehouse path + +* chore: union types for whitelisted function + +--------- + +Co-authored-by: Heather Kusmierz <heather.kusmierz@gmail.com> ([`e3fb9c7`](https://github.com/agritheory/inventory_tools/commit/e3fb9c7f2a8ed3c42f4cb47078086bcdd60f91c7)) + +* Enforce UOMs to those that exist in the Item's conversion detail (#27) + +* wip: uom restricted query + +* feat: refactor UOM enforcement validation to be hookable + +* docs: add docs for UOM enforcement + +* tests: fix test logger problem, add xfail uom test ([`65d42e1`](https://github.com/agritheory/inventory_tools/commit/65d42e126d81f3ed1b98178f7b6c68c6a070986e)) + + +## v14.0.1 (2023-08-10) + +### Chore + +* chore: update test data for erpnext codebase changes (#24) ([`48160aa`](https://github.com/agritheory/inventory_tools/commit/48160aa27f5d0deb3be5e6e55f16d35fd92ae086)) + +### Ci + +* ci: update remote name ([`218eb06`](https://github.com/agritheory/inventory_tools/commit/218eb06a6a4a2ac3f5b9ee214a130e2c8ba30d29)) + +* ci: update version number ([`6e0c194`](https://github.com/agritheory/inventory_tools/commit/6e0c194235a19011b0c7db5adb8ed7e5954ba5eb)) + +* ci: add python semantic release ([`3382787`](https://github.com/agritheory/inventory_tools/commit/3382787d4726d7483a7243eafff03eb775d0ac3e)) + +### Feature + +* feat: based on item option ([`cc90229`](https://github.com/agritheory/inventory_tools/commit/cc90229b9ccf27cb5a872b24109dc7071f146802)) + +* feat: wip, make rfqs ([`c5a8867`](https://github.com/agritheory/inventory_tools/commit/c5a88673c3d57480a10ec68091e04a75488e18cb)) + +* feat: wip material demand options ([`bedd3d4`](https://github.com/agritheory/inventory_tools/commit/bedd3d42f299c73c1c1fa80c3a4928316d1428a2)) + +* feat: requires_rfq custom field, creation options in report ([`cd4ec42`](https://github.com/agritheory/inventory_tools/commit/cd4ec42e15fdbfa2cc24dc41b16562e74daf6124)) + +### Fix + +* fix: fix JS after adding draft PO column (#26) ([`42aefa9`](https://github.com/agritheory/inventory_tools/commit/42aefa9abab60962f923870a151d7bacbceba141)) + +### Unknown + +* Merge pull request #22 from agritheory/ci_fix + +ci: update remote name ([`946657b`](https://github.com/agritheory/inventory_tools/commit/946657b179d2a8ba6934c2df7ca5d83f9bb04f29)) + +* Merge pull request #21 from agritheory/py_sem_rel_14 + +ci: add python semantic release ([`13b41fa`](https://github.com/agritheory/inventory_tools/commit/13b41fad052f9c45e017b6fbac6897bdf38b7883)) + + +## v14.0.0 (2023-07-21) + +### Documentation + +* docs: wip material demand docs ([`6116a1b`](https://github.com/agritheory/inventory_tools/commit/6116a1b30b77565ff0470af32f8f59aa7a949786)) + +### Feature + +* feat: add column for draft PO amount ([`59d837b`](https://github.com/agritheory/inventory_tools/commit/59d837b1d3dbf4798d08618d033f732cad76cf1f)) + +* feat: create inventory tools settings when company is created ([`0121499`](https://github.com/agritheory/inventory_tools/commit/0121499bd99fa9ff5126b7432dd1d9a1d2816dd4)) + +* feat: create inventory tools settings when company is created ([`edff215`](https://github.com/agritheory/inventory_tools/commit/edff215f58ce8163da37436af893e1395164520c)) + +* feat: material demand PO creation ([`794f735`](https://github.com/agritheory/inventory_tools/commit/794f7352b27eb000ca96ece71c8081b69f519751)) + +* feat: add setting doctype ([`25a75de`](https://github.com/agritheory/inventory_tools/commit/25a75de2f1415ed78634d45c255438f1ed7a3ad1)) + +* feat: Initialize App ([`9e932fe`](https://github.com/agritheory/inventory_tools/commit/9e932fe49e70d5f2507d5900839c32f063a20898)) + +### Fix + +* fix: purchase order custom filed missing, carry price list from report to PO ([`4fe5ac8`](https://github.com/agritheory/inventory_tools/commit/4fe5ac84f52d656db3017af5e5a2a08111b2a729)) + +* fix: add back price list filter, fix schema ([`ceda857`](https://github.com/agritheory/inventory_tools/commit/ceda85797cc62a2d0e70e3a1135a88492f5c2cc0)) + +* fix: rebased v14 conflicts ([`b1614cb`](https://github.com/agritheory/inventory_tools/commit/b1614cb28a3f57d31c2056bf4e4506fd94a6e997)) + +* fix: supplier level de-selection and filtering ([`94140d0`](https://github.com/agritheory/inventory_tools/commit/94140d042a2e3fec79a56f0cacb86725dc2539b7)) + +* fix: module import name ([`ae290f8`](https://github.com/agritheory/inventory_tools/commit/ae290f8a392d9c5b3ea941244e68c8f1099728ef)) + +### Unknown + +* Merge pull request #20 from agritheory/material_demand + +Material Demand report fixes ([`7eb3c87`](https://github.com/agritheory/inventory_tools/commit/7eb3c877932a4d072ff56af23ad77958aba2fc36)) + +* Merge pull request #15 from agritheory/material_demand + +Material Demand ([`486fde6`](https://github.com/agritheory/inventory_tools/commit/486fde69cfdfa8810d93445ef4b32eb0753e799c)) + +* Merge branch 'version-14' into material_demand ([`9275dbf`](https://github.com/agritheory/inventory_tools/commit/9275dbf04f30a7391a0c003fff77cc5b105bd4cb)) + +* wip: material demand report improvements ([`d167804`](https://github.com/agritheory/inventory_tools/commit/d167804e90e10fc8299a49f6f0b90a512e93b5a3)) + +* wip: material demand ([`067b0d7`](https://github.com/agritheory/inventory_tools/commit/067b0d71dc372c35ac988556b20d13b9ab95f009)) + +* Merge pull request #16 from agritheory/settings_hook + +feat: create inventory tools settings when company is created ([`acc2b9c`](https://github.com/agritheory/inventory_tools/commit/acc2b9c640e1cb9eadf2e96060518a323ce34990)) + +* wip: material demand + +selection helpers look good except for supplier-level deselect, which toggles everything backwards ([`de7e09c`](https://github.com/agritheory/inventory_tools/commit/de7e09c4e04456fcd5d23fdbfa30c3a8a1933c28)) + +* wip: warehouse path ([`3ef9d3e`](https://github.com/agritheory/inventory_tools/commit/3ef9d3e7a30a98339f7d09c5ecba2e666e877b49)) + +* wip: material demand report improvements ([`c20379a`](https://github.com/agritheory/inventory_tools/commit/c20379aae9659fc1280f3c884bc888cce765116d)) + +* wip: material demand ([`3f50d5f`](https://github.com/agritheory/inventory_tools/commit/3f50d5f96b5b0a57993320e4f3c56034a490cd9a)) + +* wip: material demand ([`0cb8694`](https://github.com/agritheory/inventory_tools/commit/0cb869487fc576470f5ddb7f1b8f3b23f4cc1a57)) + +* Merge pull request #7 from agritheory/settings_doctype + +feat: add setting doctype ([`5285a99`](https://github.com/agritheory/inventory_tools/commit/5285a9967fc3615f16c7d4b06ac6f55589966975)) + +* Merge pull request #6 from agritheory/test_data_fixes + +fix: module import name ([`6edf7fb`](https://github.com/agritheory/inventory_tools/commit/6edf7fb0127648c90b0e3c203473681b8b964b63)) + +* initial commit ([`a09e1ed`](https://github.com/agritheory/inventory_tools/commit/a09e1ed6724ea49e39d5e208e6031283bf282f97)) + diff --git a/inventory_tools/customize.py b/inventory_tools/customize.py index 25a6f64..6d78a51 100644 --- a/inventory_tools/customize.py +++ b/inventory_tools/customize.py @@ -9,7 +9,12 @@ def load_customizations(): customizations_directory = ( - Path().cwd().parent / "apps" / "check_run" / "check_run" / "check_run" / "custom" + Path().cwd().parent + / "apps" + / "inventory_tools" + / "inventory_tools" + / "inventory_tools" + / "custom" ) files = list(customizations_directory.glob("**/*.json")) for file in files: diff --git a/inventory_tools/docs/assets/edit_specifications.PNG b/inventory_tools/docs/assets/edit_specifications.PNG new file mode 100644 index 0000000..5557b47 Binary files /dev/null and b/inventory_tools/docs/assets/edit_specifications.PNG differ diff --git a/inventory_tools/docs/assets/specification.PNG b/inventory_tools/docs/assets/specification.PNG new file mode 100644 index 0000000..595fa2d Binary files /dev/null and b/inventory_tools/docs/assets/specification.PNG differ diff --git a/inventory_tools/docs/assets/specification_attribute.PNG b/inventory_tools/docs/assets/specification_attribute.PNG new file mode 100644 index 0000000..ada1e0d Binary files /dev/null and b/inventory_tools/docs/assets/specification_attribute.PNG differ diff --git a/inventory_tools/docs/faceted_search.md b/inventory_tools/docs/faceted_search.md new file mode 100644 index 0000000..53e1e6f --- /dev/null +++ b/inventory_tools/docs/faceted_search.md @@ -0,0 +1,50 @@ + + + +# Faceted Search + +Faceted search works on top of ERPNext's Shopping Cart to add additional Ecommerce controls for marketplace users. This feature allows you to list your products under `/all-products` and filter them based on their specifications. + +# Manual Setup + +Follow the steps to create your first listed product with multiple specifications and their variations. + +### Steps: + +1. Items and Website Items should be configured according to the instructions provided for the [Ecommerce Module](https://docs.erpnext.com/docs/user/manual/en/set_up_e_commerce) + +2. To create Specifications for the Item: + 1. Type `Specification` in Awesomebar > `Add Specification`. + 2. Select `DocType` on which you would like to customize the specification. + 3. Select `Apply On` based on the doctype or leave it blank to apply to all records of that DocType. This currently only works for Item. + 4. Mark it `Enabled`. +![Screen shot of ](./assets/specification.PNG) + + 5. Under `Attributes` table click `Add Row` > `Edit`. + + - Write unique `Attribute Name`. + - Choose if the value is expected to be a `Date Value` or `Numeric Value`. + - Select `Apply On` (In this case, we can choose `Item`). + - Optionally select the `Field` on which the attribute should be applied. + - Choose a `Component` based on the following criteria: + + 1. `FacetedSearchColorPicker`: if the attribute is related to colors. + 2. `FacetedSearchDateRange`: if the attribute is related to date values. + 3. `FacetedSearchNumericRange`: if the attribute is related to numeric values. + 4. `AttributeFilter`: if the attribute doesn't belong to any of the above. +![Screen shot of ](./assets/specification_attribute.PNG) + + - `Save` the Specification(s). +7. Create `Specification Values`: + - Route through `Stock` > `Item` > Choose Item. + - On Item page click `Actions` (Top Right) > Click `Edit Specification` + - This will try to pre-populate the specifications based on your selection. If no specifications are set, please select from the dropdown instead. + - `Save` +![Screen shot of ](./assets/edit_specifications.PNG) + + +This is how you can create and manage your specification. you can go to `/all-products`, you will see listed Item and Filter(s) on left. + +### Note: +`Specification` and `Specification Values` are reusable as long as the grouping of Items is done correctly. You may want to create new Specifications for different types of goods. \ No newline at end of file diff --git a/inventory_tools/docs/index.md b/inventory_tools/docs/index.md index 1dd9280..c5348ee 100644 --- a/inventory_tools/docs/index.md +++ b/inventory_tools/docs/index.md @@ -11,6 +11,7 @@ The Inventory Tools application enhances and extends inventory-related functiona - **[Subcontracting Workflow via Work Order](./wo_subcontracting.md)**: an alternative to ERPNext's subcontracting workflow that enables a user to employ Work Orders, subcontracting Purchase Orders, and manufacturing Stock Entries in lieu of Purchase Receipts or Subcontracting Orders/Receipts. Enhancements to the subcontracting Purchase Invoice allow a user to quickly reconcile what Items have been received with what is being invoiced - **[Inline Landed Costing](./landed_costing.md)**: Coming soon! This features enables a user to include any additional costs to be capitalized into an Item's valuation directly in a Purchase Receipt or Purchase Invoice without needing to create a separate Landed Cost Voucher - **[Manufacturing Capacity](./manufacturing_capacity.md)**: a report-based interface to show, for a given BOM, the entire hierarchy of any BOM tree containing that BOM with demand and in-stock quantities for all levels +- **[Faceted Search](./faceted_search.md)**: loosely-coupled attributes for Items, visible in both Ecommerce and Item Listview search contexts ## Configuration Any feature in Inventory Tools may be toggled on or off via the Inventory Tools Settings document. The only exception to this is the Material Demand report, which is generally available upon installation of the app. There may be one settings document for each company in ERPNext to enable features on a per-company basis. Follow the links above for further details around feature-specific configuration. diff --git a/inventory_tools/hooks.py b/inventory_tools/hooks.py index f242b30..1d0d119 100644 --- a/inventory_tools/hooks.py +++ b/inventory_tools/hooks.py @@ -9,18 +9,27 @@ app_description = "Inventory Tools" app_email = "support@agritheory.dev" app_license = "MIT" -required_apps = ["erpnext", "hrms"] +required_apps = ["erpnext", "hrms", "webshop"] # Includes in # ------------------ # include js, css files in header of desk.html -# app_include_css = "/assets/inventory_tools/css/inventory_tools.css" -app_include_js = ["inventory_tools.bundle.js"] +app_include_css = [ + "/assets/inventory_tools/dist/js/style.css", +] +app_include_js = [ + "inventory_tools.bundle.js", + "/assets/inventory_tools/dist/js/inventory_tools.js", +] # include js, css files in header of web template -# web_include_css = "/assets/inventory_tools/css/inventory_tools.css" -# web_include_js = "/assets/inventory_tools/js/inventory_tools.js" +web_include_css = [ + "/assets/inventory_tools/dist/js/style.css", +] +web_include_js = [ + "/assets/inventory_tools/dist/js/inventory_tools.js", +] # include custom scss in every website theme (without file extension ".scss") # website_theme_scss = "inventory_tools/public/scss/website" @@ -123,6 +132,7 @@ "Stock Entry": "inventory_tools.inventory_tools.overrides.stock_entry.InventoryToolsStockEntry", "Work Order": "inventory_tools.inventory_tools.overrides.work_order.InventoryToolsWorkOrder", "Workstation": "inventory_tools.inventory_tools.overrides.workstation.InventoryToolsWorkstation", + "Website Item": "inventory_tools.inventory_tools.overrides.website_item.InventoryToolsWebsiteItem", } @@ -143,7 +153,10 @@ ], }, "Item": { - "validate": ["inventory_tools.inventory_tools.overrides.uom.duplicate_weight_to_uom_conversion"], + "validate": [ + "inventory_tools.inventory_tools.overrides.uom.duplicate_weight_to_uom_conversion", + "inventory_tools.inventory_tools.faceted_search.update_specification_attribute_values", + ], }, "Warehouse": { "validate": ["inventory_tools.inventory_tools.overrides.warehouse.update_warehouse_path"] @@ -187,6 +200,7 @@ override_whitelisted_methods = { "erpnext.manufacturing.doctype.work_order.work_order.make_stock_entry": "inventory_tools.inventory_tools.overrides.work_order.make_stock_entry", "erpnext.stock.get_item_details.get_item_details": "inventory_tools.inventory_tools.overrides.purchase_order.get_item_details", + "webshop.webshop.api.get_product_filter_data": "inventory_tools.inventory_tools.faceted_search.get_product_filter_data", } diff --git a/inventory_tools/inventory_tools/boot.py b/inventory_tools/inventory_tools/boot.py index 1a20d32..01dda3e 100644 --- a/inventory_tools/inventory_tools/boot.py +++ b/inventory_tools/inventory_tools/boot.py @@ -1,8 +1,11 @@ -import frappe - - -def boot_session(bootinfo): - bootinfo.inventory_tools_settings = {} - for company in frappe.get_all("Inventory Tools Settings", pluck="company"): - settings = frappe.get_doc("Inventory Tools Settings", company) - bootinfo.inventory_tools_settings[company] = settings +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe + + +def boot_session(bootinfo): + bootinfo.inventory_tools_settings = {} + for company in frappe.get_all("Inventory Tools Settings", pluck="company"): + settings = frappe.get_doc("Inventory Tools Settings", company) + bootinfo.inventory_tools_settings[company] = settings diff --git a/inventory_tools/inventory_tools/custom/color.json b/inventory_tools/inventory_tools/custom/color.json new file mode 100644 index 0000000..f0b78c0 --- /dev/null +++ b/inventory_tools/inventory_tools/custom/color.json @@ -0,0 +1,57 @@ +{ + "custom_fields": [ + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "columns": 0, + "creation": "2024-02-22 10:20:21.468031", + "default": null, + "docstatus": 0, + "dt": "Color", + "fetch_if_empty": 0, + "fieldname": "image", + "fieldtype": "Attach", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "idx": 1, + "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": "color", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Image", + "length": 0, + "modified": "2024-02-22 10:20:21.468031", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Color-image", + "no_copy": 0, + "non_negative": 0, + "owner": "Administrator", + "permlevel": 0, + "precision": "", + "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 + } + ], + "custom_perms": [], + "doctype": "Color", + "links": [], + "property_setters": [], + "sync_on_migrate": 1 +} diff --git a/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json b/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json index d7d8742..b3de695 100644 --- a/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json +++ b/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json @@ -25,9 +25,14 @@ "overproduction_percentage_for_work_order", "section_break_0", "update_warehouse_path", - "section_break_gzcbr", + "column_break_ddssn", + "allow_alternative_workstations", "uoms_section", - "enforce_uoms" + "enforce_uoms", + "faceted_search_tab", + "section_break_pxnx", + "show_on_website", + "show_in_listview" ], "fields": [ { @@ -87,11 +92,6 @@ "fieldtype": "Check", "label": "Update Warehouse Path" }, - { - "fieldname": "section_break_gzcbr", - "fieldtype": "Section Break", - "label": "Warehouses" - }, { "fieldname": "uoms_section", "fieldtype": "Section Break", @@ -146,11 +146,43 @@ "fieldtype": "Link", "label": "Aggregated Sales Warehouse", "options": "Warehouse" + }, + { + "fieldname": "faceted_search_tab", + "fieldtype": "Tab Break", + "label": "Faceted Search" + }, + { + "default": "0", + "fieldname": "show_on_website", + "fieldtype": "Check", + "label": "Show on Website" + }, + { + "default": "0", + "fieldname": "show_in_listview", + "fieldtype": "Check", + "label": "Show in Listview" + }, + { + "fieldname": "column_break_ddssn", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "allow_alternative_workstations", + "fieldtype": "Check", + "label": "Allow Alternative Workstations" + }, + { + "description": "These settings will apply to all companies", + "fieldname": "section_break_pxnx", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2024-04-12 10:02:32.688310", + "modified": "2024-08-05 10:12:40.728422", "modified_by": "Administrator", "module": "Inventory Tools", "name": "Inventory Tools Settings", diff --git a/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.py b/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.py index 8356d57..a10518f 100644 --- a/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.py +++ b/inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.py @@ -9,6 +9,7 @@ class InventoryToolsSettings(Document): def validate(self): self.create_warehouse_path_custom_field() self.validate_single_aggregation_company() + self.set_faceted_search_for_all_companies() def create_warehouse_path_custom_field(self): if frappe.db.exists("Custom Field", "Warehouse-warehouse_path"): @@ -79,6 +80,13 @@ def validate_single_aggregation_company(self): f"Sales Order Aggregation Company in {its.name} Inventory Tools Settings is set to {its.sales_order_aggregation_company}" ) + def set_faceted_search_for_all_companies(self): + for its in frappe.get_all("Inventory Tools Settings", pluck="name"): + if its == self.name: + continue + frappe.db.set_value("Inventory Tools Settings", its, "show_on_website", self.show_on_website) + frappe.db.set_value("Inventory Tools Settings", its, "show_in_listview", self.show_in_listview) + @frappe.whitelist() def create_inventory_tools_settings(doc, method=None) -> None: @@ -89,3 +97,12 @@ def create_inventory_tools_settings(doc, method=None) -> None: its = frappe.new_doc("Inventory Tools Settings") its.company = doc.name its.save() + + +@frappe.whitelist(allow_guest=True) +def faceted_search_enabled(): + its = frappe.get_last_doc("Inventory Tools Settings") + return { + "show_on_website": its.show_on_website, + "show_in_listview": its.show_in_listview, + } diff --git a/inventory_tools/inventory_tools/doctype/specification/__init__.py b/inventory_tools/inventory_tools/doctype/specification/__init__.py new file mode 100644 index 0000000..6b9109e --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.js b/inventory_tools/inventory_tools/doctype/specification/specification.js new file mode 100644 index 0000000..d03854b --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification/specification.js @@ -0,0 +1,48 @@ +// Copyright (c) 2023, AgriTheory and contributors +// For license information, please see license.txt + +frappe.provide('inventory_tools') + +frappe.ui.form.on('Specification', { + refresh: frm => { + add_specification_dialog(frm) + }, +}) + +frappe.ui.form.on('Specification Attribute', { + applied_on: (frm, cdt, cdn) => { + get_data_fieldnames(frm, cdt, cdn) + }, + form_render: (frm, cdt, cdn) => { + get_data_fieldnames(frm, cdt, cdn) + }, +}) + +function get_data_fieldnames(frm, cdt, cdn) { + let row = locals[cdt][cdn] + if (!row.applied_on) { + return + } + frappe + .xcall('inventory_tools.inventory_tools.doctype.specification.specification.get_data_fieldnames', { + doctype: row.applied_on, + }) + .then(r => { + if (!r) { + return + } + frm.set_df_property('attributes', 'options', r, frm.doc.name, 'field', row.name) + frm.refresh_field('attributes') + }) +} + +function add_specification_dialog(frm) { + // save before continuing + frm.add_custom_button( + __('Generate Specification Values'), + () => { + inventory_tools.specification_dialog(frm) + }, + 'Actions' + ) +} diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.json b/inventory_tools/inventory_tools/doctype/specification/specification.json new file mode 100644 index 0000000..e6faa4b --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification/specification.json @@ -0,0 +1,125 @@ +{ + "actions": [], + "allow_rename": 1, + "autoname": "prompt", + "creation": "2023-11-13 16:45:20.279292", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": ["title", "dt", "enabled", "column_break_conqh", "apply_on", "section_break_5", "attributes"], + "fields": [ + { + "fieldname": "dt", + "fieldtype": "Link", + "label": "DocType", + "options": "DocType" + }, + { + "default": "0", + "fieldname": "enabled", + "fieldtype": "Check", + "label": "Enabled" + }, + { + "fieldname": "apply_on", + "fieldtype": "Dynamic Link", + "label": "Apply On", + "options": "dt" + }, + { + "fieldname": "column_break_conqh", + "fieldtype": "Column Break" + }, + { + "fieldname": "section_break_5", + "fieldtype": "Section Break", + "label": "Attributes" + }, + { + "fieldname": "attributes", + "fieldtype": "Table", + "options": "Specification Attribute" + }, + { + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title" + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-06-08 11:21:58.918995", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Specification", + "naming_rule": "Set by user", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Item Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Website Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Master Manager", + "share": 1, + "write": 1 + }, + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "title" +} diff --git a/inventory_tools/inventory_tools/doctype/specification/specification.py b/inventory_tools/inventory_tools/doctype/specification/specification.py new file mode 100644 index 0000000..9b61a64 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification/specification.py @@ -0,0 +1,356 @@ +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +import datetime +import json +import time +from pytz import UnknownTimeZoneError, timezone + +import frappe +from frappe.core.doctype.doctype.doctype import no_value_fields, table_fields +from frappe.model.document import Document +from frappe.query_builder import DocType +from frappe.utils.data import flt, get_datetime + +from erpnext.controllers.queries import get_fields + + +class Specification(Document): + def validate(self): + self.title = f"{self.dt}" + if self.apply_on: + self.title += f" - {self.apply_on}" + + def create_linked_values(self, doc, extra_attributes=None): + for at in self.attributes: + if at.field: + existing_attribute_value = frappe.db.get_value( + "Specification Value", + { + "reference_doctype": at.applied_on, + "reference_name": doc.name, + "attribute": at.attribute_name, + "specification": self.name, + }, + ) + if existing_attribute_value: + av = frappe.get_doc("Specification Value", existing_attribute_value) + av.value = doc.get(at.field) + else: + av = frappe.new_doc("Specification Value") + av.reference_doctype = at.applied_on + av.reference_name = doc.name + av.specification = self.name + av.attribute = at.attribute_name + av.field = at.field + av.value = doc.get(at.field) + if at.date_values: + av.value = convert_to_epoch(av.value) + if av.value: + av.save() + if extra_attributes and at.attribute_name in extra_attributes: + if isinstance(extra_attributes[at.attribute_name], (str, int, float)): + existing_attribute_value = frappe.db.get_value( + "Specification Value", + { + "reference_doctype": at.applied_on, + "reference_name": doc.name, + "attribute": at.attribute_name, + }, + ) + if existing_attribute_value: + av = frappe.get_doc("Specification Value", existing_attribute_value) + else: + av = frappe.new_doc("Specification Value") + av.reference_doctype = at.applied_on + av.reference_name = doc.name + av.attribute = at.attribute_name + av.value = extra_attributes[at.attribute_name] + if at.date_values: + av.value = convert_to_epoch(av.value) + av.save() + continue + + if not extra_attributes: + continue + + for value in extra_attributes[at.attribute_name]: # list, tuple or set / not dict + existing_attribute_value = frappe.db.get_value( + "Specification Value", + { + "reference_doctype": at.applied_on, + "reference_name": doc.name, + "attribute": at.attribute_name, + }, + ) + if existing_attribute_value: + av = frappe.get_doc("Specification Value", existing_attribute_value) + else: + av = frappe.new_doc("Specification Value") + av.reference_doctype = at.applied_on + av.reference_name = doc.name + av.attribute = at.attribute_name + av.value = value + if at.date_values: + av.value = convert_to_epoch(av.value) + av.save() + + @property + def applied_on_doctypes(self): + return [r.applied_on for r in self.attributes] + + def applies_to(self, doc): + if doc.doctype not in self.applied_on_doctypes: + return + for field in doc.meta.fields: + if field.options == self.dt and not self.apply_on: + return True + if field.options == self.dt and doc.get(field.fieldname) == self.apply_on: + return True + + +def convert_to_epoch(date): + tzname = time.tzname if isinstance(time.tzname, (int, str)) else time.tzname[0] + + try: + tz = timezone(tzname) + except UnknownTimeZoneError: + # default to beginning of epoch + return + + d = datetime.datetime.now(tz) # or some other local date + utc_offset = d.utcoffset() + if utc_offset: + utc_offset_seconds = utc_offset.total_seconds() + offset_d = ( + get_datetime(date) - datetime.timedelta(hours=12, seconds=int(utc_offset_seconds)) + ) - get_datetime("1970-01-01") + return offset_d.total_seconds() + return + + +def convert_from_epoch(date): + system_settings = frappe.get_cached_doc("System Settings", "System Settings") + d = datetime.datetime.utcfromtimestamp(int(flt(date))) + utc_offset = d.utcoffset().total_seconds() if d.utcoffset() else 0 + return (d + datetime.timedelta(hours=12, seconds=int(utc_offset))).date() + + +@frappe.whitelist() +def get_data_fieldnames(doctype): + meta = frappe.get_meta(doctype) + return sorted( + f.fieldname for f in meta.fields if f.fieldtype not in no_value_fields + table_fields + ) + + +def get_applicable_specification(doc): + doc = frappe.get_doc(json.loads(doc)) if isinstance(doc, str) else doc + if doc.doctype != "Item": # implement other doctypes later + return + applicable_specifications = [] + specification_candidates = frappe.db.sql( + """ + SELECT DISTINCT `tabSpecification`.name, + `tabSpecification`.dt, + `tabSpecification`.apply_on + FROM `tabSpecification`, `tabSpecification Attribute` + WHERE `tabSpecification`.name = `tabSpecification Attribute`.parent + AND `tabSpecification Attribute`.applied_on = %(doctype)s + """, + {"doctype": doc.doctype}, + as_dict=True, + ) + + i = frappe.get_meta(doc.doctype) + for s in specification_candidates: + if not s.apply_on: + applicable_specifications.append(s.name) + else: + fields = [h.fieldname for h in i.fields if h.options == s.dt] + if any([doc.get(field) == s.apply_on for field in fields]): + applicable_specifications.append(s.name) + + return applicable_specifications + + +""" +Should return a union of existing specification values and specification attributes where values are not present + +""" + + +@frappe.whitelist() +def get_specification_values(reference_doctype, reference_name, specification=None): + _r = [] + if not specification: + specs = get_applicable_specification(frappe.get_doc(reference_doctype, reference_name)) + else: + specs = [specification] + for s in specs: + spec = frappe.get_cached_doc("Specification", s) + for row in spec.attributes: + r = frappe.get_all( + "Specification Value", + filters={ + "reference_doctype": reference_doctype, + "reference_name": reference_name, + "attribute": row.attribute_name, + "specification": spec.name, + }, + fields=["name AS row_name", "attribute", "value", "field", "specification"], + order_by="attribute ASC, value ASC", + ) + if not r: + _r.append( + frappe._dict( + { + "row_name": None, + "attribute": row.attribute_name, + "value": None, + "field": row.field, + "specification": spec.name, + } + ) + ) + else: + [_r.append(i) for i in r] + + return _r + + +@frappe.whitelist() +def create_specification_values( + spec, specifications=None, reference_doctype=None, reference_name=None +): + if isinstance(specifications, str): + specifications = [frappe._dict(**s) for s in json.loads(specifications)] + specification = frappe.get_doc("Specification", spec) + specification_values = frappe._dict( + { + s.get("attribute"): s.get("value") + for s in specifications + if not s.get("field") and s.get("value") + } + ) + for row in specification.attributes: + if not row.field and not specification_values.get(row.attribute_name): + continue + value = specification_values.get(row.attribute_name) or None + if frappe.flags.in_test: + _create_specification_values(specification, row, value) + else: + frappe.enqueue( + _create_specification_values, + specification=specification, + attribute=row, + value=value, + queue="short", + ) + + +def _create_specification_values(specification, attribute, value): + apply_on_filters = [] + if specification.apply_on: + apply_on_filters.append([frappe.scrub(specification.dt), "=", specification.apply_on]) + applicable_documents = frappe.get_all(attribute.applied_on, filters=apply_on_filters) + for doc in applicable_documents: + if not value: + value = frappe.get_value(attribute.applied_on, doc.name, attribute.field) + if not value: + continue + values = frappe._dict( + { + "attribute": attribute.attribute_name, + "field": attribute.field, + "specification": specification.name, + "reference_doctype": attribute.applied_on, + "reference_name": doc.name, + "value": value, + } + ) + if not frappe.get_all("Specification Value", filters=values): + s = frappe.new_doc("Specification Value") + s.update(values) + s.save() + + +@frappe.whitelist() +def update_specification_values(reference_doctype, reference_name, spec=None, specifications=None): + if isinstance(specifications, str): + specifications = json.loads(specifications) + specifications = [frappe._dict(**s) for s in specifications] + # convert dates to epoch + existing_values = get_specification_values(reference_doctype, reference_name) + # print('existing_values', reference_name) + # [print(existing_value) for existing_value in existing_values] + for s in specifications: + if not s.row_name and not s.value and not s.field: + continue + if existing_values: + for row in existing_values: + # print(row) + if row.row_name and row.row_name == s.row_name and row.value != s.value: + if not s.value: + frappe.delete_doc("Specification Value", row.row_name) + continue + date_values = frappe.get_value( + "Specification Attribute", {"parent": spec, "attribute_name": s.attribute}, ["date_values"] + ) + if date_values: + s.value = convert_to_epoch(s.value) + frappe.set_value("Specification Value", s.row_name, "value", s.value) + if not s.row_name: + av = frappe.new_doc("Specification Value") + av.reference_doctype = reference_doctype + av.reference_name = reference_name + av.attribute = s.attribute + av.specification = s.specification + if s.field: + s.value = frappe.get_value(reference_doctype, reference_name, s.field) + av.value = s.value + # TODO: + date_values = frappe.get_value( + "Specification Attribute", {"parent": spec, "attribute_name": s.attribute}, ["date_values"] + ) + if date_values: + av.value = convert_to_epoch(av.value) + if av.value: + av.save() + + +@frappe.whitelist() +def get_apply_on_fields(doctype): + Spec = DocType("Specification") + SpecAttr = DocType("Specification Attribute") + query = ( + frappe.qb.from_(Spec) + .select(Spec.dt, Spec.apply_on) + .inner_join(SpecAttr) + .on(Spec.name == SpecAttr.parent) + .where(Spec.enabled == 1) + ) + if doctype != "Specification": + query = query.where(SpecAttr.applied_on == doctype) + return query.distinct().run(as_dict=True) + + +@frappe.whitelist() +# @frappe.readonly() +@frappe.validate_and_sanitize_search_inputs +def specification_query(doctype, txt, searchfield, start, page_len, filters): + _filters = {} + if filters.get("reference_doctype") != "Specification": + doc = frappe.get_doc(filters.get("reference_doctype"), filters.get("reference_name")) + _filters["name"] = ["in", get_applicable_specification(doc)] + + search_fields = get_fields("Specification") + specifications = frappe.get_all( + "Specification", + filters=_filters, + fields=search_fields, + limit_start=start, + limit_page_length=page_len, + as_list=1, + ) + return specifications diff --git a/inventory_tools/inventory_tools/doctype/specification_attribute/__init__.py b/inventory_tools/inventory_tools/doctype/specification_attribute/__init__.py new file mode 100644 index 0000000..6b9109e --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_attribute/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt diff --git a/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json new file mode 100644 index 0000000..73ea1c0 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.json @@ -0,0 +1,94 @@ +{ + "actions": [], + "creation": "2023-11-13 16:49:36.888483", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "attribute_name", + "numeric_values", + "date_values", + "required", + "column_break_k0wzm", + "applied_on", + "field", + "section_break_lbzwl", + "component" + ], + "fields": [ + { + "columns": 1, + "fieldname": "attribute_name", + "fieldtype": "Data", + "in_list_view": 1, + "label": "Attribute Name", + "reqd": 1, + "unique": 1 + }, + { + "columns": 1, + "default": "0", + "depends_on": "eval: !doc.date_values", + "fieldname": "numeric_values", + "fieldtype": "Check", + "label": "Numeric Values" + }, + { + "fieldname": "column_break_k0wzm", + "fieldtype": "Column Break" + }, + { + "default": "0", + "fieldname": "required", + "fieldtype": "Check", + "hidden": 1, + "label": "Required" + }, + { + "columns": 2, + "fieldname": "field", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Field" + }, + { + "columns": 2, + "fieldname": "applied_on", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Applied On", + "options": "DocType" + }, + { + "fieldname": "section_break_lbzwl", + "fieldtype": "Section Break" + }, + { + "fieldname": "component", + "fieldtype": "Select", + "label": "Component", + "options": "AttributeFilter\nFacetedSearchColorPicker\nFacetedSearchDateRange\nFacetedSearchNumericRange" + }, + { + "default": "0", + "depends_on": "eval: !doc.numeric_values", + "fieldname": "date_values", + "fieldtype": "Check", + "label": "Date Values" + } + ], + "icon": "fa fa-edit", + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-04-11 07:50:09.799568", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Specification Attribute", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "track_changes": 1 +} diff --git a/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.py b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.py new file mode 100644 index 0000000..1b5c767 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_attribute/specification_attribute.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SpecificationAttribute(Document): + pass diff --git a/inventory_tools/inventory_tools/doctype/specification_value/__init__.py b/inventory_tools/inventory_tools/doctype/specification_value/__init__.py new file mode 100644 index 0000000..6b9109e --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.js b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.js new file mode 100644 index 0000000..0244537 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.js @@ -0,0 +1,7 @@ +// Copyright (c) 2023, AgriTheory and contributors +// For license information, please see license.txt + +frappe.ui.form.on('Specification Value', { + // refresh: function(frm) { + // } +}) diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json new file mode 100644 index 0000000..2101597 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.json @@ -0,0 +1,89 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2023-11-14 16:01:49.101184", + "default_view": "List", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": ["reference_doctype", "reference_name", "specification", "field", "attribute", "value"], + "fields": [ + { + "fieldname": "attribute", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Attribute", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "value", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Value", + "reqd": 1 + }, + { + "fieldname": "reference_doctype", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference Doctype", + "options": "DocType", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "reference_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Reference Name", + "options": "reference_doctype", + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "field", + "fieldtype": "Data", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Field", + "read_only": 1 + }, + { + "fieldname": "specification", + "fieldtype": "Link", + "label": "Specification", + "options": "Specification", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "links": [], + "modified": "2024-06-08 11:46:12.702788", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Specification Value", + "owner": "Administrator", + "permissions": [ + { + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, + "write": 1 + } + ], + "sort_field": "modified", + "sort_order": "DESC", + "states": [], + "title_field": "value" +} diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value.py b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.py new file mode 100644 index 0000000..18cd589 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value.py @@ -0,0 +1,9 @@ +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class SpecificationValue(Document): + pass diff --git a/inventory_tools/inventory_tools/doctype/specification_value/specification_value_list.js b/inventory_tools/inventory_tools/doctype/specification_value/specification_value_list.js new file mode 100644 index 0000000..75a1c89 --- /dev/null +++ b/inventory_tools/inventory_tools/doctype/specification_value/specification_value_list.js @@ -0,0 +1,8 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +frappe.listview_settings['Specification Value'] = { + // refresh: function(frm) { + // } + hide_name_column: true, +} diff --git a/inventory_tools/inventory_tools/faceted_search.py b/inventory_tools/inventory_tools/faceted_search.py new file mode 100644 index 0000000..b8c8872 --- /dev/null +++ b/inventory_tools/inventory_tools/faceted_search.py @@ -0,0 +1,220 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import json +from time import localtime + +import frappe +from webshop.webshop.api import get_product_filter_data as webshop_get_product_filter_data +from webshop.webshop.product_data_engine.filters import ProductFiltersBuilder +from webshop.webshop.product_data_engine.query import ProductQuery +from webshop.webshop.doctype.override_doctype.item_group import get_child_groups_for_website +from frappe.utils.data import cint, flt, getdate + +from inventory_tools.inventory_tools.doctype.specification.specification import convert_to_epoch +from inventory_tools.inventory_tools.faceted_search_query import FacetedSearchQuery + + +@frappe.whitelist(allow_guest=True) +def show_faceted_search_components(doctype="Item", filters=None): + attributes = frappe.get_all( + "Specification Attribute", + {"applied_on": doctype}, + [ + "component", + "attribute_name", + "numeric_values", + "date_values", + "name AS attribute_id", + ], + order_by="idx ASC", + ) + components = { + attribute.attribute_name: {**attribute, "values": set(), "visible": False} + for attribute in attributes + } + + for attribute in attributes: + values = sorted( + list( + set( + frappe.get_all( + "Specification Value", + {"attribute": attribute.attribute_name, "reference_doctype": doctype}, + pluck="value", + ) + ) + ), + key=lambda x: x or "", + ) + if attribute.numeric_values and values: + _values = [flt(v) for v in values] + _min, _max = min(_values), max(_values) + attribute.values = [_min, _max] + elif attribute.date_values and values: + _values = [localtime(int(flt(v))) for v in values] + _min, _max = min(_values), max(_values) + elif attribute.component == "FacetedSearchColorPicker": + values = [ + tuple(r.values()) + for r in frappe.get_all( + "Color", + [ + "name", + "color", + "image", + ], + order_by="name", + ) + ] + [components[attribute.attribute_name]["values"].add(value) for value in values] + return components + + +@frappe.whitelist(allow_guest=True) +def get_product_filter_data(query_args=None): + its = frappe.get_last_doc("Inventory Tools Settings") + if not its.show_on_website: + return webshop_get_product_filter_data(query_args) + + if isinstance(query_args, str): + query_args = json.loads(query_args) + + query_args = frappe._dict(query_args) + + if query_args: + search = query_args.get("search") + field_filters = query_args.get("field_filters", {}) + attribute_filters = query_args.get("attributes", {}) + sort_order = query_args.get("sort_order") if query_args.get("sort_order") else "" + start = cint(query_args.start) if query_args.get("start") else 0 + item_group = query_args.get("item_group") + from_filters = query_args.get("from_filters") + else: + search, attribute_filters, item_group, from_filters = None, None, None, None + field_filters = {} + start = 0 + sort_order = "" + + if from_filters: + start = 0 + + sub_categories = [] + if item_group: + sub_categories = get_child_groups_for_website(item_group, immediate=True) + + engine = FacetedSearchQuery() + + # try: + result = engine.query( + attribute_filters, + field_filters, + search_term=search, + start=start, + item_group=item_group, + sort_order=sort_order, + ) + # except Exception: + # frappe.log_error("Product query with filter failed") + # return {"exc": "Something went wrong!"} + + filters = {} + discounts = result["discounts"] + + if discounts: + filter_engine = ProductFiltersBuilder() + filters["discount_filters"] = filter_engine.get_discount_filters(discounts) + + r = { + "items": result["items"] or [], + "filters": filters, + "settings": engine.settings, + "sub_categories": sub_categories, + "items_count": result["items_count"], + } + return r + + +@frappe.whitelist() +def update_specification_attribute_values(doc, method=None): + specifications = frappe.get_all( + "Specification Attribute", + fields=["parent"], + filters={"applied_on": doc.doctype}, + pluck="parent", + distinct=True, + ) + if not specifications: + return + for spec in specifications: + spec = frappe.get_doc("Specification", spec) + if spec.applies_to(doc): + spec.create_linked_values(doc) + + +@frappe.whitelist() +def get_specification_items(attributes): + attributes = json.loads(attributes) if isinstance(attributes, str) else attributes + specification_items = set() + + attributes_in_use = {k: v for (k, v) in attributes.items() if v} + for attribute, spec_and_values in attributes_in_use.items(): + spec = spec_and_values.get("attribute_id") + values = spec_and_values.get("values") + if not values: + continue + if not isinstance(values, list): + values = [values] + filters = None + + date_or_numeric = frappe.get_value( + "Specification Attribute", spec, ["numeric_values", "date_values"], as_dict=True + ) + if date_or_numeric.numeric_values == 1: + values[0], values[-1] = ( + flt(values[0]) if values[0] else None, + flt(values[-1]) if values[-1] else None, + ) + if values[0] and values[-1] and values[0] > values[-1]: + values[0], values[-1] = values[-1], values[0] + filters = [ + ["attribute", "=", attribute], + ] + if values[0]: + filters.append( + ["value", ">=", flt(values[0])], + ) + if values[-1]: + filters.append( + ["value", "<=", flt(values[-1])], + ) + + elif date_or_numeric.date_values == 1: + filters = [ + ["attribute", "=", attribute], + [ + "value", + ">=", + convert_to_epoch(getdate(values[0])) if values[0] else convert_to_epoch(getdate("1900-1-1")), + ], + [ + "value", + "<=", + convert_to_epoch(getdate(values[-1])) + if values[-1] + else convert_to_epoch(getdate("2100-12-31")), + ], + ] + else: + filters = { + "attribute": attribute, + "value": ["in", values], + } + item_codes = frappe.get_all( + "Specification Value", + filters=filters, + pluck="reference_name", + ) + specification_items.update(item_codes) + + return list(specification_items) diff --git a/inventory_tools/inventory_tools/faceted_search_query.py b/inventory_tools/inventory_tools/faceted_search_query.py new file mode 100644 index 0000000..75fa47f --- /dev/null +++ b/inventory_tools/inventory_tools/faceted_search_query.py @@ -0,0 +1,486 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import json + +import frappe +from erpnext.accounts.doctype.pricing_rule.pricing_rule import get_pricing_rule_for_item +from erpnext.stock.get_item_details import get_conversion_factor +from webshop.webshop.api import * +from webshop.webshop.doctype.webshop_settings.webshop_settings import ( + get_shopping_cart_settings, + show_quantity_in_website, +) +from webshop.webshop.doctype.override_doctype.item_group import get_item_for_list_in_html +from webshop.webshop.doctype.item_review.item_review import get_customer +from webshop.webshop.product_data_engine.query import ProductQuery +from webshop.webshop.shopping_cart.cart import _get_cart_quotation, _set_price_list, get_party +from webshop.webshop.utils.product import ( + get_non_stock_item_status, + adjust_qty_for_expired_items, +) +from frappe.utils.data import cint, flt, getdate, fmt_money + +from inventory_tools.inventory_tools.doctype.specification.specification import ( + convert_to_epoch, +) + +SORT_ORDER_LOOKUP = { + "Title A-Z": "item_name ASC, ranking DESC", + "Title Z-A": "item_name DESC, ranking DESC", + "Item Code A-Z": "item_code ASC, ranking DESC", + "Item Code Z-A": "item_code DESC, ranking DESC", +} + + +class FacetedSearchQuery(ProductQuery): + def query( + self, + attributes=None, + fields=None, + search_term=None, + start=0, + item_group=None, + sort_order="", + ): + # track if discounts included in field filters + self.filter_with_discount = bool(fields.get("discount")) + result, discount_list, website_item_groups, cart_items, count = [], [], [], [], 0 + + if fields: + self.build_fields_filters(fields) + if item_group: + self.build_item_group_filters(item_group) + if search_term: + self.build_search_filters(search_term) + if self.settings.hide_variants: + self.filters.append(["variant_of", "is", "not set"]) + + sort_order = SORT_ORDER_LOOKUP.get(sort_order) if sort_order else "item_name ASC" + + # query results + if attributes: + result, count = self.query_items_with_attributes(attributes, start, sort_order=sort_order) + else: + result, count = self.query_items(start=start, sort_order=sort_order) + + # sort combined results by ranking + result = sorted(result, key=lambda x: x.get("ranking"), reverse=True) + + if self.settings.enabled: + cart_items = self.get_cart_items() + + result, discount_list = self.add_display_details(result, discount_list, cart_items) + + discounts = [] + if discount_list: + discounts = [min(discount_list), max(discount_list)] + + result = self.filter_results_by_discount(fields, result) + + return {"items": result, "items_count": count, "discounts": discounts} + + def query_items(self, start=0, sort_order=""): + """Build a query to fetch Website Items based on field filters.""" + # MySQL does not support offset without limit, + # frappe does not accept two parameters for limit + # https://dev.mysql.com/doc/refman/8.0/en/select.html#id4651989 + count_items = frappe.db.get_all( + "Website Item", + filters=self.filters, + or_filters=self.or_filters, + limit_page_length=184467440737095516, + limit_start=start, # get all items from this offset for total count ahead + order_by=sort_order, + # debug=True + ) + count = len(count_items) + + # If discounts included, return all rows. + # Slice after filtering rows with discount (See `filter_results_by_discount`). + # Slicing before hand will miss discounted items on the 3rd or 4th page. + # Discounts are fetched on computing Pricing Rules so we cannot query them directly. + page_length = 184467440737095516 if self.filter_with_discount else self.page_length + + items = frappe.db.get_all( + "Website Item", + fields=self.fields, + filters=self.filters, + or_filters=self.or_filters, + limit_page_length=page_length, + limit_start=start, + order_by=sort_order, + # debug=True + ) + + return items, count + + def query_items_with_attributes(self, attributes, start=0, sort_order=""): + item_codes = get_specification_items(attributes) + + if item_codes: + item_codes = list(set.intersection(*item_codes)) + self.filters.append(["item_code", "in", item_codes]) + + return self.query_items(start=start, sort_order=sort_order) + + +@frappe.whitelist() +def get_specification_items(attributes, start=0, sort_order=""): + attributes = json.loads(attributes) if isinstance(attributes, str) else attributes + item_codes = [] + + attributes_in_use = {k: v for (k, v) in attributes.items() if v} + for attribute, spec_and_values in attributes_in_use.items(): + spec = spec_and_values.get("attribute_id") + values = spec_and_values.get("values") + if not values: + continue + if not isinstance(values, list): + values = [values] + filters = None + + date_or_numeric = frappe.get_value( + "Specification Attribute", spec, ["numeric_values", "date_values"] + ) + if date_or_numeric[0] == 1: + values[0], values[-1] = ( + flt(values[0]) if values[0] else None, + flt(values[-1]) if values[-1] else None, + ) + if values[0] and values[-1] and values[0] > values[-1]: + values[0], values[-1] = values[-1], values[0] + filters = [ + ["attribute", "=", attribute], + ] + if values[0]: + filters.append( + ["value", ">=", flt(values[0])], + ) + if values[-1]: + filters.append( + ["value", "<=", flt(values[-1])], + ) + + elif date_or_numeric[1] == 1: + filters = [ + ["attribute", "=", attribute], + [ + "value", + ">=", + convert_to_epoch(getdate(values[0])) if values[0] else convert_to_epoch(getdate("1900-1-1")), + ], + [ + "value", + "<=", + convert_to_epoch(getdate(values[-1])) + if values[-1] + else convert_to_epoch(getdate("2100-12-31")), + ], + ] + else: + filters = { + "attribute": attribute, + "value": ["in", values], + } + item_code_list = frappe.get_all( + "Specification Value", + fields=["reference_name"], + filters=filters, # debug=True + ) + item_codes.append({x.reference_name for x in item_code_list}) + + if item_codes: + return list(item_codes) + + def add_display_details(self, result, discount_list, cart_items): + """Add price and availability details in result.""" + for item in result: + product_info = _get_product_info_for_website(item.item_code, skip_quotation_creation=True).get( + "product_info" + ) + + if product_info and product_info["price"]: + # update/mutate item and discount_list objects + self.get_price_discount_info(item, product_info["price"], discount_list) + + if self.settings.show_stock_availability: + self.get_stock_availability(item) + + item.in_cart = item.item_code in cart_items + + item.wished = False + if frappe.db.exists( + "Wishlist Item", {"item_code": item.item_code, "parent": frappe.session.user} + ): + item.wished = True + + return result, discount_list + + +def _get_product_info_for_website(item_code, skip_quotation_creation=False): + cart_settings = get_shopping_cart_settings() + if not cart_settings.enabled: + # return settings even if cart is disabled + return frappe._dict({"product_info": {}, "cart_settings": cart_settings}) + + cart_quotation = frappe._dict() + if not skip_quotation_creation: + cart_quotation = _get_cart_quotation() + + selling_price_list = ( + cart_quotation.get("selling_price_list") + if cart_quotation + else _set_price_list(cart_settings, None) + ) + + customer = get_customer(silent=True) + if customer: + customer_group, customer_warehouse = frappe.db.get_value( + "Customer", customer, ["customer_group", "up_default_warehouse"] + ) + if not customer_warehouse: + customer_warehouse = "Slauson Facility - UPIL" + customer_uoms = frappe.get_all( + "Item Customer Detail", + {"parent": item_code}, + "uom", + or_filters={"customer_name": customer, "customer_group": customer_group}, + pluck="uom", + ) + uom = customer_uoms[0] if customer_uoms else None + + price = {} + if cart_settings.show_price: + is_guest = frappe.session.user == "Guest" + # Show Price if logged in. + # If not logged in, check if price is hidden for guest. + if not is_guest or not cart_settings.hide_price_for_guest: + price = _get_price( + item_code, + selling_price_list, + cart_settings.default_customer_group, + cart_settings.company, + uom=uom, + ) + + stock_status = None + + if cart_settings.show_stock_availability: + on_backorder = frappe.get_cached_value("Website Item", {"item_code": item_code}, "on_backorder") + if on_backorder: + stock_status = frappe._dict({"on_backorder": True}) + else: + stock_status = get_web_item_qty_in_stock(item_code, "website_warehouse", customer_warehouse) + + other_uoms = frappe.get_all("UOM Conversion Detail", ["uom"], {"parent": item_code}, pluck="uom") + if customer_uoms: + other_uoms = [o for o in other_uoms if o in customer_uoms] + if not uom: + uom = frappe.db.get_value("Item", item_code, "stock_uom") + sales_uom = frappe.db.get_value("Item", item_code, "sales_uom") + + product_info = { + "price": price, + "qty": 0, + "uom": uom, + "sales_uom": sales_uom or uom, + "other_uoms": other_uoms, + } + + if stock_status: + if stock_status.on_backorder: + product_info["on_backorder"] = True + else: + product_info["stock_qty"] = stock_status.stock_qty + product_info["in_stock"] = ( + stock_status.in_stock + if stock_status.is_stock_item + else get_non_stock_item_status(item_code, "website_warehouse") + ) + product_info["show_stock_qty"] = show_quantity_in_website() + + if product_info["price"]: + if frappe.session.user != "Guest": + item = cart_quotation.get({"item_code": item_code}) if cart_quotation else None + if item: + product_info["qty"] = int(item[0].qty) + + return frappe._dict({"product_info": product_info, "cart_settings": cart_settings}) + + +def set_product_info_for_website(item): + """set product price uom for website""" + product_info = _get_product_info_for_website(item.item_code, skip_quotation_creation=True).get( + "product_info" + ) + + if product_info: + item.update(product_info) + item["stock_uom"] = product_info.get("uom") + item["sales_uom"] = product_info.get("sales_uom") + if product_info.get("price"): + item["price_stock_uom"] = product_info.get("price").get("formatted_price") + item["price_sales_uom"] = product_info.get("price").get("formatted_price_sales_uom") + else: + item["price_stock_uom"] = "" + item["price_sales_uom"] = "" + + +@frappe.whitelist(allow_guest=True) +def get_product_list(search=None, start=0, limit=12): + data = get_product_data(search, start, limit) + + for item in data: + set_product_info_for_website(item) + + return [get_item_for_list_in_html(r) for r in data] + + +def _get_price(item_code, price_list, customer_group, company, qty=1, uom=None): + template_item_code = frappe.db.get_value("Item", item_code, "variant_of") + + if price_list: + price = frappe.get_all( + "Item Price", + fields=["price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": item_code}, + ) + + if template_item_code and not price: + price = frappe.get_all( + "Item Price", + fields=["price_list_rate", "currency"], + filters={"price_list": price_list, "item_code": template_item_code}, + ) + + if price: + party = get_party() + stock_uom = frappe.db.get_value("Item", item_code, "stock_uom") + customer_group = frappe.db.get_value("Customer", party.name, "customer_group") + uom = frappe.get_all( + "Item Customer Detail", + {"parent": item_code}, + "uom", + or_filters={"customer_name": party.name, "customer_group": customer_group}, + ) + if uom: + uom = uom[0].get("uom") + else: + sales_uom, stock_uom = frappe.db.get_value("Item", item_code, ["sales_uom", "stock_uom"]) + uom = sales_uom if sales_uom else stock_uom + + conversion_factor = get_conversion_factor(item_code, uom).get("conversion_factor", 1) + pricing_rule_dict = frappe._dict( + { + "item_code": item_code, + "qty": qty, + "uom": uom, + "transaction_type": "selling", + "price_list": price_list, + "customer_group": customer_group, + "company": company, + "conversion_rate": conversion_factor, + "for_shopping_cart": True, + "currency": frappe.db.get_value("Price List", price_list, "currency"), + } + ) + + if party and party.doctype == "Customer": + pricing_rule_dict.update({"customer": party.name}) + + pricing_rule = get_pricing_rule_for_item(pricing_rule_dict) + price_obj = price[0] + + if pricing_rule: + # price without any rules applied + mrp = price_obj.price_list_rate or 0 + + if pricing_rule.pricing_rule_for == "Discount Percentage": + price_obj.discount_percent = pricing_rule.discount_percentage + price_obj.formatted_discount_percent = str(flt(pricing_rule.discount_percentage, 0)) + "%" + price_obj.price_list_rate = flt( + price_obj.price_list_rate * (1.0 - (flt(pricing_rule.discount_percentage) / 100.0)) + ) + + if pricing_rule.pricing_rule_for == "Rate": + rate_discount = flt(mrp) - flt(pricing_rule.price_list_rate) + if rate_discount > 0: + price_obj.formatted_discount_rate = fmt_money(rate_discount, currency=price_obj["currency"]) + price_obj.price_list_rate = pricing_rule.price_list_rate or 0 + + if price_obj: + price_obj["formatted_price"] = fmt_money( + price_obj["price_list_rate"], currency=price_obj["currency"] + ) + if mrp != price_obj["price_list_rate"]: + price_obj["formatted_mrp"] = fmt_money(mrp, currency=price_obj["currency"]) + + price_obj["currency_symbol"] = ( + not cint(frappe.db.get_default("hide_currency_symbol")) + and ( + frappe.db.get_value("Currency", price_obj.currency, "symbol", cache=True) + or price_obj.currency + ) + or "" + ) + + uom_conversion_factor = get_conversion_factor(item_code, uom) + uom_conversion_factor = uom_conversion_factor.get("conversion_factor") or 1 + price_obj["formatted_price_sales_uom"] = fmt_money( + price_obj["price_list_rate"] * uom_conversion_factor, + currency=price_obj["currency"], + ) + if uom_conversion_factor != 1: + price_obj["formatted_price"] = price_obj["formatted_price_sales_uom"] + + if not price_obj["price_list_rate"]: + price_obj["price_list_rate"] = price_obj["price_list_rate"] * uom_conversion_factor + + if not price_obj["currency"]: + price_obj["currency"] = "" + + if not price_obj["formatted_price"]: + price_obj["formatted_price"], price_obj["formatted_mrp"] = "", "" + + return price_obj + + +def get_web_item_qty_in_stock(item_code, item_warehouse_field, warehouse=None): + in_stock, stock_qty = 0, "" + template_item_code, is_stock_item = frappe.db.get_value( + "Item", item_code, ["variant_of", "is_stock_item"] + ) + + if not warehouse: + warehouse = frappe.db.get_value("Website Item", {"item_code": item_code}, item_warehouse_field) + + if not warehouse and template_item_code and template_item_code != item_code: + warehouse = frappe.db.get_value( + "Website Item", {"item_code": template_item_code}, item_warehouse_field + ) + + if warehouse: + lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) + stock_qty = frappe.db.sql( + """ + SELECT SUM(GREATEST(`tabBin`.actual_qty - `tabBin`.reserved_qty - `tabBin`.reserved_qty_for_production - `tabBin`.reserved_qty_for_sub_contract, 0)) AS stock_qty + FROM `tabBin`, `tabItem`, `tabWarehouse` + WHERE + `tabWarehouse`.lft >= %(lft)s + AND `tabWarehouse`.rgt <= %(rgt)s + AND `tabItem`.name = `tabBin`.item_code + AND `tabItem`.name = %(item_code)s + AND `tabWarehouse`.name = `tabBin`.warehouse + AND `tabWarehouse`.up_purpose = 'Storage' + """, + {"item_code": item_code, "lft": lft, "rgt": rgt}, + ) + + if stock_qty: + stock_qty = adjust_qty_for_expired_items(item_code, stock_qty, warehouse) + in_stock = stock_qty[0][0] > 0 and 1 or 0 + + return frappe._dict( + {"in_stock": in_stock, "stock_qty": stock_qty, "is_stock_item": is_stock_item} + ) diff --git a/inventory_tools/inventory_tools/overrides/production_plan.py b/inventory_tools/inventory_tools/overrides/production_plan.py index 5485fbb..fae8735 100644 --- a/inventory_tools/inventory_tools/overrides/production_plan.py +++ b/inventory_tools/inventory_tools/overrides/production_plan.py @@ -1,10 +1,13 @@ # Copyright (c) 2024, AgriTheory and contributors # For license information, please see license.txt + +import json + import frappe +from frappe import _ from erpnext.manufacturing.doctype.production_plan.production_plan import ProductionPlan from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse -from frappe import _ class InventoryToolsProductionPlan(ProductionPlan): diff --git a/inventory_tools/inventory_tools/overrides/purchase_invoice.py b/inventory_tools/inventory_tools/overrides/purchase_invoice.py index c0d6a7d..5636713 100644 --- a/inventory_tools/inventory_tools/overrides/purchase_invoice.py +++ b/inventory_tools/inventory_tools/overrides/purchase_invoice.py @@ -13,7 +13,7 @@ class InventoryToolsPurchaseInvoice(PurchaseInvoice): def validate_with_previous_doc(self): """ - HASH: 804f1d4772e80994be17b276d9a0af7b66dde20d + HASH: 183ac4155046dc5d18f9a28c560b7586c5f12b4b REPO: https://github.com/frappe/erpnext/ PATH: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py METHOD: validate_with_previous_doc diff --git a/inventory_tools/inventory_tools/overrides/purchase_order.py b/inventory_tools/inventory_tools/overrides/purchase_order.py index e1d7a75..1985f86 100644 --- a/inventory_tools/inventory_tools/overrides/purchase_order.py +++ b/inventory_tools/inventory_tools/overrides/purchase_order.py @@ -225,7 +225,7 @@ def make_sales_invoices(docname: str, rows: list | str) -> None: @frappe.whitelist() def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): """ - HASH: d396c18689e530d3f11a791ef1064c2d9775466e + HASH: 334c4d06766b67a4e35b4ec4fe99a9d8dc07691a REPO: https://github.com/frappe/erpnext/ PATH: erpnext/stock/get_item_details.py METHOD: get_item_details @@ -243,7 +243,7 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru @frappe.whitelist() def validate_item_details(args, item): """ - HASH: d396c18689e530d3f11a791ef1064c2d9775466e + HASH: 334c4d06766b67a4e35b4ec4fe99a9d8dc07691a REPO: https://github.com/frappe/erpnext/ PATH: erpnext/stock/get_item_details.py METHOD: validate_item_details diff --git a/inventory_tools/inventory_tools/overrides/uom.py b/inventory_tools/inventory_tools/overrides/uom.py index 36983be..21fa22d 100644 --- a/inventory_tools/inventory_tools/overrides/uom.py +++ b/inventory_tools/inventory_tools/overrides/uom.py @@ -8,9 +8,13 @@ @frappe.whitelist() def uom_restricted_query(doctype, txt, searchfield, start, page_len, filters): - company = frappe.defaults.get_defaults().get("company") + if "company" in filters: + company = filters.pop("company") + else: + company = frappe.defaults.get_defaults().get("company") + if frappe.get_cached_value("Inventory Tools Settings", company, "enforce_uoms"): - return execute( + return frappe.get_all( "UOM Conversion Detail", filters=filters, fields=["uom", "conversion_factor"], diff --git a/inventory_tools/inventory_tools/overrides/website_item.py b/inventory_tools/inventory_tools/overrides/website_item.py new file mode 100644 index 0000000..10d8459 --- /dev/null +++ b/inventory_tools/inventory_tools/overrides/website_item.py @@ -0,0 +1,37 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe + +from webshop.webshop.doctype.website_item.website_item import WebsiteItem +from inventory_tools.inventory_tools.faceted_search_query import _get_product_info_for_website +from webshop.webshop.shopping_cart.cart import get_party + + +class InventoryToolsWebsiteItem(WebsiteItem): + def set_shopping_cart_data(self, context): + its = frappe.get_last_doc("Inventory Tools Settings") + if not its.show_on_website: + super().set_shopping_cart_data(context) + else: + party = get_party() + quotation = frappe.get_all( + "Quotation", + fields=["name"], + filters={ + "party_name": party.name, + "contact_email": frappe.session.user, + "order_type": "Shopping Cart", + "docstatus": 0, + }, + order_by="modified desc", + limit_page_length=1, + ) + context.shopping_cart = _get_product_info_for_website( + self.item_code, skip_quotation_creation=not bool(quotation) + ) + context.shopping_cart.cart_items = [] + if quotation: + context.shopping_cart.cart_items = frappe.get_all( + "Quotation Item", {"parent": quotation[0].name}, ["item_code"], pluck="item_code" + ) diff --git a/inventory_tools/inventory_tools/overrides/workstation.py b/inventory_tools/inventory_tools/overrides/workstation.py index 43a189e..7907ab8 100644 --- a/inventory_tools/inventory_tools/overrides/workstation.py +++ b/inventory_tools/inventory_tools/overrides/workstation.py @@ -101,7 +101,7 @@ def get_alternative_workstations(doctype, txt, searchfield, start, page_len, fil operation = filters.get("operation") if not operation: - frappe.throw("Please select a Operation first.") + frappe.throw(frappe._("Please select a Operation first.")) searchfields = list(reversed(frappe.get_meta(doctype).get_search_fields())) select = ",\n".join([f"`tabWorkstation`.{field}" for field in searchfields]) @@ -124,10 +124,11 @@ def get_alternative_workstations(doctype, txt, searchfield, start, page_len, fil "Workstation", default_workstation_name, searchfields, as_dict=True ) if default_workstation_name not in [row[0] for row in workstation]: + field_values = ",".join([v for k, v in default_workstation_fields[0].items() if k != "name"]) _default = tuple( [ default_workstation_fields[0].name, - f"{frappe.bold('Default')} - {','.join([v for k, v in default_workstation_fields[0].items() if k != 'name'])}", + f"{frappe._('(Default Workstation)')} {' - ' if field_values else '' }{field_values}", ] ) workstation.insert(0, _default) diff --git a/inventory_tools/inventory_tools/report/specification/__init__.py b/inventory_tools/inventory_tools/report/specification/__init__.py new file mode 100644 index 0000000..6b9109e --- /dev/null +++ b/inventory_tools/inventory_tools/report/specification/__init__.py @@ -0,0 +1,2 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt diff --git a/inventory_tools/inventory_tools/report/specification/specification.js b/inventory_tools/inventory_tools/report/specification/specification.js new file mode 100644 index 0000000..70bd9c4 --- /dev/null +++ b/inventory_tools/inventory_tools/report/specification/specification.js @@ -0,0 +1,79 @@ +// Copyright (c) 2023, AgriTheory and contributors +// For license information, please see license.txt + +frappe.query_reports['Specification'] = { + filters: [ + { + fieldname: 'specification', + label: __('Specification'), + options: 'Specification', + fieldtype: 'Link', + reqd: 1, + }, + ], + refresh: reportview => { + this.get_datatable_options(frappe.query_report.datatable.options) + }, + get_datatable_options(options) { + options.columns[3].editable = true + return Object.assign(options, { + getEditor: this.get_editing_object.bind(this), + }) + }, + get_editing_object(colIndex, rowIndex, value, parent) { + if (frappe.query_report.datatable.datamanager.data[rowIndex].field != undefined) { + frappe.show_alert(__('This value cannot be edited in this report')) + return false + } + const control = this.render_editing_input(colIndex, value, parent) + if (!control) return false + control.df.change = () => control.set_focus() + try { + return { + initValue: async value => { + return control.set_value(value) + }, + setValue: value => { + let row = frappe.query_report.datatable.datamanager.data[rowIndex] + let docname = row.name + if (!value) { + return control.get_value() + } + if (row.indent === 0) { + return control.get_value() + } + if (row.billed_date) { + return control.get_value() + } + + return frappe + .xcall('inventory_tools.inventory_tools.report.specification.specification.set_value', { + docname: docname, + value: value, + }) + .catch(e => { + return control.get_value(value) + }) + }, + getValue: async () => { + return control.get_value() + }, + } + } catch (error) { + console.log(error) + } + }, + render_editing_input(colIndex, value, parent) { + const col = frappe.query_report.datatable.getColumn(colIndex) + let control = null + control = frappe.ui.form.make_control({ + df: col, + parent: parent, + render_input: true, + }) + control.set_value(value || '') + control.toggle_label(false) + control.toggle_description(false) + return control + }, +} diff --git a/inventory_tools/inventory_tools/report/specification/specification.json b/inventory_tools/inventory_tools/report/specification/specification.json new file mode 100644 index 0000000..1319248 --- /dev/null +++ b/inventory_tools/inventory_tools/report/specification/specification.json @@ -0,0 +1,29 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2023-11-24 15:51:03.630333", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2023-11-24 15:51:03.630333", + "modified_by": "Administrator", + "module": "Inventory Tools", + "name": "Specification", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "Specification", + "report_name": "Specification", + "report_type": "Script Report", + "roles": [ + { + "role": "System Manager" + }, + { + "role": "Item Manager" + } + ] +} diff --git a/inventory_tools/inventory_tools/report/specification/specification.py b/inventory_tools/inventory_tools/report/specification/specification.py new file mode 100644 index 0000000..83d7c69 --- /dev/null +++ b/inventory_tools/inventory_tools/report/specification/specification.py @@ -0,0 +1,78 @@ +# Copyright (c) 2023, AgriTheory and contributors +# For license information, please see license.txt + +from itertools import groupby + +import frappe + + +def execute(filters=None): + specification = frappe.get_doc("Specification", filters.specification) + return get_columns(filters, specification), get_data(filters, specification) + + +def get_data(filters, specification): + doctypes = [row.applied_on for row in specification.attributes] + attributes = [row.attribute_name for row in specification.attributes] + fields = {row.attribute_name: row.field for row in specification.attributes if row.field} + _data = frappe.get_all( + "Specification Value", + {"reference_doctype": ["in", doctypes], "attribute": ["in", attributes]}, + ["reference_doctype", "reference_name", "attribute", "value", "name"], + order_by="reference_name", + ) + data = [] + for ref, d in groupby(_data, key=lambda x: x.get("reference_name")): + _d = sorted(sorted(list(d), key=lambda x: x.get("value")), key=lambda x: x.get("attribute")) + data.append( + { + "reference_name": frappe.bold(ref), + "indent": 0, + } + ) + for __d in _d: + if __d.attribute in fields: + __d.field = fields[__d.attribute] + __d.indent = 1 + data.append(__d) + return data + + +def get_columns(filters, specification): + return [ + { + "label": f"{specification.dt} - {specification.apply_on}" + if specification.apply_on + else specification.dt, + "fieldname": "reference_name", + "fieldtype": "Link", + "options": "Doctype", + "width": "250px", + }, + { + "label": "DocType", + "fieldname": "reference_doctype", + "fieldtype": "Data", + "width": "250px", + }, + { + "fieldname": "attribute", + "fieldtype": "Data", + "label": "Attribute", + "width": "200px", + }, + { + "fieldname": "value", + "label": "Value", + "fieldtype": "Data", + "width": "250px", + }, + {"fieldname": "field", "fieldtype": "Data", "hidden": 1}, + {"fieldname": "name", "fieldtype": "Data", "hidden": 1}, + ] + + +@frappe.whitelist() +def set_value(docname, value): + frappe.set_value("Specification Value", docname, "value", value) + frappe.msgprint("Updated", alert=True) diff --git a/inventory_tools/patches/rename_alternative_workstation.py b/inventory_tools/patches/rename_alternative_workstation.py index 7ca7c6c..cbf43d5 100644 --- a/inventory_tools/patches/rename_alternative_workstation.py +++ b/inventory_tools/patches/rename_alternative_workstation.py @@ -1,11 +1,14 @@ -import frappe -from frappe.model.rename_doc import rename_doc - - -def execute(): - if frappe.db.exists("DocType", "Alternative Workstations"): - rename_doc( - "DocType", "Alternative Workstations", "Alternative Workstation", ignore_if_exists=True - ) - - frappe.reload_doc("inventory_tools", "doctype", "alternative_workstation", force=True) +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +from frappe.model.rename_doc import rename_doc + + +def execute(): + if frappe.db.exists("DocType", "Alternative Workstations"): + rename_doc( + "DocType", "Alternative Workstations", "Alternative Workstation", ignore_if_exists=True + ) + + frappe.reload_doc("inventory_tools", "doctype", "alternative_workstation", force=True) diff --git a/inventory_tools/public/js/faceted_search/AttributeFilter.vue b/inventory_tools/public/js/faceted_search/AttributeFilter.vue new file mode 100644 index 0000000..e6ceaa8 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/AttributeFilter.vue @@ -0,0 +1,37 @@ + + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearch.vue b/inventory_tools/public/js/faceted_search/FacetedSearch.vue new file mode 100644 index 0000000..9341503 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/FacetedSearch.vue @@ -0,0 +1,275 @@ + + + + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue b/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue new file mode 100644 index 0000000..752cfe2 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/FacetedSearchColorPicker.vue @@ -0,0 +1,130 @@ + + + + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue b/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue new file mode 100644 index 0000000..6937e1b --- /dev/null +++ b/inventory_tools/public/js/faceted_search/FacetedSearchDateRange.vue @@ -0,0 +1,74 @@ + + + + + diff --git a/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue new file mode 100644 index 0000000..2aafcda --- /dev/null +++ b/inventory_tools/public/js/faceted_search/FacetedSearchNumericRange.vue @@ -0,0 +1,69 @@ + + + + + diff --git a/inventory_tools/public/js/faceted_search/env.d.ts b/inventory_tools/public/js/faceted_search/env.d.ts new file mode 100644 index 0000000..b6ca62c --- /dev/null +++ b/inventory_tools/public/js/faceted_search/env.d.ts @@ -0,0 +1,13 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +declare global { + const $: any + const webshop: any + const frappe: any + interface Window { + cur_list: any + } +} + +export {} diff --git a/inventory_tools/public/js/faceted_search/faceted_search.js b/inventory_tools/public/js/faceted_search/faceted_search.js new file mode 100644 index 0000000..b4551f1 --- /dev/null +++ b/inventory_tools/public/js/faceted_search/faceted_search.js @@ -0,0 +1,101 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { createApp } from 'vue' + +import FacetedSearch from './FacetedSearch.vue' +import AttributeFilter from './AttributeFilter.vue' +import FacetedSearchNumericRange from './FacetedSearchNumericRange.vue' +import FacetedSearchDateRange from './FacetedSearchDateRange.vue' +import FacetedSearchColorPicker from './FacetedSearchColorPicker.vue' + +frappe.provide('faceted_search') + +faceted_search.mount = el => { + faceted_search.$search = createApp(FacetedSearch, { props: { doctype: 'Item' } }) + faceted_search.$search.component('AttributeFilter', AttributeFilter) + faceted_search.$search.component('FacetedSearchNumericRange', FacetedSearchNumericRange) + faceted_search.$search.component('FacetedSearchDateRange', FacetedSearchDateRange) + faceted_search.$search.component('FacetedSearchColorPicker', FacetedSearchColorPicker) + faceted_search.$instance = faceted_search.$search.mount(el) +} + +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)) + observer.disconnect() + } + }) + observer.observe(document.body, { + childList: true, + subtree: true, + }) + }) +} + +function mount_listview() { + if (!faceted_search.$search && $('#faceted-search').length === 0 && $('.filter-section').length > 0) { + $('.filter-section').prepend('') + waitForElement('#faceted-search').then(async el => { + faceted_search.mount(el) + }) + } +} + +function mount_ecommerce_view(el) { + faceted_search.mount(el) +} + +waitForElement('[data-route]').then(element => { + const observer = new MutationObserver(() => { + if (frappe.boot.inventory_tools_settings[Object.keys(frappe.boot.inventory_tools_settings)[0]].show_in_listview) { + if (cur_list && cur_list.doctype == 'Item') { + mount_listview() + } + } + }) + const config = { attributes: true, childList: false, characterData: true } + observer.observe(element, config) +}) + +waitForElement('.filter-x-button').then(element => { + cur_list.filter_area.filter_x_button.on('click', () => { + faceted_search.$instance.resetFacets() + }) +}) + +waitForElement('#product-filters').then(element => { + frappe.ready(() => { + frappe + .xcall( + 'inventory_tools.inventory_tools.doctype.inventory_tools_settings.inventory_tools_settings.faceted_search_enabled' + ) + .then(r => { + if (!r.show_on_website) { + return + } + mount_ecommerce_view(element) + waitForElement('.toggle-container').then(element => { + let el = $(element) + el.prepend( + `` + ) + el.on('change', e => { + faceted_search.$instance.updateFilters({ sort_order: e.target.value }) + }) + }) + }) + }) +}) diff --git a/inventory_tools/public/js/faceted_search/specification_dialog.js b/inventory_tools/public/js/faceted_search/specification_dialog.js new file mode 100644 index 0000000..c93a35d --- /dev/null +++ b/inventory_tools/public/js/faceted_search/specification_dialog.js @@ -0,0 +1,222 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +frappe.provide('inventory_tools') + +inventory_tools.specification_dialog = async frm => { + var data = [] + data = await get_specification_values(frm) + let is_new = false + if (!data.length) { + is_new = true + } + let apply_on_fields = await get_apply_on_fields(frm) + let attributes = get_attributes(data) + let fields = [ + { + fieldtype: 'Data', + fieldname: 'specification', + label: __('Specification'), + in_list_view: 1, + read_only: 1, + }, + { + fieldtype: 'Select', + fieldname: 'attribute', + in_list_view: 1, + read_only: 0, + disabled: 0, + label: __('Attribute'), + options: attributes, + onchange: a => { + validate_duplicate_attributes() + }, + }, + { + fieldtype: 'Data', + fieldname: 'field', + label: __('Field'), + in_list_view: 1, + read_only: 1, + }, + { + fieldtype: 'Data', + fieldname: 'value', + label: __('Value'), + in_list_view: 1, + read_only: 0, + }, + ] + return new Promise(resolve => { + let d = new frappe.ui.Dialog({ + title: frm.doc.doctype == 'Specification' ? __('Generate Specification Values') : __('Edit Specification Values'), + fields: [ + { + label: __('Specification'), + fieldname: 'specification', + fieldtype: 'Link', + options: 'Specification', + read_only: frm.doc.doctype == 'Specification' ? 1 : 0, + default: frm.doc.doctype == 'Specification' ? frm.doc.name : '', + onchange: async () => { + values = d.get_values() + if (values) { + let _data = await get_specification_values(frm, values.specification) + if (_data.length) { + d.fields_dict.specs.grid.df.data = [] + data = _data + d.fields_dict.specs.grid.docfields[0].options = get_attributes(data) + _data.forEach((row, index) => { + row.value = '' + if (row.field && frm.doc[row.field]) { + row.value = frm.doc[row.field] + } + d.fields_dict.specs.grid.df.data.push({ + name: `row-${index}`, + idx: d.fields_dict.specs.grid.df.data.length + 1, + __islocal: true, + ...row, + }) + }) + d.fields_dict.specs.grid.refresh() + _data.forEach((row, index) => { + let grid_row = d.fields_dict.specs.grid.get_row(`row-${index}`) + if (row.field) { + grid_row.set_field_property('attribute', 'read_only', 1) + } + grid_row.refresh() + }) + } + d.$wrapper.find('.grid-add-row').show() + } + }, + }, + { + fieldtype: 'Column Break', + fieldname: 'col_break_1', + }, + { + fieldtype: 'Section Break', + fieldname: 'section_break_1', + }, + { + fieldname: 'specs', + fieldtype: 'Table', + in_place_edit: true, + editable_grid: true, + data: data, + fields: fields, + }, + { + fieldtype: 'HTML', + fieldname: 'table-notes', + options: __('

To remove a non-computed value, remove the contents of the "Value" field

'), + }, + ], + primary_action: () => { + let values = d.get_values() + if (!values) { + return + } + validate_duplicate_attributes(values) + let method = 'inventory_tools.inventory_tools.doctype.specification.specification.update_specification_values' + if (frm.doc.doctype == 'Specification') { + method = 'inventory_tools.inventory_tools.doctype.specification.specification.create_specification_values' + } + frappe.xcall(method, { + spec: frm.doc.doctype == 'Specification' ? frm.doc.name : values.specification, + specifications: values.specs, + reference_doctype: frm.doc.doctype == 'Specification' ? '' : frm.doc.doctype, + reference_name: frm.doc.doctype == 'Specification' ? '' : frm.doc.name, + }) + resolve(d.hide()) + }, + primary_action_label: frm.doc.doctype == 'Specification' ? __('Generate') : __('Save'), + size: 'extra-large', + }) + d.show() + d.fields_dict.specification.get_query = () => { + return { + query: 'inventory_tools.inventory_tools.doctype.specification.specification.specification_query', + filters: { + reference_doctype: frm.doc.doctype, + reference_name: frm.doc.name, + }, + } + } + let values = d.get_values() + if (values && values.specification) { + d.$wrapper.find('.grid-add-row').show() + } else { + d.$wrapper.find('.grid-add-row').hide() + } + }) +} + +async function get_specification_values(frm, specification = null) { + let args = {} + if (frm.doc.doctype == 'Specification') { + args = { + reference_doctype: frm.doc.dt, + reference_name: frm.doc.apply_on, + specification: frm.doc.name, + } + return frm.doc.attributes.map(row => { + return { + attribute: row.attribute_name, + field: row.field, + value: null, + } + }) + } else { + args = { + reference_doctype: frm.doc.doctype, + reference_name: frm.doc.name, + specification: specification, + } + return await frappe.xcall( + 'inventory_tools.inventory_tools.doctype.specification.specification.get_specification_values', + args + ) + } +} + +function validate_duplicate_attributes(values = null) { + if (!values) { + values = cur_dialog.get_values() + } + const attributes = values.specs.filter(i => { + if (i.attribute && i.field) { + return i.attribute + } + }) + let is_duplicate = attributes.some((item, idx) => { + return attributes.indexOf(item) != idx + }) + if (is_duplicate) { + frappe.throw(__('Field level duplicates are not permitted in a Specification')) + } +} + +function get_attributes(data) { + if (!data) { + return + } + let r = Array.from( + new Set( + data.map(r => { + if (r.attribute && !r.field) { + return r.attribute + } + }) + ) + ).sort() + r.pop(undefined) + return r +} + +async function get_apply_on_fields(frm) { + return await frappe.xcall('inventory_tools.inventory_tools.doctype.specification.specification.get_apply_on_fields', { + doctype: frm.doc.doctype, + }) +} diff --git a/inventory_tools/public/js/inventory_tools.bundle.js b/inventory_tools/public/js/inventory_tools.bundle.js index 07e1c36..a9c3654 100644 --- a/inventory_tools/public/js/inventory_tools.bundle.js +++ b/inventory_tools/public/js/inventory_tools.bundle.js @@ -3,3 +3,4 @@ import './uom_enforcement.js' import './custom/utils.js' +import './faceted_search/specification_dialog.js' diff --git a/inventory_tools/public/js/uom_enforcement.js b/inventory_tools/public/js/uom_enforcement.js index 82d8b69..32f532b 100644 --- a/inventory_tools/public/js/uom_enforcement.js +++ b/inventory_tools/public/js/uom_enforcement.js @@ -49,7 +49,7 @@ function setup_uom_enforcement(frm) { } return { query: 'inventory_tools.inventory_tools.overrides.uom.uom_restricted_query', - filters: { parent: frm.doc.item_code }, + filters: { parent: frm.doc.item_code, company: frm.doc?.company }, } }) }) @@ -63,7 +63,7 @@ function setup_uom_enforcement(frm) { } return { query: 'inventory_tools.inventory_tools.overrides.uom.uom_restricted_query', - filters: { parent: locals[cdt][cdn].item_code }, + filters: { parent: locals[cdt][cdn].item_code, company: frm.doc?.company }, } }) }) diff --git a/inventory_tools/public/js/vite.config.js b/inventory_tools/public/js/vite.config.js new file mode 100644 index 0000000..ad94684 --- /dev/null +++ b/inventory_tools/public/js/vite.config.js @@ -0,0 +1,26 @@ +// Copyright (c) 2024, AgriTheory and contributors +// For license information, please see license.txt + +import { defineConfig } from 'vite' +import vue from '@vitejs/plugin-vue' +import path from 'path' + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [vue()], + build: { + lib: { + entry: path.resolve(__dirname, './faceted_search/faceted_search.js'), + name: 'inventory_tools', + fileName: format => `inventory_tools.js`, // creates module only output + }, + outDir: './inventory_tools/public/dist/js', + target: 'esnext', + emptyOutDir: false, + minify: false, + }, + optimizeDeps: {}, + define: { + 'process.env': process.env, + }, +}) diff --git a/inventory_tools/tests/fixtures.py b/inventory_tools/tests/fixtures.py index 2e91d82..8d4f7da 100644 --- a/inventory_tools/tests/fixtures.py +++ b/inventory_tools/tests/fixtures.py @@ -204,6 +204,8 @@ "item_code": "Ambrosia Pie", "item_group": "Baked Goods", "uom": "Nos", + "weight_per_unit": 7.5, + "weight_uom": "Pound", "item_price": 11.00, "default_warehouse": "Refrigerated Display - APC", "description": "

Ambrosia Pie is the marquee product of Ambrosia Pie Company. A filling of heavenly cloudberries pair perfectly with the tart hairless rambutan, finished with drizzles of tayberry nectar. It's a feast fit for Mt Olympus!

", @@ -211,6 +213,8 @@ { "item_code": "Double Plum Pie", "uom": "Nos", + "weight_per_unit": 8, + "weight_uom": "Pound", "item_group": "Baked Goods", "item_price": 10.50, "default_warehouse": "Refrigerated Display - APC", @@ -219,6 +223,8 @@ { "item_code": "Gooseberry Pie", "uom": "Nos", + "weight_per_unit": 8.5, + "weight_uom": "Pound", "item_group": "Baked Goods", "item_price": 12.00, "default_warehouse": "Refrigerated Display - APC", @@ -228,6 +234,8 @@ "item_code": "Kaduka Key Lime Pie", "item_group": "Baked Goods", "uom": "Nos", + "weight_per_unit": 9, + "weight_uom": "Pound", "item_price": 11.50, "default_warehouse": "Refrigerated Display - APC", "description": "

Take your tastebuds on an adventure with this whimsical twist on the classic Key Lime pie. Made with kaduka limes and the exotic limequat, this seasonal pie is sure to satisfy even the most weary culinary explorer. Grab it when you can - it's only available April through September.

", @@ -347,6 +355,8 @@ { "item_code": "Cloudberry", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Cloudberry", "item_group": "Ingredients", "item_price": 0.65, @@ -356,6 +366,8 @@ { "item_code": "Cocoplum", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Cocoplum", "item_group": "Ingredients", "item_price": 0.35, @@ -365,6 +377,8 @@ { "item_code": "Damson Plum", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Damson Plum", "item_group": "Ingredients", "item_price": 0.85, @@ -374,6 +388,8 @@ { "item_code": "Gooseberry", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Gooseberry", "item_group": "Ingredients", "item_price": 0.99, @@ -383,6 +399,8 @@ { "item_code": "Hairless Rambutan", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Hairless Rambutan", "item_price": 0.50, "item_group": "Ingredients", @@ -392,6 +410,8 @@ { "item_code": "Kaduka Lime", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Kaduka Lime", "item_group": "Ingredients", "item_price": 0.89, @@ -401,6 +421,8 @@ { "item_code": "Limequat", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Limequat", "item_group": "Ingredients", "item_price": 0.75, @@ -410,6 +432,8 @@ { "item_code": "Tayberry", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Tayberry", "item_group": "Ingredients", "item_price": 0.85, @@ -419,6 +443,8 @@ { "item_code": "Bayberry", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Bayberry", "item_group": "Ingredients", "item_price": 0.45, @@ -428,6 +454,8 @@ { "item_code": "Butter", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Butter", "item_group": "Ingredients", "item_price": 4.5, @@ -440,6 +468,8 @@ { "item_code": "Cornstarch", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Cornstarch", "item_group": "Ingredients", "item_price": 0.52, @@ -449,6 +479,8 @@ { "item_code": "Ice Water", "uom": "Cup", + "weight_uom": "Pound", + "weight_per_unit": 0.52, "description": "Ice Water - necessary for pie crusts", "item_group": "Ingredients", "item_price": 0.01, @@ -459,6 +491,8 @@ { "item_code": "Flour", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Flour", "item_group": "Ingredients", "item_price": 0.66, @@ -496,6 +530,8 @@ { "item_code": "Salt", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Salt", "item_group": "Ingredients", "item_price": 0.36, @@ -505,6 +541,8 @@ { "item_code": "Sugar", "uom": "Pound", + "weight_uom": "Pound", + "weight_per_unit": 1, "description": "Sugar", "item_group": "Ingredients", "item_price": 0.60, @@ -514,6 +552,8 @@ { "item_code": "Water", "uom": "Cup", + "weight_uom": "Pound", + "weight_per_unit": 0.52, "item_price": 0.05, "description": "Water", "item_group": "Ingredients", @@ -1010,3 +1050,131 @@ "TransAmerica Bank Cafeteria", "Whole Harvest Grocery Group", ] + +specifications = [ + { + "dt": "Item", + "name": "Items", + "apply_on": "", + "enabled": 1, + "attributes": [ + { + "attribute_name": "Color", + "applied_on": "Item", + "component": "FacetedSearchColorPicker", + }, + { + "attribute_name": "Weight", + "applied_on": "Item", + "field": "weight_per_unit", + "numeric_values": 1, + "component": "FacetedSearchNumericRange", + }, + { + "attribute_name": "Brand", + "applied_on": "Item", + "field": "brand", + "component": "AttributeFilter", + }, + ], + }, + { + "dt": "Item Group", + "name": "Baked Goods", + "apply_on": "Baked Goods", + "enabled": 1, + "attributes": [ + { + "attribute_name": "Price", + "applied_on": "Item", + "numeric_values": 1, + "component": "FacetedSearchNumericRange", + }, + {"attribute_name": "Fruits", "applied_on": "Item", "component": "AttributeFilter"}, + { + "attribute_name": "Shelf Life", + "applied_on": "Item", + "field": "shelf_life_in_days", + "numeric_values": 1, + "component": "FacetedSearchNumericRange", + }, + { + "attribute_name": "EOL", + "applied_on": "Item", + "field": "end_of_life", + "date_values": 1, + "component": "FacetedSearchDateRange", + }, + ], + }, +] + + +attributes = { + "Ambrosia Pie": { + "Fruits": ["Hairless Rambutan", "Cloudberry", "Tayberry"], + "Price": 11.00, + "Color": ["Blue", "Red"], + "Brand": "Chelsea Fruit Co", + }, + "Double Plum Pie": { + "Fruits": ["Cocoplum", "Damson Plum"], + "Price": 10.50, + "Color": ["Purple"], + "Brand": "Chelsea Fruit Co", + }, + "Gooseberry Pie": { + "Fruits": "Gooseberry", + "Price": 12.00, + "Color": ["Yellow"], + "Brand": "Chelsea Fruit Co", + }, + "Kaduka Key Lime Pie": { + "Fruits": ["Kaduka Lime", "Limequat"], + "Price": 11.50, + "Color": ["Green", "Yellow"], + "Brand": "Chelsea Fruit Co", + }, + "Tayberry": { + "Color": ["Red"], + }, + "Limequat": { + "Color": ["Yellow", "Green"], + }, + "Kaduka Lime": { + "Color": ["Green"], + }, + "Hairless Rambutan": { + "Color": ["Red"], + }, + "Gooseberry": { + "Color": ["Yellow"], + }, + "Damson Plum": { + "Color": ["Purple"], + }, + "Cocoplum": { + "Color": ["Purple", "Black"], + }, + "Bayberry": { + "Color": ["Red", "Green", "Blue"], + }, + "Sugar": { + "Color": ["White"], + }, + "Salt": { + "Color": ["White"], + }, + "Flour": { + "Color": ["White"], + }, + "Cornstarch": { + "Color": ["White"], + }, + "Butter": { + "Color": ["Yellow"], + }, + "Cloudberry": { + "Color": ["Yellow", "Red"], + }, +} diff --git a/inventory_tools/tests/setup.py b/inventory_tools/tests/setup.py index 4642bb8..6059abe 100644 --- a/inventory_tools/tests/setup.py +++ b/inventory_tools/tests/setup.py @@ -2,25 +2,22 @@ # For license information, please see license.txt import datetime -import types -from itertools import groupby import frappe -from erpnext.accounts.doctype.account.account import update_account_number from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, ) -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 erpnext.setup.utils import set_defaults_for_tests from frappe.desk.page.setup_wizard.setup_wizard import setup_complete -from frappe.utils import add_months, nowdate -from frappe.utils.data import flt, getdate +from frappe.utils.data import add_months, flt, getdate, nowdate, get_datetime +from webshop.webshop.doctype.website_item.website_item import make_website_item from inventory_tools.tests.fixtures import ( boms, customers, items, operations, + specifications, suppliers, workstations, ) @@ -108,6 +105,7 @@ def create_test_data(): create_production_plan(settings, prod_plan_from_doc) create_fruit_material_request(settings) create_quotations(settings) + create_specifications(settings) def create_suppliers(settings): @@ -193,9 +191,16 @@ def setup_manufacturing_settings(settings): "Inventory Tools Settings", settings.company, "enable_work_order_subcontracting", 1 ) frappe.set_value("Inventory Tools Settings", settings.company, "create_purchase_orders", 0) + frappe.set_value("Inventory Tools Settings", settings.company, "enforce_uoms", 1) + frappe.set_value( + "Inventory Tools Settings", settings.company, "allow_alternative_workstations", 1 + ) + frappe.set_value("Inventory Tools Settings", settings.company, "create_purchase_orders", 0) frappe.set_value( "Inventory Tools Settings", settings.company, "overproduction_percentage_for_work_order", 50 ) + frappe.set_value("Inventory Tools Settings", settings.company, "show_on_website", 1) + frappe.set_value("Inventory Tools Settings", settings.company, "show_in_listview", 1) def create_workstations(): @@ -243,6 +248,11 @@ def create_item_groups(settings): ig.parent_item_group = "All Item Groups" ig.save() + if not frappe.db.exists("Brand", "Ambrosia Pie Co"): + brand = frappe.new_doc("Brand") + brand.brand = "Ambrosia Pie Co" + brand.save() + def create_price_lists(settings): if not frappe.db.exists("Price List", "Bakery Buying"): @@ -290,6 +300,8 @@ def create_items(settings): i.valuation_rate = item.get("valuation_rate") or 0 i.is_sub_contracted_item = item.get("is_sub_contracted_item") or 0 i.default_warehouse = settings.get("warehouse") + i.weight_uom = item.get("weight_uom") if i.is_stock_item else None + i.weight_per_unit = item.get("weight_per_unit") i.default_material_request_type = ( "Purchase" if item.get("item_group") in ("Bakery Supplies", "Ingredients") @@ -307,6 +319,9 @@ def create_items(settings): else 0 ) i.is_sales_item = 1 if item.get("item_group") == "Baked Goods" else 0 + i.sales_uom = "Nos" if i.is_sales_item else None + i.shelf_life_in_days = 7 if i.is_sales_item else None + i.brand = "Ambrosia Pie Co" if i.is_sales_item else None i.append( "item_defaults", { @@ -351,6 +366,11 @@ def create_items(settings): ) se.save() se.submit() + if i.is_sales_item: + website_item = make_website_item(i, True) + website_item = frappe.get_doc("Website Item", website_item[0]) + website_item.route = f"products/{frappe.scrub(i.name)}" + website_item.save() def create_warehouses(settings): @@ -621,11 +641,22 @@ def create_production_plan(settings, prod_plan_from_doc): wo.save() wo.submit() job_cards = frappe.get_all("Job Card", {"work_order": wo.name}) + start_time = get_datetime() for job_card in job_cards: job_card = frappe.get_doc("Job Card", job_card) - job_card.append("time_logs", {"completed_qty": wo.qty}) - job_card.save() - job_card.submit() + 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, + "from_time": start_time, + "to_time": start_time + datetime.timedelta(minutes=time_in_mins), + "time_in_mins": time_in_mins, + }, + ) def create_fruit_material_request(settings): @@ -757,3 +788,51 @@ def create_quotations(settings): quotation.update(values) quotation.save() quotation.submit() + + +def create_specifications(settings=None): + for c in ( + ("Red", "#E24C4C"), + ("Blue", "#2490EF"), + ("Purple", "#8684FF"), + ("Green", "#8CCF54"), + ("Yellow", "#FFFF00"), + ("White", "#EEEEEE"), + ("Black", "#111111"), + ): + if not frappe.db.exists("Color", c[0]): + color = frappe.new_doc("Color") + color.name = c[0] + color.color = c[1] + color.save() + + for spec in specifications: + if frappe.db.exists("Specification", spec.get("name")): + s = frappe.get_doc("Specification", spec.get("name")) + else: + s = frappe.new_doc("Specification") + s.name = spec.get("name") + s.dt = spec.get("dt") + s.apply_on = spec.get("apply_on") + s.enabled = spec.get("enabled") + for at in spec.get("attributes"): + s.append("attributes", at) + s.save() + + +def create_demo_specification_values(): + """ + run this if you need to manually create data for demoing faceted search + bench execute 'inventory_tools.tests.setup.create_demo_specification_values' + """ + from inventory_tools.tests.test_faceted_search import ( + test_values_updated_on_item_save, + test_generate_values, + test_generate_values_on_overlapping_items, + test_manual_attribute_addition, + ) + + test_values_updated_on_item_save() + test_generate_values() + test_generate_values_on_overlapping_items() + test_manual_attribute_addition() diff --git a/inventory_tools/tests/test_alternative_workstation.py b/inventory_tools/tests/test_alternative_workstation.py index 2b43bf9..8e5d75f 100644 --- a/inventory_tools/tests/test_alternative_workstation.py +++ b/inventory_tools/tests/test_alternative_workstation.py @@ -1,37 +1,39 @@ -import frappe -import pytest - - -@pytest.mark.order(45) -def test_alternative_workstation_query(): - # test default settings - frappe.call( - "frappe.desk.search.search_link", - **{ - "doctype": "Workstation", - "txt": "", - "reference_doctype": "Job Card", - }, - ) - assert len(frappe.response.results) == 16 # all workstations - - # test with inventory tools settings - inventory_tools_settings = frappe.get_doc( - "Inventory Tools Settings", frappe.defaults.get_defaults().get("company") - ) - inventory_tools_settings.allow_alternative_workstations = True - inventory_tools_settings.save() - frappe.call( - "frappe.desk.search.search_link", - **{ - "doctype": "Workstation", - "txt": "", - "query": "inventory_tools.inventory_tools.overrides.workstation.get_alternative_workstations", - "filters": {"operation": "Gather Pie Filling Ingredients"}, - "reference_doctype": "Job Card", - }, - ) - assert len(frappe.response.results) == 2 - assert frappe.response.results[0].get("value") == "Food Prep Table 1" # default returns first - assert "Default" in frappe.response.results[0].get("description") - assert frappe.response.results[1].get("value") == "Food Prep Table 2" +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +import pytest + + +@pytest.mark.order(45) +def test_alternative_workstation_query(): + # test default settings + response = frappe.call( + "frappe.desk.search.search_link", + **{ + "doctype": "Workstation", + "txt": "", + "reference_doctype": "Job Card", + }, + ) + assert len(response) == 10 + # test with inventory tools settings + inventory_tools_settings = frappe.get_doc( + "Inventory Tools Settings", frappe.defaults.get_defaults().get("company") + ) + inventory_tools_settings.allow_alternative_workstations = True + inventory_tools_settings.save() + response = frappe.call( + "frappe.desk.search.search_link", + **{ + "doctype": "Workstation", + "txt": "", + "query": "inventory_tools.inventory_tools.overrides.workstation.get_alternative_workstations", + "filters": {"operation": "Gather Pie Filling Ingredients"}, + "reference_doctype": "Job Card", + }, + ) + assert len(response) == 2 + assert response[0].get("value") == "Food Prep Table 1" # default returns first + assert "Default" in response[0].get("description") + assert response[1].get("value") == "Food Prep Table 2" diff --git a/inventory_tools/tests/test_faceted_search.py b/inventory_tools/tests/test_faceted_search.py new file mode 100644 index 0000000..9ca23b3 --- /dev/null +++ b/inventory_tools/tests/test_faceted_search.py @@ -0,0 +1,199 @@ +# Copyright (c) 2024, AgriTheory and contributors +# For license information, please see license.txt + +import frappe +import pytest + +from inventory_tools.tests.fixtures import attributes + +# don't generate spec values in setup +# create overlapping spec (Item) + +# test generation of values for + +# spec_items = frappe.get_all("Item", {"item_group": "Baked Goods"}) +# for spec_item in spec_items: +# if spec_item.name not in attributes: +# continue +# spec_item = frappe.get_doc("Item", spec_item) +# s.create_linked_values(spec_item, attributes[spec_item.name]) + + +@pytest.mark.order(70) +def test_values_updated_on_item_save(): + # assert spec value doesn't exist + frappe.flags.in_test = True + values = frappe.get_all( + "Specification Value", {"reference_doctype": "Item", "reference_name": "Double Plum Pie"} + ) + assert values == [] + doc = frappe.get_doc("Item", "Double Plum Pie") + doc.save() + values = frappe.get_all( + "Specification Value", {"reference_doctype": "Item", "reference_name": "Double Plum Pie"} + ) + assert len(values) == 4 + doc.weight_per_unit = 12 + doc.save() + values = frappe.get_all( + "Specification Value", + {"reference_doctype": "Item", "reference_name": "Double Plum Pie"}, + ) + assert len(values) == 4 + new_weight = frappe.get_all( + "Specification Value", + {"reference_doctype": "Item", "reference_name": "Double Plum Pie", "attribute": "Weight"}, + "value", + ) + assert len(new_weight) == 1 + assert int(new_weight[0].value) == 12 + doc.weight_per_unit = 8 + doc.save() + new_weight = frappe.get_all( + "Specification Value", + {"reference_doctype": "Item", "reference_name": "Double Plum Pie", "attribute": "Weight"}, + "value", + ) + assert len(new_weight) == 1 + assert int(new_weight[0].value) == 8 + values = frappe.get_all( + "Specification Value", {"reference_doctype": "Item", "reference_name": "Double Plum Pie"} + ) + assert len(values) == 4 + # cleanup + for value in values: + frappe.delete_doc("Specification Value", value.name, force=True) + + +@pytest.mark.order(71) +def test_generate_values(): + frappe.flags.in_test = True + doc = frappe.get_doc("Specification", "Items") + assert len(doc.attributes) == 3 + frappe.call( + "inventory_tools.inventory_tools.doctype.specification.specification.create_specification_values", + **{ + "spec": doc.name, + "specifications": [ + { + "attribute": a.attribute_name, + "field": a.field, + } + for a in doc.attributes + ], + } + ) + assert ( + len(frappe.get_all("Specification Value", {"specification": doc.name})) == 36 * 2 + ) # total items x computed attributes + + +@pytest.mark.order(72) +def test_generate_values_on_overlapping_items(): + frappe.flags.in_test = True + doc = frappe.get_doc("Specification", "Baked Goods") + assert len(doc.attributes) == 4 + assert len(frappe.get_all("Specification Value", {"specification": doc.name})) == 0 + frappe.call( + "inventory_tools.inventory_tools.doctype.specification.specification.create_specification_values", + **{ + "spec": doc.name, + "specifications": [ + { + "attribute": a.attribute_name, + "field": a.field, + } + for a in doc.attributes + ], + } + ) + assert ( + len(frappe.get_all("Specification Value", {"specification": doc.name})) == 6 * 2 + ) # total items x computed attributes + + +@pytest.mark.order(73) +def test_manual_attribute_addition(): + for item, fixtures_values in attributes.items(): + _args = { + "reference_doctype": "Item", + "reference_name": item, + "specification": "Items", + } + values = frappe.call( + "inventory_tools.inventory_tools.doctype.specification.specification.get_specification_values", + **_args + ) + for attribute, manual_values in fixtures_values.items(): + if isinstance(manual_values, list): + for v in manual_values: + values.append({"row_name": "", "attribute": attribute, "value": v, "specification": "Items"}) + else: + values.append( + { + "row_name": "", + "attribute": attribute, + "value": manual_values, + "specification": "Items", + } + ) + # print(existing_values) + args = { + "spec": "Items", + "specifications": frappe.as_json(values), + "reference_doctype": "Item", + "reference_name": item, + } + frappe.call( + "inventory_tools.inventory_tools.doctype.specification.specification.update_specification_values", + **args + ) + assert ( + len(frappe.get_all("Specification Value", {"specification": "Items", "attribute": "Color"})) + == 25 + ) # all colors in baked goods items from fixtures + + +@pytest.mark.order(74) +def test_delete_of_specification_value(): + frappe.flags.in_test = True + _args = { + "reference_doctype": "Item", + "reference_name": "Ambrosia Pie", + "specification": "Items", + } + values = frappe.call( + "inventory_tools.inventory_tools.doctype.specification.specification.get_specification_values", + **_args + ) + colors = [v.value for v in values if v.attribute == "Color"] + assert "Blue" in colors + assert len(colors) == 2 + + for v in values: + if v.attribute == "Color" and v.value == "Blue": + v.value = None + + args = { + "spec": _args.get("specification"), + "specifications": frappe.as_json(values), + "reference_doctype": _args.get("reference_doctype"), + "reference_name": _args.get("reference_name"), + } + frappe.call( + "inventory_tools.inventory_tools.doctype.specification.specification.update_specification_values", + **args + ) + assert ( + len( + frappe.get_all( + "Specification Value", + { + "specification": "Items", + "attribute": "Color", + "reference_name": _args.get("reference_name"), + }, + ) + ) + == 1 + ) # all colors in baked goods items from fixtures diff --git a/inventory_tools/tests/test_overproduction.py b/inventory_tools/tests/test_overproduction.py index 284705f..6a15763 100644 --- a/inventory_tools/tests/test_overproduction.py +++ b/inventory_tools/tests/test_overproduction.py @@ -1,11 +1,13 @@ # Copyright (c) 2024, AgriTheory and contributors # For license information, please see license.txt +import datetime + import frappe import pytest from erpnext.manufacturing.doctype.work_order.work_order import create_job_card, make_stock_entry from frappe.exceptions import ValidationError -from frappe.utils import now, strip_html +from frappe.utils import now, strip_html, get_datetime from inventory_tools.inventory_tools.overrides.work_order import get_allowance_percentage @@ -137,14 +139,23 @@ def test_validate_job_card(): jc = frappe.get_doc( "Job Card", {"work_order": work_order.name, "operation": work_order.operations[0].operation} ) - jc.cancel() + jc.delete() + start_time = ( + frappe.get_value("Job Card Time Log", {"docstatus": 1}, "MAX(to_time) AS to_time") + or get_datetime() + ) job_card = create_job_card(work_order, work_order.operations[0].as_dict(), auto_create=True) + 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) * work_order.qty job_card.append( "time_logs", { - "from_time": now(), - "to_time": now(), "completed_qty": work_order.qty, + "from_time": start_time + datetime.timedelta(minutes=1), + "time_in_mins": time_in_mins, + "to_time": start_time + datetime.timedelta(minutes=time_in_mins + 1), }, ) job_card.save() diff --git a/inventory_tools/tests/test_uom.py b/inventory_tools/tests/test_uom.py index 36fb36c..a400dce 100644 --- a/inventory_tools/tests/test_uom.py +++ b/inventory_tools/tests/test_uom.py @@ -24,12 +24,15 @@ def test_uom_enforcement_validation(): @pytest.mark.order(41) def test_uom_enforcement_query(): - inventory_tools_settings = frappe.get_doc( - "Inventory Tools Settings", frappe.defaults.get_defaults().get("company") + inventory_tools_settings = frappe.get_cached_doc( + "Inventory Tools Settings", "Ambrosia Pie Company" ) inventory_tools_settings.enforce_uoms = True inventory_tools_settings.save() - frappe.call( + inventory_tools_settings = frappe.get_cached_doc("Inventory Tools Settings", "Chelsea Fruit Co") + inventory_tools_settings.enforce_uoms = True + inventory_tools_settings.save() + response = frappe.call( "frappe.desk.search.search_link", **{ "doctype": "UOM", @@ -39,8 +42,8 @@ def test_uom_enforcement_query(): "reference_doctype": "Purchase Order Item", }, ) - assert len(frappe.response.results) == 2 - assert frappe.response.results[0].get("value") == "Nos" - assert frappe.response.results[0].get("description") == "1.0" - assert frappe.response.results[1].get("value") == "Box" - assert frappe.response.results[1].get("description") == "100.0" + assert len(response) == 2 + assert response[0].get("value") == "Nos" + assert response[0].get("description") == "1.0" + assert response[1].get("value") == "Box" + assert response[1].get("description") == "100.0" diff --git a/package.json b/package.json index d8e5a75..b0370d7 100644 --- a/package.json +++ b/package.json @@ -1,16 +1,24 @@ { "name": "inventory_tools", - "scripts": {}, - "dependencies": { - "onscan.js": "^1.5.2" - }, - "devDependencies": {}, + "private": true, + "license": "MIT", "repository": { "type": "git", - "url": "https://github.com/agritheory/invnetory_tools.git" + "url": "https://github.com/agritheory/inventory_tools.git" + }, + "scripts": { + "build": "vite build --config=./inventory_tools/public/js/vite.config.js", + "dev": "vite build --watch --config=./inventory_tools/public/js/vite.config.js" + }, + "dependencies": { + "@vueuse/core": "^10.11.0", + "vue": "^3.4.35" + }, + "devDependencies": { + "@vitejs/plugin-vue": "^5.1.2", + "vite": "^5.3.5" }, "publishConfig": { "access": "restricted" - }, - "private": true + } } diff --git a/pyproject.toml b/pyproject.toml index c22a854..4ece7ec 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -19,7 +19,7 @@ requires = ["poetry-core"] build-backend = "poetry.core.masonry.api" [tool.pytest.ini_options] -addopts = "--cov=inventory_tools --cov-report term-missing" +addopts = "-s --disable-warnings" #--cov=inventory_tools --cov-report term-missing" [tool.black] line-length = 99 diff --git a/yarn.lock b/yarn.lock index fa1b1d6..151c9af 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,7 +2,456 @@ # yarn lockfile v1 -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== +"@babel/parser@^7.24.7": + version "7.24.8" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.24.8.tgz#58a4dbbcad7eb1d48930524a3fd93d93e9084c6f" + integrity sha512-WzfbgXOkGzZiXXCqk43kKwZjzwx4oulxZi3nq2TYL9mOjQv6kYwul9mz6ID36njuL7Xkp6nJEfok848Zj10j/w== + +"@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== + +"@jridgewell/sourcemap-codec@^1.4.15": + version "1.5.0" + resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.0.tgz#3188bcb273a414b0d215fd22a58540b989b9409a" + integrity sha512-gv3ZRaISU3fjPAgNsriBRqGWQL6quFx04YMPW/zD8XMLsU32mhCCbfbO6KZFLjvYpCZ8zyDEgqsgf+PwPaM7GQ== + +"@rollup/rollup-android-arm-eabi@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.20.0.tgz#c3f5660f67030c493a981ac1d34ee9dfe1d8ec0f" + integrity sha512-TSpWzflCc4VGAUJZlPpgAJE1+V60MePDQnBd7PPkpuEmOy8i87aL6tinFGKBFKuEDikYpig72QzdT3QPYIi+oA== + +"@rollup/rollup-android-arm64@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.20.0.tgz#64161f0b67050023a3859e723570af54a82cff5c" + integrity sha512-u00Ro/nok7oGzVuh/FMYfNoGqxU5CPWz1mxV85S2w9LxHR8OoMQBuSk+3BKVIDYgkpeOET5yXkx90OYFc+ytpQ== + +"@rollup/rollup-darwin-arm64@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.20.0.tgz#25f3d57b1da433097cfebc89341b355901615763" + integrity sha512-uFVfvzvsdGtlSLuL0ZlvPJvl6ZmrH4CBwLGEFPe7hUmf7htGAN+aXo43R/V6LATyxlKVC/m6UsLb7jbG+LG39Q== + +"@rollup/rollup-darwin-x64@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.20.0.tgz#d8ddaffb636cc2f59222c50316e27771e48966df" + integrity sha512-xbrMDdlev53vNXexEa6l0LffojxhqDTBeL+VUxuuIXys4x6xyvbKq5XqTXBCEUA8ty8iEJblHvFaWRJTk/icAQ== + +"@rollup/rollup-linux-arm-gnueabihf@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.20.0.tgz#41bd4fcffa20fb84f3dbac6c5071638f46151885" + integrity sha512-jMYvxZwGmoHFBTbr12Xc6wOdc2xA5tF5F2q6t7Rcfab68TT0n+r7dgawD4qhPEvasDsVpQi+MgDzj2faOLsZjA== + +"@rollup/rollup-linux-arm-musleabihf@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.20.0.tgz#842077c5113a747eb5686f19f2f18c33ecc0acc8" + integrity sha512-1asSTl4HKuIHIB1GcdFHNNZhxAYEdqML/MW4QmPS4G0ivbEcBr1JKlFLKsIRqjSwOBkdItn3/ZDlyvZ/N6KPlw== + +"@rollup/rollup-linux-arm64-gnu@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.20.0.tgz#65d1d5b6778848f55b7823958044bf3e8737e5b7" + integrity sha512-COBb8Bkx56KldOYJfMf6wKeYJrtJ9vEgBRAOkfw6Ens0tnmzPqvlpjZiLgkhg6cA3DGzCmLmmd319pmHvKWWlQ== + +"@rollup/rollup-linux-arm64-musl@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.20.0.tgz#50eef7d6e24d0fe3332200bb666cad2be8afcf86" + integrity sha512-+it+mBSyMslVQa8wSPvBx53fYuZK/oLTu5RJoXogjk6x7Q7sz1GNRsXWjn6SwyJm8E/oMjNVwPhmNdIjwP135Q== + +"@rollup/rollup-linux-powerpc64le-gnu@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.20.0.tgz#8837e858f53c84607f05ad0602943e96d104c6b4" + integrity sha512-yAMvqhPfGKsAxHN8I4+jE0CpLWD8cv4z7CK7BMmhjDuz606Q2tFKkWRY8bHR9JQXYcoLfopo5TTqzxgPUjUMfw== + +"@rollup/rollup-linux-riscv64-gnu@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.20.0.tgz#c894ade2300caa447757ddf45787cca246e816a4" + integrity sha512-qmuxFpfmi/2SUkAw95TtNq/w/I7Gpjurx609OOOV7U4vhvUhBcftcmXwl3rqAek+ADBwSjIC4IVNLiszoj3dPA== + +"@rollup/rollup-linux-s390x-gnu@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.20.0.tgz#5841e5390d4c82dd5cdf7b2c95a830e3c2f47dd3" + integrity sha512-I0BtGXddHSHjV1mqTNkgUZLnS3WtsqebAXv11D5BZE/gfw5KoyXSAXVqyJximQXNvNzUo4GKlCK/dIwXlz+jlg== + +"@rollup/rollup-linux-x64-gnu@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.20.0.tgz#cc1f26398bf777807a99226dc13f47eb0f6c720d" + integrity sha512-y+eoL2I3iphUg9tN9GB6ku1FA8kOfmF4oUEWhztDJ4KXJy1agk/9+pejOuZkNFhRwHAOxMsBPLbXPd6mJiCwew== + +"@rollup/rollup-linux-x64-musl@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.20.0.tgz#1507465d9056e0502a590d4c1a00b4d7b1fda370" + integrity sha512-hM3nhW40kBNYUkZb/r9k2FKK+/MnKglX7UYd4ZUy5DJs8/sMsIbqWK2piZtVGE3kcXVNj3B2IrUYROJMMCikNg== + +"@rollup/rollup-win32-arm64-msvc@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.20.0.tgz#86a221f01a2c248104dd0defb4da119f2a73642e" + integrity sha512-psegMvP+Ik/Bg7QRJbv8w8PAytPA7Uo8fpFjXyCRHWm6Nt42L+JtoqH8eDQ5hRP7/XW2UiIriy1Z46jf0Oa1kA== + +"@rollup/rollup-win32-ia32-msvc@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.20.0.tgz#8bc8f77e02760aa664694b4286d6fbea7f1331c5" + integrity sha512-GabekH3w4lgAJpVxkk7hUzUf2hICSQO0a/BLFA11/RMxQT92MabKAqyubzDZmMOC/hcJNlc+rrypzNzYl4Dx7A== + +"@rollup/rollup-win32-x64-msvc@4.20.0": + version "4.20.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.20.0.tgz#601fffee719a1e8447f908aca97864eec23b2784" + integrity sha512-aJ1EJSuTdGnM6qbVC4B5DSmozPTqIag9fSzXRNNo+humQLG89XpPgdt16Ia56ORD7s+H8Pmyx44uczDQ0yDzpg== + +"@types/estree@1.0.5": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/estree/-/estree-1.0.5.tgz#a6ce3e556e00fd9895dd872dd172ad0d4bd687f4" + integrity sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw== + +"@types/web-bluetooth@^0.0.20": + version "0.0.20" + resolved "https://registry.yarnpkg.com/@types/web-bluetooth/-/web-bluetooth-0.0.20.tgz#f066abfcd1cbe66267cdbbf0de010d8a41b41597" + integrity sha512-g9gZnnXVq7gM7v3tJCWV/qw7w+KeOlSHAhgF9RytFyifW6AF61hdT2ucrYhPq9hLs5JIryeupHV3qGk95dH9ow== + +"@vitejs/plugin-vue@^5.1.2": + version "5.1.2" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-5.1.2.tgz#f11091e0130eca6c1ca8cfb85ee71ea53b255d31" + integrity sha512-nY9IwH12qeiJqumTCLJLE7IiNx7HZ39cbHaysEUd+Myvbz9KAqd2yq+U01Kab1R/H1BmiyM2ShTYlNH32Fzo3A== + +"@vue/compiler-core@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.35.tgz#421922a75ecabf1aabc6b7a2ce98b5acb2fc2d65" + integrity sha512-gKp0zGoLnMYtw4uS/SJRRO7rsVggLjvot3mcctlMXunYNsX+aRJDqqw/lV5/gHK91nvaAAlWFgdVl020AW1Prg== + dependencies: + "@babel/parser" "^7.24.7" + "@vue/shared" "3.4.35" + entities "^4.5.0" + estree-walker "^2.0.2" + source-map-js "^1.2.0" + +"@vue/compiler-dom@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.35.tgz#cd0881f1b4ed655cd96367bce4845f87023a5a2d" + integrity sha512-pWIZRL76/oE/VMhdv/ovZfmuooEni6JPG1BFe7oLk5DZRo/ImydXijoZl/4kh2406boRQ7lxTYzbZEEXEhj9NQ== + dependencies: + "@vue/compiler-core" "3.4.35" + "@vue/shared" "3.4.35" + +"@vue/compiler-sfc@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.35.tgz#16f87dd3bdab64cef14d3a6fcf53f8673e404071" + integrity sha512-xacnRS/h/FCsjsMfxBkzjoNxyxEyKyZfBch/P4vkLRvYJwe5ChXmZZrj8Dsed/752H2Q3JE8kYu9Uyha9J6PgA== + dependencies: + "@babel/parser" "^7.24.7" + "@vue/compiler-core" "3.4.35" + "@vue/compiler-dom" "3.4.35" + "@vue/compiler-ssr" "3.4.35" + "@vue/shared" "3.4.35" + estree-walker "^2.0.2" + magic-string "^0.30.10" + postcss "^8.4.40" + source-map-js "^1.2.0" + +"@vue/compiler-ssr@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.35.tgz#0774c9a0afed74d71615209904b38f3fa9711adb" + integrity sha512-7iynB+0KB1AAJKk/biENTV5cRGHRdbdaD7Mx3nWcm1W8bVD6QmnH3B4AHhQQ1qZHhqFwzEzMwiytXm3PX1e60A== + dependencies: + "@vue/compiler-dom" "3.4.35" + "@vue/shared" "3.4.35" + +"@vue/reactivity@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.4.35.tgz#dfbb4f5371da1290ac86e3313d0e9a68bb0ab38d" + integrity sha512-Ggtz7ZZHakriKioveJtPlStYardwQH6VCs9V13/4qjHSQb/teE30LVJNrbBVs4+aoYGtTQKJbTe4CWGxVZrvEw== + dependencies: + "@vue/shared" "3.4.35" + +"@vue/runtime-core@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.4.35.tgz#c036013a7b1bbe0d14a6b76eb4355dae6690d2e6" + integrity sha512-D+BAjFoWwT5wtITpSxwqfWZiBClhBbR+bm0VQlWYFOadUUXFo+5wbe9ErXhLvwguPiLZdEF13QAWi2vP3ZD5tA== + dependencies: + "@vue/reactivity" "3.4.35" + "@vue/shared" "3.4.35" + +"@vue/runtime-dom@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.4.35.tgz#74254c7c327163d692e1d7d2b6d9e92463744e90" + integrity sha512-yGOlbos+MVhlS5NWBF2HDNgblG8e2MY3+GigHEyR/dREAluvI5tuUUgie3/9XeqhPE4LF0i2wjlduh5thnfOqw== + dependencies: + "@vue/reactivity" "3.4.35" + "@vue/runtime-core" "3.4.35" + "@vue/shared" "3.4.35" + csstype "^3.1.3" + +"@vue/server-renderer@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.4.35.tgz#188e94e82d8e729ba7b40dd91d10678b85f77c6b" + integrity sha512-iZ0e/u9mRE4T8tNhlo0tbA+gzVkgv8r5BX6s1kRbOZqfpq14qoIvCZ5gIgraOmYkMYrSEZgkkojFPr+Nyq/Mnw== + dependencies: + "@vue/compiler-ssr" "3.4.35" + "@vue/shared" "3.4.35" + +"@vue/shared@3.4.35": + version "3.4.35" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.35.tgz#5432f4b1c79e763fcf78cc830faf59ff01248968" + integrity sha512-hvuhBYYDe+b1G8KHxsQ0diDqDMA8D9laxWZhNAjE83VZb5UDaXl9Xnz7cGdDSyiHM90qqI/CyGMcpBpiDy6VVQ== + +"@vueuse/core@^10.11.0": + version "10.11.0" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-10.11.0.tgz#b042585a8bf98bb29c177b33999bd0e3fcd9e65d" + integrity sha512-x3sD4Mkm7PJ+pcq3HX8PLPBadXCAlSDR/waK87dz0gQE+qJnaaFhc/dZVfJz+IUYzTMVGum2QlR7ImiJQN4s6g== + dependencies: + "@types/web-bluetooth" "^0.0.20" + "@vueuse/metadata" "10.11.0" + "@vueuse/shared" "10.11.0" + vue-demi ">=0.14.8" + +"@vueuse/metadata@10.11.0": + version "10.11.0" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-10.11.0.tgz#27be47cf115ee98e947a1bfcd0b1b5b35d785fb6" + integrity sha512-kQX7l6l8dVWNqlqyN3ePW3KmjCQO3ZMgXuBMddIu83CmucrsBfXlH+JoviYyRBws/yLTQO8g3Pbw+bdIoVm4oQ== + +"@vueuse/shared@10.11.0": + version "10.11.0" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-10.11.0.tgz#be09262b2c5857069ed3dadd1680f22c4cb6f984" + integrity sha512-fyNoIXEq3PfX1L3NkNhtVQUSRtqYwJtJg+Bp9rIzculIZWHTkKSysujrOk2J+NrRulLTQH9+3gGSfYLWSEWU1A== + dependencies: + vue-demi ">=0.14.8" + +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== + +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== + +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" + +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== + +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== + +magic-string@^0.30.10: + version "0.30.10" + resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e" + integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ== + dependencies: + "@jridgewell/sourcemap-codec" "^1.4.15" + +nanoid@^3.3.7: + version "3.3.7" + resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.7.tgz#d0c301a691bc8d54efa0a2226ccf3fe2fd656bd8" + integrity sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g== + +picocolors@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/picocolors/-/picocolors-1.0.1.tgz#a8ad579b571952f0e5d25892de5445bcfe25aaa1" + integrity sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew== + +postcss@^8.4.39, postcss@^8.4.40: + version "8.4.40" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.40.tgz#eb81f2a4dd7668ed869a6db25999e02e9ad909d8" + integrity sha512-YF2kKIUzAofPMpfH6hOi2cGnv/HrUlfucspc7pDyvv7kGdqXrfj8SCl/t8owkEgKEuu8ZcRjSOxFxVLqwChZ2Q== + dependencies: + nanoid "^3.3.7" + picocolors "^1.0.1" + source-map-js "^1.2.0" + +rollup@^4.13.0: + version "4.20.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.20.0.tgz#f9d602161d29e178f0bf1d9f35f0a26f83939492" + integrity sha512-6rbWBChcnSGzIlXeIdNIZTopKYad8ZG8ajhl78lGRLsI2rX8IkaotQhVas2Ma+GPxJav19wrSzvRvuiv0YKzWw== + dependencies: + "@types/estree" "1.0.5" + optionalDependencies: + "@rollup/rollup-android-arm-eabi" "4.20.0" + "@rollup/rollup-android-arm64" "4.20.0" + "@rollup/rollup-darwin-arm64" "4.20.0" + "@rollup/rollup-darwin-x64" "4.20.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.20.0" + "@rollup/rollup-linux-arm-musleabihf" "4.20.0" + "@rollup/rollup-linux-arm64-gnu" "4.20.0" + "@rollup/rollup-linux-arm64-musl" "4.20.0" + "@rollup/rollup-linux-powerpc64le-gnu" "4.20.0" + "@rollup/rollup-linux-riscv64-gnu" "4.20.0" + "@rollup/rollup-linux-s390x-gnu" "4.20.0" + "@rollup/rollup-linux-x64-gnu" "4.20.0" + "@rollup/rollup-linux-x64-musl" "4.20.0" + "@rollup/rollup-win32-arm64-msvc" "4.20.0" + "@rollup/rollup-win32-ia32-msvc" "4.20.0" + "@rollup/rollup-win32-x64-msvc" "4.20.0" + fsevents "~2.3.2" + +source-map-js@^1.2.0: + version "1.2.0" + resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af" + integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg== + +vite@^5.3.5: + version "5.3.5" + resolved "https://registry.yarnpkg.com/vite/-/vite-5.3.5.tgz#b847f846fb2b6cb6f6f4ed50a830186138cb83d8" + integrity sha512-MdjglKR6AQXQb9JGiS7Rc2wC6uMjcm7Go/NHNO63EwiJXfuk9PgqiP/n5IDJCziMkfw9n4Ubp7lttNwz+8ZVKA== + dependencies: + esbuild "^0.21.3" + postcss "^8.4.39" + rollup "^4.13.0" + optionalDependencies: + fsevents "~2.3.3" + +vue-demi@>=0.14.8: + version "0.14.10" + resolved "https://registry.yarnpkg.com/vue-demi/-/vue-demi-0.14.10.tgz#afc78de3d6f9e11bf78c55e8510ee12814522f04" + integrity sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg== + +vue@^3.4.35: + version "3.4.35" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.4.35.tgz#9ad23525919eece40153fdf8675d07ddd879eb33" + integrity sha512-+fl/GLmI4GPileHftVlCdB7fUL4aziPcqTudpTGXCT8s+iZWuOCeNEB5haX6Uz2IpRrbEXOgIFbe+XciCuGbNQ== + dependencies: + "@vue/compiler-dom" "3.4.35" + "@vue/compiler-sfc" "3.4.35" + "@vue/runtime-dom" "3.4.35" + "@vue/server-renderer" "3.4.35" + "@vue/shared" "3.4.35"