├── .editorconfig ├── .flake8 ├── .github ├── helper │ ├── install.sh │ ├── install_dependencies.sh │ └── site_config.json ├── validate_customizations.py └── workflows │ ├── backport.yml │ ├── lint.yaml │ ├── overrides.yaml │ ├── pytest.yaml │ └── release.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── MANIFEST.in ├── README.md ├── inventory_tools ├── __init__.py ├── customize.py ├── docs │ ├── assets │ │ ├── fridge.png │ │ ├── manufacturing_capacity_report.png │ │ ├── md_based_on_item.png │ │ ├── md_create_rfq.png │ │ ├── md_draft_po_qty.png │ │ ├── md_item_based_banner.png │ │ ├── md_item_based_po.png │ │ ├── md_item_based_rfq.png │ │ ├── md_po_dialog.png │ │ ├── md_purchase_order.png │ │ ├── md_report_view.png │ │ ├── md_rfq.png │ │ ├── md_rfq_dialog.png │ │ ├── md_selection.png │ │ ├── md_settings_detail.png │ │ ├── md_supplier_item_rfq.png │ │ ├── qd_banner.png │ │ ├── qd_dialog.png │ │ ├── qd_report_view.png │ │ ├── qd_sales_order.png │ │ ├── qd_selection.png │ │ ├── qd_settings_detail.png │ │ ├── qd_split_qty_edition.png │ │ ├── settings.png │ │ ├── subc_bom.png │ │ ├── subc_draft_po.png │ │ ├── subc_fetch_se.png │ │ ├── subc_pi_reconciliation.png │ │ ├── subc_po_items_work_orders.png │ │ ├── subc_se_manufacture.png │ │ ├── subc_se_manufacture_items.png │ │ ├── subc_se_material_transfer.png │ │ ├── subc_wo_subcontracting_button.png │ │ ├── uom_item.png │ │ ├── uom_options.png │ │ └── warehouse_tree.png │ ├── exampledata.md │ ├── index.md │ ├── landed_costing.md │ ├── manufacturing_capacity.md │ ├── material_demand.md │ ├── quotation_demand.md │ ├── uom_enforcement.md │ ├── warehouse_path.md │ ├── wo_subcontracting.md │ └── work_order_subcontracting.md ├── hooks.py ├── inventory_tools │ ├── __init__.py │ ├── boot.py │ ├── custom │ │ ├── bom.json │ │ ├── item.json │ │ ├── item_supplier.json │ │ ├── operation.json │ │ ├── purchase_invoice.json │ │ ├── purchase_invoice_item.json │ │ ├── purchase_order.json │ │ ├── purchase_order_item.json │ │ ├── sales_order.json │ │ ├── stock_entry_detail.json │ │ ├── supplier.json │ │ └── work_order.json │ ├── doctype │ │ ├── __init__.py │ │ ├── alternative_workstation │ │ │ ├── alternative_workstation.json │ │ │ └── alternative_workstation.py │ │ ├── inventory_tools_settings │ │ │ ├── __init__.py │ │ │ ├── inventory_tools_settings.js │ │ │ ├── inventory_tools_settings.json │ │ │ ├── inventory_tools_settings.py │ │ │ └── test_inventory_tools_settings.py │ │ ├── purchase_invoice_subcontracting_detail │ │ │ ├── __init__.py │ │ │ ├── purchase_invoice_subcontracting_detail.json │ │ │ └── purchase_invoice_subcontracting_detail.py │ │ ├── purchase_order_subcontracting_detail │ │ │ ├── __init__.py │ │ │ ├── purchase_order_subcontracting_detail.json │ │ │ └── purchase_order_subcontracting_detail.py │ │ └── subcontracting_default │ │ │ ├── __init__.py │ │ │ ├── subcontracting_default.json │ │ │ └── subcontracting_default.py │ ├── overrides │ │ ├── job_card.py │ │ ├── operation.py │ │ ├── production_plan.py │ │ ├── purchase_invoice.py │ │ ├── purchase_order.py │ │ ├── purchase_receipt.py │ │ ├── sales_order.py │ │ ├── stock_entry.py │ │ ├── uom.py │ │ ├── warehouse.py │ │ ├── work_order.py │ │ └── workstation.py │ └── report │ │ ├── __init__.py │ │ ├── manufacturing_capacity │ │ ├── __init__.py │ │ ├── manufacturing_capacity.js │ │ ├── manufacturing_capacity.json │ │ └── manufacturing_capacity.py │ │ ├── material_demand │ │ ├── __init__.py │ │ ├── material_demand.js │ │ ├── material_demand.json │ │ └── material_demand.py │ │ └── quotation_demand │ │ ├── __init__.py │ │ ├── quotation_demand.js │ │ ├── quotation_demand.json │ │ └── quotation_demand.py ├── modules.txt ├── patches.txt ├── patches │ └── rename_alternative_workstation.py ├── public │ ├── .gitkeep │ └── js │ │ ├── custom │ │ ├── item.js │ │ ├── job_card_custom.js │ │ ├── operation_custom.js │ │ ├── purchase_invoice_custom.js │ │ ├── purchase_order_custom.js │ │ ├── stock_entry_custom.js │ │ └── work_order_custom.js │ │ ├── inventory_tools.bundle.js │ │ ├── uom_enforcement.js │ │ └── utils.js ├── tests │ ├── conftest.py │ ├── fixtures.py │ ├── setup.py │ ├── test_aggregated_purchasing.py │ ├── test_alternative_workstation.py │ ├── test_manufacturing_capacity.py │ ├── test_material_demand.py │ ├── test_overproduction.py │ ├── test_quotation_demand_report.py │ ├── test_uom.py │ └── test_warehouse_path.py └── www │ ├── __init__.py │ ├── bulk-order.html │ └── bulk_order.py ├── license.txt ├── mypy.ini ├── package.json ├── poetry.lock ├── pyproject.toml ├── setup.py └── yarn.lock /.editorconfig: -------------------------------------------------------------------------------- 1 | # Root editor config file 2 | root = true 3 | 4 | # Common settings 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | trim_trailing_whitespace = true 9 | charset = utf-8 10 | 11 | # python, js indentation settings 12 | [{*.py,*.js,*.vue,*.css,*.scss,*.html}] 13 | indent_style = tab 14 | indent_size = 2 15 | max_line_length = 99 16 | -------------------------------------------------------------------------------- /.flake8: -------------------------------------------------------------------------------- 1 | [flake8] 2 | ignore = 3 | B001, 4 | B007, 5 | B009, 6 | B010, 7 | B950, 8 | E101, 9 | E111, 10 | E114, 11 | E116, 12 | E117, 13 | E121, 14 | E122, 15 | E123, 16 | E124, 17 | E125, 18 | E126, 19 | E127, 20 | E128, 21 | E131, 22 | E201, 23 | E202, 24 | E203, 25 | E211, 26 | E221, 27 | E222, 28 | E223, 29 | E224, 30 | E225, 31 | E226, 32 | E228, 33 | E231, 34 | E241, 35 | E242, 36 | E251, 37 | E261, 38 | E262, 39 | E265, 40 | E266, 41 | E271, 42 | E272, 43 | E273, 44 | E274, 45 | E301, 46 | E302, 47 | E303, 48 | E305, 49 | E306, 50 | E402, 51 | E501, 52 | E502, 53 | E701, 54 | E702, 55 | E703, 56 | E741, 57 | F401, 58 | F403, 59 | F405, 60 | W191, 61 | W291, 62 | W292, 63 | W293, 64 | W391, 65 | W503, 66 | W504, 67 | W604, 68 | E711, 69 | E129, 70 | F841, 71 | E713, 72 | E712, 73 | B028, 74 | 75 | max-line-length = 200 76 | exclude=,test_*.py 77 | -------------------------------------------------------------------------------- /.github/helper/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PIP_ROOT_USER_ACTION=ignore 4 | 5 | set -e 6 | 7 | # Check for merge conflicts before proceeding 8 | python -m compileall -f "${GITHUB_WORKSPACE}" 9 | if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" 10 | then echo "Found merge conflicts" 11 | exit 1 12 | fi 13 | 14 | cd ~ || exit 15 | 16 | # sudo apt update -y && sudo apt install redis-server -y 17 | 18 | pip install --upgrade pip 19 | pip install frappe-bench 20 | 21 | mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'" 22 | mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" 23 | 24 | mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE OR REPLACE DATABASE test_site" 25 | mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE OR REPLACE USER 'test_site'@'localhost' IDENTIFIED BY 'test_site'" 26 | mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_site\`.* TO 'test_site'@'localhost'" 27 | 28 | mysql --host 127.0.0.1 --port 3306 -u root -e "ALTER USER 'root'@'localhost' IDENTIFIED BY 'root'" # match site_cofig 29 | mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES" 30 | 31 | if [ "${GITHUB_EVENT_NAME}" = 'pull_request' ]; then 32 | BRANCH_NAME="${GITHUB_BASE_REF}" 33 | else 34 | BRANCH_NAME="${GITHUB_REF_NAME}" 35 | fi 36 | echo "BRANCH_NAME: ${BRANCH_NAME}" 37 | 38 | git clone https://github.com/frappe/frappe --branch "${BRANCH_NAME}" 39 | bench init frappe-bench --frappe-path ~/frappe --python "$(which python)" --skip-assets --ignore-exist 40 | 41 | mkdir ~/frappe-bench/sites/test_site 42 | cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/ 43 | 44 | cd ~/frappe-bench || exit 45 | 46 | sed -i 's/watch:/# watch:/g' Procfile 47 | sed -i 's/schedule:/# schedule:/g' Procfile 48 | sed -i 's/socketio:/# socketio:/g' Procfile 49 | sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile 50 | 51 | bench get-app hrms --branch "${BRANCH_NAME}" --skip-assets --overwrite 52 | bench get-app erpnext --branch "${BRANCH_NAME}" --skip-assets --overwrite 53 | bench get-app inventory_tools "${GITHUB_WORKSPACE}" --skip-assets 54 | 55 | printf '%s\n' 'frappe' 'erpnext' 'hrms' 'inventory_tools' > ~/frappe-bench/sites/apps.txt 56 | bench setup requirements --python 57 | bench use test_site 58 | 59 | bench start &> bench_run_logs.txt & 60 | CI=Yes & 61 | bench --site test_site reinstall --yes --admin-password admin 62 | 63 | echo "BENCH VERSION NUMBERS:" 64 | bench version 65 | echo "SITE LIST-APPS:" 66 | bench list-apps 67 | 68 | bench start &> bench_run_logs.txt & 69 | CI=Yes & 70 | bench execute 'inventory_tools.tests.setup.before_test' 71 | -------------------------------------------------------------------------------- /.github/helper/install_dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Check for merge conflicts before proceeding 4 | python -m compileall -f $GITHUB_WORKSPACE 5 | if grep -lr --exclude-dir=node_modules "^<<<<<<< " $GITHUB_WORKSPACE 6 | then echo "Found merge conflicts" 7 | exit 1 8 | fi 9 | 10 | sudo apt update -y && sudo apt install redis-server libcups2-dev mariadb-client -y 11 | -------------------------------------------------------------------------------- /.github/helper/site_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "allow_tests": true, 3 | "db_host": "127.0.0.1", 4 | "db_port": 3306, 5 | "db_name": "test_site", 6 | "db_password": "admin", 7 | "auto_email_id": "test@example.com", 8 | "mail_server": "smtp.example.com", 9 | "mail_login": "test@example.com", 10 | "mail_password": "test", 11 | "admin_password": "admin", 12 | "root_login": "root", 13 | "root_password": "admin", 14 | "host_name": "http://test_site:8000", 15 | "throttle_user_limit": 100, 16 | "developer_mode": 1, 17 | "install_apps": ["inventory_tools"] 18 | } -------------------------------------------------------------------------------- /.github/validate_customizations.py: -------------------------------------------------------------------------------- 1 | import json 2 | import pathlib 3 | import sys 4 | 5 | 6 | def scrub(txt: str) -> str: 7 | return txt.replace(" ", "_").replace("-", "_").lower() 8 | 9 | 10 | def unscrub(txt: str) -> str: 11 | return txt.replace("_", " ").replace("-", " ").title() 12 | 13 | 14 | def get_customized_doctypes(): 15 | apps_dir = pathlib.Path(__file__).resolve().parent.parent.parent 16 | apps_order = pathlib.Path(__file__).resolve().parent.parent.parent.parent / "sites" / "apps.txt" 17 | apps_order = apps_order.read_text().split("\n") 18 | customized_doctypes = {} 19 | for _app_dir in apps_order: 20 | app_dir = (apps_dir / _app_dir).resolve() 21 | if not app_dir.is_dir(): 22 | continue 23 | modules = (app_dir / _app_dir / "modules.txt").read_text().split("\n") 24 | for module in modules: 25 | if not (app_dir / _app_dir / scrub(module) / "custom").exists(): 26 | continue 27 | for custom_file in list((app_dir / _app_dir / scrub(module) / "custom").glob("**/*.json")): 28 | if custom_file.stem in customized_doctypes: 29 | customized_doctypes[custom_file.stem].append(custom_file.resolve()) 30 | else: 31 | customized_doctypes[custom_file.stem] = [custom_file.resolve()] 32 | 33 | return dict(sorted(customized_doctypes.items())) 34 | 35 | 36 | def validate_module(customized_doctypes, set_module=False): 37 | exceptions = [] 38 | app_dir = pathlib.Path(__file__).resolve().parent.parent 39 | this_app = app_dir.stem 40 | for doctype, customize_files in customized_doctypes.items(): 41 | for customize_file in customize_files: 42 | if not this_app == customize_file.parent.parent.parent.parent.stem: 43 | continue 44 | module = customize_file.parent.parent.stem 45 | file_contents = json.loads(customize_file.read_text()) 46 | if file_contents.get("custom_fields"): 47 | for custom_field in file_contents.get("custom_fields"): 48 | if set_module: 49 | custom_field["module"] = unscrub(module) 50 | continue 51 | if not custom_field.get("module"): 52 | exceptions.append( 53 | f"Custom Field for {custom_field.get('dt')} in {this_app} '{custom_field.get('fieldname')}' does not have a module key" 54 | ) 55 | continue 56 | elif custom_field.get("module") != unscrub(module): 57 | exceptions.append( 58 | f"Custom Field for {custom_field.get('dt')} in {this_app} '{custom_field.get('fieldname')}' has module key ({custom_field.get('module')}) associated with another app" 59 | ) 60 | continue 61 | if file_contents.get("property_setters"): 62 | for ps in file_contents.get("property_setters"): 63 | if set_module: 64 | ps["module"] = unscrub(module) 65 | continue 66 | if not ps.get("module"): 67 | exceptions.append( 68 | f"Property Setter for {ps.get('doc_type')} in {this_app} '{ps.get('property')}' on {ps.get('field_name')} does not have a module key" 69 | ) 70 | continue 71 | elif ps.get("module") != unscrub(module): 72 | exceptions.append( 73 | f"Property Setter for {ps.get('doc_type')} in {this_app} '{ps.get('property')}' on {ps.get('field_name')} has module key ({ps.get('module')}) associated with another app" 74 | ) 75 | continue 76 | if set_module: 77 | with customize_file.open("w", encoding="UTF-8") as target: 78 | json.dump(file_contents, target, sort_keys=True, indent=2) 79 | 80 | return exceptions 81 | 82 | 83 | def validate_no_custom_perms(customized_doctypes): 84 | exceptions = [] 85 | this_app = pathlib.Path(__file__).resolve().parent.parent.stem 86 | for doctype, customize_files in customized_doctypes.items(): 87 | for customize_file in customize_files: 88 | if not this_app == customize_file.parent.parent.parent.parent.stem: 89 | continue 90 | file_contents = json.loads(customize_file.read_text()) 91 | if file_contents.get("custom_perms"): 92 | exceptions.append(f"Customization for {doctype} in {this_app} contains custom permissions") 93 | return exceptions 94 | 95 | 96 | def validate_duplicate_customizations(customized_doctypes): 97 | exceptions = [] 98 | common_fields = {} 99 | common_property_setters = {} 100 | app_dir = pathlib.Path(__file__).resolve().parent.parent 101 | this_app = app_dir.stem 102 | for doctype, customize_files in customized_doctypes.items(): 103 | if len(customize_files) == 1: 104 | continue 105 | common_fields[doctype] = {} 106 | common_property_setters[doctype] = {} 107 | for customize_file in customize_files: 108 | module = customize_file.parent.parent.stem 109 | app = customize_file.parent.parent.parent.parent.stem 110 | file_contents = json.loads(customize_file.read_text()) 111 | if file_contents.get("custom_fields"): 112 | fields = [cf.get("fieldname") for cf in file_contents.get("custom_fields")] 113 | common_fields[doctype][module] = fields 114 | if file_contents.get("property_setters"): 115 | ps = [ps.get("name") for ps in file_contents.get("property_setters")] 116 | common_property_setters[doctype][module] = ps 117 | 118 | for doctype, module_and_fields in common_fields.items(): 119 | if this_app not in module_and_fields.keys(): 120 | continue 121 | this_modules_fields = module_and_fields.pop(this_app) 122 | for module, fields in module_and_fields.items(): 123 | for field in fields: 124 | if field in this_modules_fields: 125 | exceptions.append( 126 | f"Custom Field for {unscrub(doctype)} in {this_app} '{field}' also appears in customizations for {module}" 127 | ) 128 | 129 | for doctype, module_and_ps in common_property_setters.items(): 130 | if this_app not in module_and_ps.keys(): 131 | continue 132 | this_modules_ps = module_and_ps.pop(this_app) 133 | for module, ps in module_and_ps.items(): 134 | for p in ps: 135 | if p in this_modules_ps: 136 | exceptions.append( 137 | f"Property Setter for {unscrub(doctype)} in {this_app} on '{p}' also appears in customizations for {module}" 138 | ) 139 | 140 | return exceptions 141 | 142 | 143 | def validate_customizations(set_module): 144 | customized_doctypes = get_customized_doctypes() 145 | exceptions = validate_no_custom_perms(customized_doctypes) 146 | exceptions += validate_module(customized_doctypes, set_module) 147 | exceptions += validate_duplicate_customizations(customized_doctypes) 148 | 149 | return exceptions 150 | 151 | 152 | if __name__ == "__main__": 153 | exceptions = [] 154 | set_module = False 155 | for arg in sys.argv: 156 | if arg == "--set-module": 157 | set_module = True 158 | exceptions.append(validate_customizations(set_module)) 159 | 160 | if exceptions: 161 | for exception in exceptions: 162 | [print(e) for e in exception] # TODO: colorize 163 | 164 | sys.exit(1) if all(exceptions) else sys.exit(0) 165 | -------------------------------------------------------------------------------- /.github/workflows/backport.yml: -------------------------------------------------------------------------------- 1 | name: Backport 2 | on: 3 | pull_request_target: 4 | types: 5 | - closed 6 | - labeled 7 | 8 | jobs: 9 | backport: 10 | name: Backport 11 | runs-on: ubuntu-latest 12 | # Only react to merged PRs for security reasons. 13 | # See https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target. 14 | if: > 15 | github.event.pull_request.merged 16 | && ( 17 | github.event.action == 'closed' 18 | || ( 19 | github.event.action == 'labeled' 20 | && contains(github.event.label.name, 'backport') 21 | ) 22 | ) 23 | steps: 24 | - uses: tibdex/backport@v2 25 | with: 26 | github_token: ${{ secrets.GITHUB_TOKEN }} 27 | -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | push: 5 | branches: 6 | - version-14 7 | - version-15 8 | pull_request: 9 | branches: 10 | - version-14 11 | - version-15 12 | 13 | env: 14 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 15 | 16 | jobs: 17 | mypy: 18 | needs: [ py_json_merge ] 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Checkout 22 | uses: actions/checkout@v4 23 | with: 24 | ref: ${{ github.head_ref }} 25 | fetch-depth: 2 26 | 27 | - name: Setup Python 28 | uses: actions/setup-python@v5 29 | with: 30 | python-version: '3.10' 31 | 32 | - name: Install mypy 33 | run: pip install mypy 34 | 35 | - name: Install mypy types 36 | run: mypy ./inventory_tools/. --install-types 37 | 38 | - name: Run mypy 39 | uses: sasanquaneuf/mypy-github-action@releases/v1 40 | with: 41 | checkName: 'mypy' 42 | env: 43 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 44 | 45 | black: 46 | needs: [ py_json_merge ] 47 | runs-on: ubuntu-latest 48 | steps: 49 | - name: Checkout 50 | uses: actions/checkout@v4 51 | with: 52 | ref: ${{ github.head_ref }} 53 | fetch-depth: 2 54 | 55 | - name: Setup Python 56 | uses: actions/setup-python@v5 57 | with: 58 | python-version: '3.10' 59 | 60 | - name: Install Black (Frappe) 61 | run: pip install git+https://github.com/frappe/black.git 62 | 63 | - name: Run Black (Frappe) 64 | run: black --check . 65 | 66 | prettier: 67 | needs: [ py_json_merge ] 68 | runs-on: ubuntu-latest 69 | steps: 70 | - name: Checkout 71 | uses: actions/checkout@v4 72 | with: 73 | ref: ${{ github.head_ref }} 74 | fetch-depth: 2 75 | 76 | - name: Prettify code 77 | uses: rutajdash/prettier-cli-action@v1.0.0 78 | with: 79 | config_path: ./.prettierrc.js 80 | ignore_path: ./.prettierignore 81 | 82 | - name: Prettier Output 83 | if: ${{ failure() }} 84 | shell: bash 85 | run: | 86 | echo "The following files are not formatted:" 87 | echo "${{steps.prettier-run.outputs.prettier_output}}" >> $GITHUB_OUTPUT 88 | 89 | json_diff: 90 | needs: [ py_json_merge ] 91 | runs-on: ubuntu-latest 92 | steps: 93 | - name: Checkout 94 | uses: actions/checkout@v4 95 | with: 96 | ref: ${{ github.ref }} 97 | fetch-depth: 2 98 | 99 | - name: Find JSON changes 100 | id: changed-json 101 | uses: tj-actions/changed-files@v43 102 | with: 103 | files: | 104 | **/*.json 105 | include_all_old_new_renamed_files: true 106 | 107 | - name: Copy head paths files 108 | run: | 109 | mkdir head 110 | touch head/acmr.txt 111 | for file in ${{ steps.changed-json.outputs.added_files }}; do 112 | echo "A,head/${file}" >> head/acmr.txt 113 | cp --parents $file head/ 114 | done 115 | for file in ${{ steps.changed-json.outputs.copied_files }}; do 116 | echo "C,head/${file}" >> head/acmr.txt 117 | cp --parents $file head/ 118 | done 119 | for file in ${{ steps.changed-json.outputs.modified_files }}; do 120 | echo "M,head/${file}" >> head/acmr.txt 121 | cp --parents $file head/ 122 | done 123 | for file in ${{ steps.changed-json.outputs.renamed_files }}; do 124 | echo "R,head/${file}" >> head/acmr.txt 125 | cp --parents $file head/ 126 | done 127 | 128 | - name: Checkout base 129 | run: git checkout $(git --no-pager log --oneline -n 2 | awk 'NR==2 {print $1}') 130 | 131 | - name: Copy base paths 132 | run: | 133 | mkdir base 134 | touch base/mrd.txt 135 | for file in ${{ steps.changed-json.outputs.modified_files }}; do 136 | echo "M,${file}" >> base/mrd.txt 137 | done 138 | for file in ${{ steps.changed-json.outputs.all_old_new_renamed_files }}; do 139 | echo "R,${file}" >> base/mrd.txt 140 | done 141 | for file in ${{ steps.changed-json.outputs.deleted_files }}; do 142 | echo "D,${file}" >> base/mrd.txt 143 | done 144 | 145 | 146 | py_json_merge: 147 | runs-on: ubuntu-latest 148 | steps: 149 | - uses: actions/checkout@v4 150 | 151 | - name: Fetch validator 152 | run: git clone --depth 1 https://gist.github.com/f1bf2c11f78331b2417189c385022c28.git validate_json 153 | 154 | - name: Validate JSON 155 | run: python3 validate_json/validate_json.py ./inventory_tools/inventory_tools/ 156 | 157 | - name: Compile 158 | run: python3 -m compileall -q ./ 159 | 160 | - name: Check merge 161 | run: | 162 | if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}" 163 | then echo "Found merge conflicts" 164 | exit 1 165 | fi 166 | -------------------------------------------------------------------------------- /.github/workflows/overrides.yaml: -------------------------------------------------------------------------------- 1 | name: Track Overrides 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - version-14 7 | - version-15 8 | 9 | jobs: 10 | track_overrides: 11 | runs-on: ubuntu-latest 12 | name: Track Overrides 13 | steps: 14 | - name: Checkout repository 15 | uses: actions/checkout@v4 16 | 17 | - name: Track Overrides 18 | uses: diamorafaela/track-overrides@main 19 | with: 20 | github-token: ${{ secrets.GITHUB_TOKEN }} 21 | -------------------------------------------------------------------------------- /.github/workflows/pytest.yaml: -------------------------------------------------------------------------------- 1 | name: Pytest CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - version-14 7 | - version-15 8 | pull_request: 9 | branches: 10 | - version-14 11 | - version-15 12 | 13 | permissions: 14 | contents: write 15 | checks: write 16 | issues: write 17 | pull-requests: write 18 | 19 | jobs: 20 | tests: 21 | runs-on: ${{ matrix.os }} 22 | strategy: 23 | matrix: 24 | os: [ubuntu-latest] 25 | fail-fast: false 26 | name: Server 27 | 28 | services: 29 | mariadb: 30 | image: mariadb:10.6 31 | env: 32 | MYSQL_ALLOW_EMPTY_PASSWORD: YES 33 | MYSQL_ROOT_PASSWORD: 'admin' 34 | ports: 35 | - 3306:3306 36 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 37 | 38 | steps: 39 | - name: Clone 40 | uses: actions/checkout@v4 41 | 42 | - name: Setup Python 43 | uses: actions/setup-python@v5 44 | with: 45 | python-version: '3.10' 46 | 47 | - name: Setup Node 48 | uses: actions/setup-node@v4 49 | with: 50 | node-version: 20 51 | check-latest: true 52 | cache: 'yarn' # Replaces `Get yarn cache directory path` and `yarn-cache` steps 53 | 54 | # Uncomment if running locally, remove after local testing (already available in github actions environment) 55 | # - name: Install Yarn 56 | # run: npm install -g yarn 57 | 58 | - name: Add to Hosts 59 | run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts 60 | 61 | - name: Cache pip 62 | uses: actions/cache@v4 63 | with: 64 | path: ~/.cache/pip 65 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} 66 | restore-keys: | 67 | ${{ runner.os }}-pip- 68 | ${{ runner.os }}- 69 | 70 | # - name: Get yarn cache directory path 71 | # id: yarn-cache-dir-path 72 | # run: 'echo "::set-output name=dir::$(yarn cache dir)"' 73 | 74 | # - uses: actions/cache@v3 75 | # id: yarn-cache 76 | # with: 77 | # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 78 | # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 79 | # restore-keys: | 80 | # ${{ runner.os }}-yarn- 81 | 82 | - name: Install Poetry 83 | uses: snok/install-poetry@v1 84 | 85 | - name: Install JS Dependencies 86 | run: yarn --prefer-offline 87 | 88 | - name: Install App Dependencies 89 | run: bash ${{ github.workspace }}/.github/helper/install_dependencies.sh 90 | 91 | - name: Install Bench Site and Apps 92 | env: 93 | MYSQL_HOST: 'localhost' 94 | MYSQL_PWD: 'admin' 95 | run: | 96 | bash ${{ github.workspace }}/.github/helper/install.sh 97 | 98 | - name: Run Tests 99 | working-directory: /home/runner/frappe-bench 100 | run: | 101 | source env/bin/activate 102 | cd apps/inventory_tools 103 | poetry install 104 | pytest --cov=inventory_tools --cov-report=xml --disable-warnings -s | tee pytest-coverage.txt 105 | 106 | - name: Pytest coverage comment 107 | uses: MishaKav/pytest-coverage-comment@main 108 | with: 109 | pytest-coverage-path: /home/runner/frappe-bench/apps/inventory_tools/pytest-coverage.txt 110 | pytest-xml-coverage-path: /home/runner/frappe-bench/apps/inventory_tools/coverage.xml 111 | -------------------------------------------------------------------------------- /.github/workflows/release.yaml: -------------------------------------------------------------------------------- 1 | name: Release 2 | on: 3 | push: 4 | branches: 5 | - version-14 6 | jobs: 7 | release: 8 | name: Release 9 | runs-on: ubuntu-latest 10 | concurrency: release 11 | permissions: 12 | id-token: write 13 | contents: write 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v4 17 | with: 18 | fetch-depth: 0 19 | - name: Python Semantic Release 20 | uses: python-semantic-release/python-semantic-release@master 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | git_committer_name: AgriTheory 24 | git_committer_email: support@agritheory.dev -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | *.py~ 3 | *.comp.js 4 | *.DS_Store 5 | locale 6 | .wnf-lang-status 7 | *.swp 8 | *.egg-info 9 | dist/ 10 | # build/ 11 | cloud_storage/docs/current 12 | cloud_storage/public/dist 13 | .vscode 14 | .vs 15 | node_modules 16 | .kdev4/ 17 | *.kdev4 18 | *debug.log 19 | 20 | # Not Recommended, but will remove once webpack ready 21 | package-lock.json 22 | 23 | # Byte-compiled / optimized / DLL files 24 | __pycache__/ 25 | *.py[cod] 26 | *$py.class 27 | 28 | # C extensions 29 | *.so 30 | 31 | # Distribution / packaging 32 | .Python 33 | # build/ 34 | develop-eggs/ 35 | dist/ 36 | downloads/ 37 | eggs/ 38 | .eggs/ 39 | lib64/ 40 | parts/ 41 | sdist/ 42 | var/ 43 | wheels/ 44 | *.egg-info/ 45 | .installed.cfg 46 | *.egg 47 | MANIFEST 48 | 49 | # PyInstaller 50 | # Usually these files are written by a python script from a template 51 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 52 | *.manifest 53 | *.spec 54 | 55 | # Installer logs 56 | pip-log.txt 57 | pip-delete-this-directory.txt 58 | 59 | # Unit test / coverage reports 60 | htmlcov/ 61 | .tox/ 62 | .coverage 63 | .coverage.* 64 | .cache 65 | nosetests.xml 66 | coverage.xml 67 | *.cover 68 | .hypothesis/ 69 | .pytest_cache/ 70 | .cypress-coverage 71 | 72 | # Translations 73 | *.mo 74 | *.pot 75 | 76 | # Django stuff: 77 | *.log 78 | .static_storage/ 79 | .media/ 80 | local_settings.py 81 | 82 | # Flask stuff: 83 | instance/ 84 | .webassets-cache 85 | 86 | # Scrapy stuff: 87 | .scrapy 88 | 89 | # Sphinx documentation 90 | docs/_build/ 91 | 92 | # PyBuilder 93 | target/ 94 | 95 | # Jupyter Notebook 96 | .ipynb_checkpoints 97 | 98 | # pyenv 99 | .python-version 100 | 101 | # celery beat schedule file 102 | celerybeat-schedule 103 | 104 | # SageMath parsed files 105 | *.sage.py 106 | 107 | # Environments 108 | .env 109 | .venv 110 | env/ 111 | venv/ 112 | ENV/ 113 | env.bak/ 114 | venv.bak/ 115 | 116 | # Spyder project settings 117 | .spyderproject 118 | .spyproject 119 | 120 | # Rope project settings 121 | .ropeproject 122 | 123 | # mkdocs documentation 124 | /site 125 | 126 | # mypy 127 | .mypy_cache/ 128 | 129 | # Logs 130 | logs 131 | *.log 132 | npm-debug.log* 133 | yarn-debug.log* 134 | yarn-error.log* 135 | 136 | # Runtime data 137 | pids 138 | *.pid 139 | *.seed 140 | *.pid.lock 141 | 142 | # Directory for instrumented libs generated by jscoverage/JSCover 143 | lib-cov 144 | 145 | # Coverage directory used by tools like istanbul 146 | coverage 147 | 148 | # nyc test coverage 149 | .nyc_output 150 | 151 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 152 | .grunt 153 | 154 | # Bower dependency directory (https://bower.io/) 155 | bower_components 156 | 157 | # node-waf configuration 158 | .lock-wscript 159 | 160 | # Compiled binary addons (https://nodejs.org/api/addons.html) 161 | build/Release 162 | 163 | # Dependency directories 164 | node_modules/ 165 | jspm_packages/ 166 | 167 | # Typescript v1 declaration files 168 | typings/ 169 | 170 | # Optional npm cache directory 171 | .npm 172 | 173 | # Optional eslint cache 174 | .eslintcache 175 | 176 | # Optional REPL history 177 | .node_repl_history 178 | 179 | # Output of 'npm pack' 180 | *.tgz 181 | 182 | # Yarn Integrity file 183 | .yarn-integrity 184 | 185 | # dotenv environment variables file 186 | .env 187 | 188 | # next.js build output 189 | .next 190 | 191 | # cypress 192 | cypress/screenshots 193 | cypress/videos 194 | 195 | # JetBrains IDEs 196 | .idea/ 197 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | exclude: 'node_modules|.git' 2 | default_stages: [commit] 3 | fail_fast: false 4 | 5 | repos: 6 | - repo: https://github.com/pre-commit/pre-commit-hooks 7 | rev: v4.3.0 8 | hooks: 9 | - id: trailing-whitespace 10 | files: 'beam.*' 11 | exclude: '.*json$|.*txt$|.*csv|.*md|.*svg' 12 | - id: check-yaml 13 | - id: no-commit-to-branch 14 | args: ['--branch', 'develop'] 15 | - id: check-merge-conflict 16 | - id: check-ast 17 | - id: check-json 18 | - id: check-toml 19 | - id: check-yaml 20 | - id: debug-statements 21 | 22 | - repo: https://github.com/asottile/pyupgrade 23 | rev: v2.34.0 24 | hooks: 25 | - id: pyupgrade 26 | args: ['--py38-plus'] 27 | 28 | - repo: https://github.com/agritheory/black 29 | rev: 951ccf4d5bb0d692b457a5ebc4215d755618eb68 30 | hooks: 31 | - id: black 32 | 33 | - repo: local 34 | hooks: 35 | - id: prettier 36 | name: prettier 37 | entry: npx prettier -w . --config .prettierrc.js --ignore-path .prettierignore 38 | language: system 39 | 40 | - repo: https://github.com/PyCQA/isort 41 | rev: 5.12.0 42 | hooks: 43 | - id: isort 44 | 45 | - repo: https://github.com/PyCQA/flake8 46 | rev: 5.0.4 47 | hooks: 48 | - id: flake8 49 | additional_dependencies: ['flake8-bugbear'] 50 | 51 | - repo: https://github.com/codespell-project/codespell 52 | rev: v2.3.0 53 | hooks: 54 | - id: codespell 55 | additional_dependencies: 56 | - tomli 57 | 58 | - repo: https://github.com/agritheory/test_utils 59 | rev: v0.15.0 60 | hooks: 61 | - id: update_pre_commit_config 62 | - id: mypy 63 | - id: validate_copyright 64 | files: '\.(js|ts|py|md)$' 65 | args: ['--app', 'inventory_tools'] 66 | - id: clean_customized_doctypes 67 | args: ['--app', 'inventory_tools'] 68 | - id: validate_customizations 69 | - id: validate_python_dependencies 70 | 71 | ci: 72 | autoupdate_schedule: weekly 73 | skip: [] 74 | submodules: false 75 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | #------------------------------------------------------------------------------------------------------------------- 2 | # Keep this section in sync with .gitignore 3 | #------------------------------------------------------------------------------------------------------------------- 4 | 5 | *.pyc 6 | *.py~ 7 | *.comp.js 8 | *.DS_Store 9 | locale 10 | .wnf-lang-status 11 | *.swp 12 | *.egg-info 13 | dist/ 14 | # build/ 15 | inventory_tools/docs/current 16 | inventory_tools/public/dist 17 | .vscode 18 | .vs 19 | node_modules 20 | .kdev4/ 21 | *.kdev4 22 | *debug.log 23 | 24 | # Not Recommended, but will remove once webpack ready 25 | package-lock.json 26 | 27 | # Byte-compiled / optimized / DLL files 28 | __pycache__/ 29 | *.py[cod] 30 | *$py.class 31 | 32 | # C extensions 33 | *.so 34 | 35 | # Distribution / packaging 36 | .Python 37 | # build/ 38 | develop-eggs/ 39 | dist/ 40 | downloads/ 41 | eggs/ 42 | .eggs/ 43 | lib64/ 44 | parts/ 45 | sdist/ 46 | var/ 47 | wheels/ 48 | *.egg-info/ 49 | .installed.cfg 50 | *.egg 51 | MANIFEST 52 | 53 | # PyInstaller 54 | # Usually these files are written by a python script from a template 55 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 56 | *.manifest 57 | *.spec 58 | 59 | # Installer logs 60 | pip-log.txt 61 | pip-delete-this-directory.txt 62 | 63 | # Unit test / coverage reports 64 | htmlcov/ 65 | .tox/ 66 | .coverage 67 | .coverage.* 68 | .cache 69 | nosetests.xml 70 | coverage.xml 71 | *.cover 72 | .hypothesis/ 73 | .pytest_cache/ 74 | .cypress-coverage 75 | 76 | # Translations 77 | *.mo 78 | *.pot 79 | 80 | # Django stuff: 81 | *.log 82 | .static_storage/ 83 | .media/ 84 | local_settings.py 85 | 86 | # Flask stuff: 87 | instance/ 88 | .webassets-cache 89 | 90 | # Scrapy stuff: 91 | .scrapy 92 | 93 | # Sphinx documentation 94 | docs/_build/ 95 | 96 | # PyBuilder 97 | target/ 98 | 99 | # Jupyter Notebook 100 | .ipynb_checkpoints 101 | 102 | # pyenv 103 | .python-version 104 | 105 | # celery beat schedule file 106 | celerybeat-schedule 107 | 108 | # SageMath parsed files 109 | *.sage.py 110 | 111 | # Environments 112 | .env 113 | .venv 114 | env/ 115 | venv/ 116 | ENV/ 117 | env.bak/ 118 | venv.bak/ 119 | 120 | # Spyder project settings 121 | .spyderproject 122 | .spyproject 123 | 124 | # Rope project settings 125 | .ropeproject 126 | 127 | # mkdocs documentation 128 | /site 129 | 130 | # mypy 131 | .mypy_cache/ 132 | 133 | # Logs 134 | logs 135 | *.log 136 | npm-debug.log* 137 | yarn-debug.log* 138 | yarn-error.log* 139 | 140 | # Runtime data 141 | pids 142 | *.pid 143 | *.seed 144 | *.pid.lock 145 | 146 | # Directory for instrumented libs generated by jscoverage/JSCover 147 | lib-cov 148 | 149 | # Coverage directory used by tools like istanbul 150 | coverage 151 | 152 | # nyc test coverage 153 | .nyc_output 154 | 155 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 156 | .grunt 157 | 158 | # Bower dependency directory (https://bower.io/) 159 | bower_components 160 | 161 | # node-waf configuration 162 | .lock-wscript 163 | 164 | # Compiled binary addons (https://nodejs.org/api/addons.html) 165 | build/Release 166 | 167 | # Dependency directories 168 | node_modules/ 169 | jspm_packages/ 170 | 171 | # Typescript v1 declaration files 172 | typings/ 173 | 174 | # Optional npm cache directory 175 | .npm 176 | 177 | # Optional eslint cache 178 | .eslintcache 179 | 180 | # Optional REPL history 181 | .node_repl_history 182 | 183 | # Output of 'npm pack' 184 | *.tgz 185 | 186 | # Yarn Integrity file 187 | .yarn-integrity 188 | 189 | # dotenv environment variables file 190 | .env 191 | 192 | # next.js build output 193 | .next 194 | 195 | # cypress 196 | cypress/screenshots 197 | cypress/videos 198 | 199 | # JetBrains IDEs 200 | .idea/ 201 | 202 | #------------------------------------------------------------------------------------------------------------------- 203 | # Prettier-specific overrides 204 | #------------------------------------------------------------------------------------------------------------------- 205 | 206 | 207 | # Package manager files 208 | pnpm-lock.yaml 209 | yarn.lock 210 | package-lock.json 211 | shrinkwrap.json 212 | 213 | # Build outputs 214 | lib 215 | .github 216 | 217 | # Prettier reformats code blocks inside Markdown, which affects rendered output 218 | *.md 219 | 220 | # In a Frappe context HTML files are either Jinja or Resig's microtemplate format and should not be checked 221 | *.html -------------------------------------------------------------------------------- /.prettierrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | arrowParens: 'avoid', 3 | bracketSameLine: true, 4 | bracketSpacing: true, 5 | embeddedLanguageFormatting: 'auto', 6 | htmlWhitespaceSensitivity: 'css', 7 | insertPragma: false, 8 | jsxSingleQuote: false, 9 | printWidth: 120, 10 | proseWrap: 'preserve', 11 | quoteProps: 'as-needed', 12 | requirePragma: false, 13 | semi: false, 14 | singleQuote: true, 15 | tabWidth: 2, 16 | trailingComma: 'es5', 17 | useTabs: true, 18 | vueIndentScriptAndStyle: false, 19 | } 20 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include MANIFEST.in 2 | include requirements.txt 3 | include *.json 4 | include *.md 5 | include *.py 6 | include *.txt 7 | recursive-include beam *.css 8 | recursive-include beam *.csv 9 | recursive-include beam *.html 10 | recursive-include beam *.ico 11 | recursive-include beam *.js 12 | recursive-include beam *.json 13 | recursive-include beam *.md 14 | recursive-include beam *.png 15 | recursive-include beam *.py 16 | recursive-include beam *.svg 17 | recursive-include beam *.txt 18 | recursive-exclude beam *.pyc -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Inventory Tools 2 | 3 | Inventory Tools for ERPNext 4 | 5 | #### License 6 | 7 | MIT 8 | 9 | ## Install Instructions 10 | 11 | Set up a new bench, substitute a path to the python version to use, which should 3.10 latest 12 | 13 | ``` 14 | # for linux development 15 | bench init --frappe-branch version-14 {{ bench name }} --python ~/.pyenv/versions/3.10.10/bin/python3 16 | ``` 17 | Create a new site in that bench 18 | ``` 19 | cd {{ bench name }} 20 | bench new-site {{ site name }} --force --db-name {{ site name }} 21 | bench use {{ site name }} 22 | ``` 23 | Download the ERPNext app 24 | ``` 25 | bench get-app erpnext --branch version-14 26 | ``` 27 | Download this application and install all apps 28 | ``` 29 | bench get-app inventory_tools git@github.com:agritheory/inventory_tools.git 30 | ``` 31 | Set developer mode in `site_config.json` 32 | ``` 33 | cd {{ site name }} 34 | nano site_config.json 35 | 36 | "developer_mode": 1, 37 | ``` 38 | 39 | Update and get the site ready 40 | ``` 41 | bench start 42 | ``` 43 | In a new terminal window 44 | ``` 45 | bench update 46 | bench migrate 47 | bench build 48 | ``` 49 | 50 | Setup test data 51 | ```shell 52 | bench execute 'inventory_tools.tests.setup.before_test' 53 | # for complete reset to run before tests: 54 | bench reinstall --yes --admin-password admin --mariadb-root-password admin && bench execute 'inventory_tools.tests.setup.before_test' 55 | ``` 56 | 57 | To run mypy 58 | ```shell 59 | source env/bin/activate 60 | mypy ./apps/inventory_tools/inventory_tools --ignore-missing-imports 61 | ``` 62 | 63 | To run pytest 64 | ```shell 65 | source env/bin/activate 66 | pytest ~/frappe-bench/apps/inventory_tools/inventory_tools/tests -s 67 | ``` 68 | -------------------------------------------------------------------------------- /inventory_tools/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "14.6.1" 2 | 3 | 4 | """ 5 | This code loads a modified version of validate_item_details function. It's called from 6 | get_item_details, which is a whitelisted method called both client-side as well as 7 | server-side (in validation functions) for several doctypes. Because of the dual nature, 8 | two methods were needed to ensure the correct code runs in all scenarios: the standard 9 | override whitelisted method in hooks.py and the monkey patch below. 10 | 11 | The modification only applies when the Enable Work Order Subcontracting feature is selected in 12 | Inventory Tools Settings. If the feature is turned off, the default ERPNext behavior runs. 13 | """ 14 | import erpnext.stock.get_item_details 15 | 16 | from inventory_tools.inventory_tools.overrides.purchase_order import validate_item_details 17 | 18 | erpnext.stock.get_item_details.validate_item_details = validate_item_details 19 | -------------------------------------------------------------------------------- /inventory_tools/customize.py: -------------------------------------------------------------------------------- 1 | import json 2 | from pathlib import Path 3 | 4 | import frappe 5 | 6 | 7 | def load_customizations(): 8 | customizations_directory = ( 9 | Path().cwd().parent / "apps" / "check_run" / "check_run" / "check_run" / "custom" 10 | ) 11 | files = list(customizations_directory.glob("**/*.json")) 12 | for file in files: 13 | customizations = json.loads(Path(file).read_text()) 14 | for field in customizations.get("custom_fields"): 15 | if field.get("module") != "Inventory Tools": 16 | continue 17 | existing_field = frappe.get_value("Custom Field", field.get("name")) 18 | custom_field = ( 19 | frappe.get_doc("Custom Field", field.get("name")) 20 | if existing_field 21 | else frappe.new_doc("Custom Field") 22 | ) 23 | field.pop("modified") 24 | {custom_field.set(key, value) for key, value in field.items()} 25 | custom_field.flags.ignore_permissions = True 26 | custom_field.flags.ignore_version = True 27 | custom_field.save() 28 | for prop in customizations.get("property_setters"): 29 | if prop.get("module") != "Inventory Tools": 30 | continue 31 | property_setter = frappe.get_doc( 32 | { 33 | "name": prop.get("name"), 34 | "doctype": "Property Setter", 35 | "doctype_or_field": prop.get("doctype_or_field"), 36 | "doc_type": prop.get("doc_type"), 37 | "field_name": prop.get("field_name"), 38 | "property": prop.get("property"), 39 | "value": prop.get("value"), 40 | "property_type": prop.get("property_type"), 41 | } 42 | ) 43 | property_setter.flags.ignore_permissions = True 44 | property_setter.insert() 45 | -------------------------------------------------------------------------------- /inventory_tools/docs/assets/fridge.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/fridge.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/manufacturing_capacity_report.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/manufacturing_capacity_report.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_based_on_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_based_on_item.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_create_rfq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_create_rfq.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_draft_po_qty.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_draft_po_qty.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_item_based_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_item_based_banner.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_item_based_po.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_item_based_po.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_item_based_rfq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_item_based_rfq.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_po_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_po_dialog.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_purchase_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_purchase_order.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_report_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_report_view.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_rfq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_rfq.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_rfq_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_rfq_dialog.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_selection.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_settings_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_settings_detail.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/md_supplier_item_rfq.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/md_supplier_item_rfq.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/qd_banner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/qd_banner.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/qd_dialog.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/qd_dialog.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/qd_report_view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/qd_report_view.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/qd_sales_order.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/qd_sales_order.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/qd_selection.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/qd_selection.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/qd_settings_detail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/qd_settings_detail.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/qd_split_qty_edition.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/qd_split_qty_edition.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/settings.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/settings.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_bom.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_bom.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_draft_po.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_draft_po.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_fetch_se.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_fetch_se.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_pi_reconciliation.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_pi_reconciliation.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_po_items_work_orders.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_po_items_work_orders.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_se_manufacture.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_se_manufacture.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_se_manufacture_items.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_se_manufacture_items.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_se_material_transfer.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_se_material_transfer.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/subc_wo_subcontracting_button.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/subc_wo_subcontracting_button.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/uom_item.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/uom_item.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/uom_options.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/uom_options.png -------------------------------------------------------------------------------- /inventory_tools/docs/assets/warehouse_tree.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/docs/assets/warehouse_tree.png -------------------------------------------------------------------------------- /inventory_tools/docs/exampledata.md: -------------------------------------------------------------------------------- 1 | # Using the Example Data to Experiment with Inventory Tools 2 | 3 | The Inventory Tools application comes with a `setup.py` script that is completely optional to use. If you execute the script, it populates an ERPNext site with demo business data for a fictitious company called Ambrosia Pie Company. The data enable you to experiment and test the Inventory Tools application's functionality before installing the app into your ERPNext site. 4 | 5 | It's recommended to install the demo data into its own site to avoid potential interference with the configuration or data in your organization's ERPNext site. 6 | 7 | With `bench start` running in the background, run the following command to install the demo data: 8 | 9 | ```shell 10 | bench execute 'inventory_tools.tests.setup.before_test' 11 | # to reinstall from scratch and set up test data 12 | bench reinstall --yes --admin-password admin --mariadb-root-password admin && bench execute 'inventory_tools.tests.setup.before_test' 13 | ``` 14 | 15 | Refer to the [application repository](https://github.com/agritheory/inventory_tools) for detailed instructions for how to set up a bench, a new site, and installing ERPNext and the Inventory Tools application. 16 | -------------------------------------------------------------------------------- /inventory_tools/docs/index.md: -------------------------------------------------------------------------------- 1 | # Inventory Tools Documentation 2 | 3 | The Inventory Tools application enhances and extends inventory-related functionality and workflows in ERPNext. It includes the following features: 4 | 5 | - **[Material Demand](./material_demand.md)**: a report-based interface to aggregate required Items across multiple sources, then optionally create Purchase Orders or Request for Quotations 6 | - **[UOM Enforcement](./uom_enforcement.md)**: for doctypes that have an Items table or Unit of Measure (UOM) fields, this feature restricts the user's options from arbitrary selections to only UOMs defined in the Item master with a specified conversion factor 7 | - **[Warehouse Path](./warehouse_path.md)**: for any warehouse selection field, this features helps clearly identify warehouses by creating a warehouse path and adding a human-readable string under the warehouse name in the format "parent warehouse(s)->warehouse" 8 | - **[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 9 | - **[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 10 | - **[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 11 | 12 | ## Configuration 13 | 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. 14 | 15 | ![Screen shot of ](./assets/settings.png) 16 | 17 | ## Installation 18 | Full [installation instructions](https://github.com/agritheory/inventory_tools) can be found on the application's repository. 19 | 20 | Note that the application includes a script to install example data to experiment and test the app's features. See the [Using the Example Data to Experiment with Inventory Tools page](./exampledata.md) for more details. 21 | -------------------------------------------------------------------------------- /inventory_tools/docs/landed_costing.md: -------------------------------------------------------------------------------- 1 | # Inline Landed Costing 2 | 3 | Coming soon! 4 | -------------------------------------------------------------------------------- /inventory_tools/docs/manufacturing_capacity.md: -------------------------------------------------------------------------------- 1 | # Manufacturing Capacity Report 2 | 3 | Manufacturing Capacity is a report-based interface that, given a BOM and Warehouse, displays the demand and in-stock quantities for the entire hierarchy of any BOM tree containing that BOM. 4 | 5 | Once the filters are set, the report traverses the BOM tree to find the top-level parents of the given BOM. From there, it finds total demand based on outstanding Sales Orders, Material Requests (of type "Manufacture"), and Work Orders, adjusting for any overlap. In stock quantities for each level are determined based on the selected Warehouse. The Parts Can Build quantity is based on what is in stock (for non-BOM/raw material rows) or the minimum Parts can Build of sub-levels for BOM rows. 6 | 7 | The Parts Can Build Qty is slightly different for non-BOM vs BOM rows. For non-BOM (raw material) rows, it's the In Stock Qty divided by the Qty per Parent BOM. For BOM rows, it's the minimum of the Parts Can Build for all sub-assemblies. So if a BOM row requires a raw material that isn't in stock, it will show 0 Parts Can Build Qty, even if there are other sub assemblies in stock. 8 | 9 | The Difference Qty calculation is also different for non-BOM and BOM rows. Since non-BOM rows account for the In Stock Qty in the Parts Can Build Qty number, the Difference Qty is the Parts Can Build less the Demanded Qty. For BOM rows, since the Parts Can Build Qty is based off available sub-assembly item quantities (and doesn't use the In Stock Qty in that calculation), the Difference Qty is the In Stock Qty plus Parts Can Build Qty less the Demanded Qty. 10 | 11 | ![Screen shot showing the Manufacturing Capacity report output for the Ambrosia Pie BOM and all Warehouses. There are rows for all levels of the BOM hierarchy - the Pie itself, sub-level rows for each sub-assembly of the Pie Crust and Pie Filling, with rows below each of those for the raw materials comprising each BOM. Columns include the BOM, Item, Description, Quantity per Parent BOM, BOM UoM, Demanded Quantity, In Stock Quantity, Parts Can Build quantity, and the Difference Quantity (demanded quantity less parts can build quantity).](./assets/manufacturing_capacity_report.png) 12 | -------------------------------------------------------------------------------- /inventory_tools/docs/material_demand.md: -------------------------------------------------------------------------------- 1 | # Material Demand 2 | 3 | Material Demand is a report-based interface that allows you to aggregate required Items across multiple Material Requests, Suppliers, and requesting Companies. From there, you can create draft Purchase Orders (PO), draft Request for Quotations (RFQ), or a combination of the two based on the Item's configuration. 4 | 5 | ![Screen shot of the Material Demand report showing rows of Items grouped by supplier with columns for the Supplier, Material Request document ID, Required By date, Item, MR Qty, Draft POs, Total Selected, UOM, Price, and Selected Amount](./assets/md_report_view.png) 6 | 7 | The right-hand side of the report has selection boxes to indicate which rows of Items to include to create the documents. Ticking the top-level supplier box will automatically check all the Items for that supplier. 8 | 9 | ![Screen shot of a Material Demand Report with the boxes next to supplier Chelsea Fruit Co's Items all checked](./assets/md_selection.png) 10 | 11 | Once you're satisfied with your selections, clicking the Create button will give you three options to generate draft documents: 12 | 13 | 1. Create PO(s) will create a Purchase Order for each supplier selected. If there is more than one company requesting materials from the same supplier, it marks the PO as a Multi-Company Purchase Order 14 | 2. Create RFQ(s) will create a Request for Quotation for each supplier-item combination 15 | 3. Create based on Item will create RFQs and/or POs depending on how the Item's supplier list is configured (in the Item master) 16 | 17 | All generated documents remain in draft status to allow you to make edits as needed before submitting them. 18 | 19 | ### Create Purchase Orders 20 | If you select the Create PO(s) option, a dialog window will appear to select the Company if it hasn't already been supplied in the filter section. 21 | 22 | ![Screen shot of the dialog window to enter the Company for the Purchase Orders](./assets/md_po_dialog.png) 23 | 24 | You can find the new documents in the Purchase Order listview. 25 | 26 | ![Screen shot of the Purchase Order listview showing the new draft Purchase Order for Chelsea Fruit Co](./assets/md_purchase_order.png) 27 | 28 | After generating the draft Purchase Orders, the Material Demand report updates to display the quantity ordered in the Draft PO column. Note that after you submit the Purchase Orders, the Items rows no longer show in the report. 29 | 30 | ![Screen shot of the Material Demand report where the Draft POs column shows the quantity ordered for the Chelsea Fruit Co Items that were selected to be in the Purchase Order](./assets/md_draft_po_qty.png) 31 | 32 | ### Create Request for Quotation 33 | ![Screen shot of Material Demand report with Parchment Paper, Pie Box, and Pie Tin Items selected for both the Freedom Provisions and Unity Bakery Supply suppliers. The Create RFQ(s) selection is highlighted in the Create button dropdown](./assets/md_create_rfq.png) 34 | 35 | If you select the Create RFQ(s) option, a dialog window will appear to select the Company and Email Template. 36 | 37 | ![Screen shot of the dialog window to enter the Company and Email Template for the RFQs](./assets/md_rfq_dialog.png) 38 | 39 | You can find the new documents in the Request for Quotation listview and make edits as needed before submitting them. 40 | 41 | ![Screen shot of a Request for Quotation document with two suppliers (Freedom Provisions and Unity Bakery Supply) and Parchment Paper, Pie Box, and Pie Tin listed in the Items table](./assets/md_rfq.png) 42 | 43 | ### Create Based on Item 44 | The final option Create based on Item will create Purchase Orders and/or RFQs depending on how each Item's supplier list is configured in the Item master. 45 | 46 | For a given Item, go to the Supplier Items table (found in the Purchasing tab's Supplier Details section) and click Edit for a Suppler. If you check the Requires RFQ? box, the Material Demand report will create an RFQ for that Item. If the box is left unchecked, the report generates a Purchase Order. 47 | 48 | ![Screen shot of the Edit Supplier form for the Supplier Items table for the Pie Tin Item. The Supplier Freedom Provisions has the Requires RFQ? box selected](./assets/md_supplier_item_rfq.png) 49 | 50 | The selection process works the same as the other options. 51 | 52 | ![Screen shot of the Material Demand report with Pie Tin, Salt, and Sugar Items selected for the Freedom Provisions Supplier and the Create based on Item option highlighted in the Create button dropdown](./assets/md_based_on_item.png) 53 | 54 | The report displays a banner to notify you of how many of each document were created. 55 | 56 | ![Screen shot of a banner that says 1 Purchase Orders created 1 Request For Quotation created](./assets/md_item_based_banner.png) 57 | 58 | The Items in each document will correspond to the Item Supplier configuration. Since the Pie Tin Item is the only one that required an RFQ, it's included in the new RFQ, whereas the other Items are in the new Purchase Order. 59 | 60 | ![Screen shot of the draft RFQ for Freedom Provisions with Pie Tin in the Items table](./assets/md_item_based_rfq.png) 61 | 62 | ![Screen shot of the draft PO for Freedom Provisions with Salt and Sugar in the Items table](./assets/md_item_based_po.png) 63 | 64 | ## Configuration 65 | The Material Demand report is available on installation of the Inventory Tools application, but there are configuration options in Inventory Tools Settings to modify its behavior. 66 | 67 | ![Screen shot of the two relevant fields (Purchase Order Aggregation Company and Aggregated Purchasing Warehouse) to configure the Material Demand report](./assets/md_settings_detail.png) 68 | 69 | When the Material Demand report generates Purchase Orders, it fills the PO Company field with the company specified in the filter, or if that's blank, the one provided in the dialog window. To retain this default behavior, leave the Purchase Order Aggregation Company field in Inventory Tools Settings blank. However, if you populate this field, the report will use its value in the Purchase Order's Company field instead. In either case, if there's more than one company requesting materials from the same supplier, the report will select the Multi-Company Purchase Order box for that supplier's PO. 70 | 71 | The Aggregated Purchasing Warehouse field has a similar impact on the report's behavior. By default, the field is blank and the Material Demand report applies the warehouses set per Item in the Material Request as the Item's warehouse in the new Purchase Order. If you set a value in this field, the report will instead use the specified warehouse for each Item in the Purchase Order. 72 | 73 | See the Create Based on Item section for instructions on how to configure specific Item-Supplier combinations to require an RFQ. 74 | -------------------------------------------------------------------------------- /inventory_tools/docs/quotation_demand.md: -------------------------------------------------------------------------------- 1 | # Quotation Demand 2 | 3 | Quotation Demand is a report-based interface that allows you to aggregate required Items across multiple Quotations, Customers, and requesting Companies. From there, you can create draft Sales Orders (SO). 4 | 5 | ![Screen shot of the Quotation Demand report showing rows of Items grouped by customer with columns for the Customer,Quotation document ID, Company, Date, Warehouse, and Item](./assets/qd_report_view.png) 6 | 7 | The right-hand side of the report has selection boxes to indicate which rows of Items to include to create the documents. Ticking the top-level customer box will automatically check all the Items for that customer. 8 | 9 | ![Screen shot of a Quotation Demand Report with the boxes next to customer Almacs Food Group Items all checked](./assets/qd_selection.png) 10 | 11 | Once you're satisfied with your selections, clicking the Create button will give you the option to generate draft documents: 12 | 13 | 1. Create SO(s) will create a Sales Order for each customer selected. If there is more than one company requesting items from the same customer, it marks the SO as a Multi-Company Sales Order 14 | 15 | All generated documents remain in draft status to allow you to make edits as needed before submitting them. 16 | 17 | ### Create Sales Orders 18 | If you select the Create SO(s) option, a dialog window will appear to select the Company if it hasn't already been supplied in the filter section. 19 | 20 | ![Screen shot of the dialog window to enter the Company for the Sales Orders](./assets/qd_dialog.png) 21 | 22 | You can find the new documents in the Sales Order listview. 23 | 24 | ![Screen shot of the Sales Order listview showing the new draft Sales Orders for Almacs Food Group](./assets/qd_sales_order.png) 25 | 26 | 27 | ## Configuration 28 | The Quotation Demand report is available on installation of the Inventory Tools application, but there are configuration options in Inventory Tools Settings to modify its behavior. 29 | 30 | ![Screen shot of the two relevant fields (Sales Order Aggregation Company and Aggregated Sales Warehouse) to configure the Quotation Demand report](./assets/qd_settings_detail.png) 31 | 32 | When the Quotation Demand report generates Sales Orders, it fills the SO Company field with the company specified in the filter, or if that's blank, the one provided in the dialog window. To retain this default behavior, leave the Sales Order Aggregation Company field in Inventory Tools Settings blank. However, if you populate this field, the report will use its value in the Sales Order's Company field instead. In either case, if there's more than one company requesting materials from the same customer, the report will select the Multi-Company Sales Order box for that customer's SO. 33 | 34 | The Aggregated Sales Warehouse field has a similar impact on the report's behavior. By default, the field is blank and the Quotation Demand report applies the warehouses set per Item in the Quotation as the Item's warehouse in the new Sales Order. If you set a value in this field, the report will instead use the specified warehouse for each Item in the Sales Order. 35 | 36 | -------------------------------------------------------------------------------- /inventory_tools/docs/uom_enforcement.md: -------------------------------------------------------------------------------- 1 | # UOM Enforcement 2 | 3 | By default, ERPNext allows its users to select any Unit of Measure (UOM) for any Item. If no conversion ratio exists between the UOM selected and the Item's stock UOM, ERPNext assumes it should be 1:1. This feature enforces that a user is only able to select and use valid UOMs. If an Item has no way to be understood in "Linear Feet" or "Volts", those UOMs will not be included as options in any UOM field for that Item. 4 | 5 | The following example shows the Parchment Paper Item has two defined UOMs in the Item master. 6 | 7 | ![Screen shot of the Item master Inventory section for Parchment Paper showing two defined Units of Measure in the UOMs table. There is Nos with a conversion factor of 1 and Box with a conversion factor of 100](./assets/uom_item.png) 8 | 9 | In a Purchase Order, the Edit Item form for Parchment Paper has only two options in the UOM field - the two defined UOMs from the Item master. 10 | 11 | ![Screen shot of a Purchase Order Edit Item form for Parchment Paper where the dropdown selections for the UOM field only shows Nos and Box as options](./assets/uom_options.png) 12 | 13 | ## Configuration 14 | To enable this feature, check the "Enforce UOMs" box in Inventory Tools Settings. 15 | 16 | ## Extending or Overriding This Feature 17 | In the event you need to enter arbitrary UOMs in a specific doctype, you can selectively override this feature in your custom app. The following example shows how to override UOM enforcement in the Opportunity doctype. 18 | 19 | ```python 20 | # custom_app/hooks.py 21 | inventory_tools_uom_enforcement = { 22 | "Opportunity": {"Opportunity Item": {"items": []}}, 23 | } 24 | ``` 25 | Here we have removed "uom" from the list of fields to check. 26 | 27 | To extend this feature to a custom doctype, follow the pattern established in the configuration object: 28 | 29 | ```python 30 | # custom_app/hooks.py 31 | inventory_tools_uom_enforcement = { 32 | "My Custom Doctype": { 33 | "My Custom Doctype": ["uom"] # a UOM field at parent/ form level 34 | "My Custom Doctype Child Table": {"items": ["uom", "weight_uom", ]}, # UOM fields in a child table 35 | "My Second Custom Doctype Child Table": {"mistakes": ["uom", "weight_uom", ]}, # UOM fields in a second child table 36 | }, 37 | } 38 | ``` 39 | 40 | -------------------------------------------------------------------------------- /inventory_tools/docs/warehouse_path.md: -------------------------------------------------------------------------------- 1 | # Warehouse Path 2 | ERPNext allows its user to construct hierarchial abstractions for their physical facilities. This can make it difficult to know when you are selecting a Warehouse if it is "Bin A" in the "Storage Closet" or if is "Bin A" from the "Repair Supplies" Warehouse. 3 | 4 | This feature encodes the Warehouse hierarchy into a string, which becomes searchable, and allows the user to more easily understand which Warehouse they are selecting. 5 | 6 | ## Example 7 | In this example there are two Warehouses that start with "Refridger..." and while they are different, they could be mixed up. 8 | 9 | ![Screen shot of the example company's Warehouse Tree. It includes a Refrigerated Display Warehouse (under the Baked Goods group) and a Refrigerator Warehouse](assets/warehouse_tree.png) 10 | 11 | In the Link dropdown, the full path is given, omitting the root "All Warehouses - APC" and the company abbreviation at each level. 12 | 13 | ![Screen shot of a Target Warehouse field with "Refr" typed in, and two Warehouse options in the dropdown. The Refrigerated Display option has "Baked Goods -> Refrigerated Display" under its name, and the Refrigerator option has "Refrigerator" under its name](assets/fridge.png) 14 | 15 | This view shows the user-provided search text of "Refr..." matches the two similarly-named Warehouses. The Warehouse path under each option's name clearly distinguishes the choices by specifying each Warehouse's hierarchy. 16 | 17 | ## Configuration 18 | To enable this feature, check the "Update Warehouse Path" box in Inventory Tools Settings. 19 | -------------------------------------------------------------------------------- /inventory_tools/docs/wo_subcontracting.md: -------------------------------------------------------------------------------- 1 | # Subcontracting Workflow via Work Order 2 | 3 | ERPNext's subcontracting workflow has changed substantially with each version release of the software, generally without backwards compatibility. This feature offers an alternative to using a Purchase Receipt or the Version-14 Subcontracting Order and Subcontracting Receipt documents. Instead, it allows you to manage the subcontracting process with Work Orders, Purchase Orders and Invoices, and Stock Entries. 4 | 5 | ## Configuration 6 | To enable this feature, check the "Enable Work Order Subcontracting" box in Inventory Tools Settings. 7 | 8 | The feature also has the following prerequisite configuration to get it up and running: 9 | 10 | - Items: 11 | - For any subcontracted Item, check the Supply Raw Materials for Purchase box. This is found in the Manufacturing section of the Item master 12 | - Optional: create a service Item (uncheck the Maintain Stock box in the Item master) to represent the services provided by the subcontractor. The ERPNext workflow requires service Items in subcontracted Purchase documents, however this is completely optional in this feature. Instead, when a subcontracted Purchase Order is created from a Work Order, the finished good received from the subcontractor will populate the Items section of the form 13 | - Warehouses: 14 | - create a dedicated warehouse for each subcontractor to track materials sent to them 15 | - Bill of Materials (BOM): 16 | - Create a BOM for the subcontracted Item that includes raw materials supplied to the subcontractor and check the Is Subcontracted box. This BOM should not include any operations, as they are done by the subcontractor 17 | - The feature supports processes done at times in-house and at times by a subcontractor. There should be separate BOMs for each process, with the appropriate one linked to in the Work Order 18 | 19 | ## Work Orders 20 | This feature starts with a Work Order for a subcontracted Item. As noted in the Configuration section, there should be a BOM in the system for the Item, which lists all the raw materials supplied to the subcontractor, no operations, and has the Is Subcontracted box checked. 21 | 22 | ![Screen shot of a second BOM for Pie Crust, an Item made in-house as well as subcontracted, with the Is Subcontracted box checked](./assets/subc_bom.png) 23 | 24 | The Work Order should tie to the subcontracted BOM. Once you submit the Work Order, you'll see a Subcontracting button with the options to create a new subcontracting Purchase Order (PO) or add the Item to an existing one. 25 | 26 | ![Screen shot of a submitted Work Order for the subcontracted version of Pie Crusts. The subcontracted BOM](./assets/subc_wo_subcontracting_button.png) 27 | 28 | ### Create a New Purchase Order 29 | If you click Create Subcontract PO, you'll see a dialog to enter the supplier, then the system generates a draft Purchase Order. 30 | 31 | ![Screen shot of the Purchase Orders listview containing a new draft PO](./assets/subc_draft_po.png) 32 | 33 | By default, the new subcontracting Purchase Order adds the same Item as what is being manufactured in the Work Order. This can be changed as needed. There's also a new Subcontracting Detail section with a table that tracks any Work Order(s) linked to the PO. The table includes the Work Order name, Warehouse, finished good Item name, BOM name, quantity, and UOM information. 34 | 35 | ![Screen shot of the Items and Subcontracting tables in the subcontracted Purchase Order. The Items table includes 10 Pie Crusts and the Subcontracting table includes the Work Order details](./assets/subc_po_items_work_orders.png) 36 | 37 | ### Add to an Existing Purchase Order 38 | If you click Add to Existing PO, you'll see a dialog to select the applicable Purchase Order. The options are limited to non-cancelled, subcontracted POs only. Note that if the selected PO is already submitted, this action will cancel and amend that PO to include the additional Items from the Work Order and keep it in draft status until you review and submit it. 39 | 40 | ### Stock Entries 41 | This workflow expects you to utilize Stock Entries to record raw material transfers to the subcontractor and to receive subcontracted finished goods into inventory. This part of the process retains default ERPNext behavior. You can create a Stock Entry of type Material Transfer for Manufacture against the Work Order to track raw materials going to the subcontractor. The Target Warehouse for Items should be a dedicated warehouse in the system for the subcontractor. 42 | 43 | ![Screen shot of a Stock Entry of type Material Transfer for Manufacture showing the raw materials required for 10 pie crusts being moved to the subcontractor's Credible Contract Baking warehouse](./assets/subc_se_material_transfer.png) 44 | 45 | When the subcontractor delivers finished Items, a Stock Entry of type Manufacture records the consumption of raw materials from the subcontractor's warehouse and the receipt of Items into the appropriate company warehouse. 46 | 47 | ![Screen shot showing a new Stock Entry of type Manufacture against the Work Order for the subcontracted goods](./assets/subc_se_manufacture.png) 48 | 49 | ![Screen shot of the Items table in a new Stock Entry of type Manufacture that shows the consumption of the raw materials from the Credible Contract Baking warehouse and receipt of Pie Crusts in return](./assets/subc_se_manufacture_items.png) 50 | 51 | ### Purchase Invoice and Stock Entry Reconciliation 52 | At invoice time, this feature includes a reconciliation tool in the Purchase Invoice (PI) to compare Items ordered from the subcontractor with what has been received and what has been paid for. Click the Fetch Stock Entries button under the Subcontracting table to collect all Stock Entries of type Manufacture for subcontracted goods. A dialog window allows you to (optionally) narrow the selections within a date range. 53 | 54 | ![Screen shot of the new Subcontracting Detail section in a subcontracted Purchase Invoice with a Fetch Stock Entries button](./assets/subc_fetch_se.png) 55 | 56 | The results of all valid stock entries populate the Subcontracting table, and show the Work Order name, Purchase Order name, Item name, total quantity, the quantity that's been paid for already (Paid Qty), and the outstanding quantity remaining (To Pay Qty). 57 | 58 | In the example, the Purchase Invoice covers two Purchase Orders (each generated off of a Work Order) for 50 and 10 Pie Crusts, respectively. A separate PI already covered 20 Pie Crusts, as shown in the Paid Qty column, so there are only 40 remaining un-invoiced Pie Crusts. The Accepted Qty field in the Items table can be adjusted to capture only what remains to be invoiced. 59 | 60 | ![Screen shot of the Subcontracting Detail table with two Stock Entries. One is for 50 Pie Crusts, of which 20 have been paid for and 30 remain outstanding, the other is for 10 Pie Crusts of which all are outstanding. The Accepted Qty field in the Items table for the 50 Pie Crusts is boxed to highlight that it needs to be adjusted for what's already been invoiced](./assets/subc_pi_reconciliation.png) 61 | -------------------------------------------------------------------------------- /inventory_tools/docs/work_order_subcontracting.md: -------------------------------------------------------------------------------- 1 | # Work Order Subcontracting 2 | 3 | Material Demand is a report-based interface that allows a user to aggregate required items across multiple Material Requests, Suppliers, requesting Companies and create draft Purchase Orders in that process. 4 | 5 | ... describe selection process with screenshots 6 | 7 | ## Configuration 8 | Multi-company setup and workflow 9 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/boot.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def boot_session(bootinfo): 5 | bootinfo.inventory_tools_settings = {} 6 | for company in frappe.get_all("Inventory Tools Settings", pluck="company"): 7 | settings = frappe.get_doc("Inventory Tools Settings", company) 8 | bootinfo.inventory_tools_settings[company] = settings 9 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/bom.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2023-07-20 11:26:07.355568", 15 | "default": "0", 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "BOM", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "is_subcontracted", 23 | "fieldtype": "Check", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 10, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "allow_alternative_item", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Is Subcontracted", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2023-07-20 11:26:07.355568", 42 | "modified_by": "Administrator", 43 | "module": "Inventory Tools", 44 | "name": "BOM-is_subcontracted", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": null, 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 0, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "sort_options": 0, 60 | "translatable": 0, 61 | "unique": 0, 62 | "width": null 63 | }, 64 | { 65 | "_assign": null, 66 | "_comments": null, 67 | "_liked_by": null, 68 | "_user_tags": null, 69 | "allow_in_quick_entry": 0, 70 | "allow_on_submit": 1, 71 | "bold": 0, 72 | "collapsible": 0, 73 | "collapsible_depends_on": null, 74 | "columns": 0, 75 | "creation": "2024-01-09 18:02:12.685077", 76 | "default": null, 77 | "depends_on": null, 78 | "description": null, 79 | "docstatus": 0, 80 | "dt": "BOM", 81 | "fetch_from": null, 82 | "fetch_if_empty": 0, 83 | "fieldname": "create_job_cards_automatically", 84 | "fieldtype": "Select", 85 | "hidden": 0, 86 | "hide_border": 0, 87 | "hide_days": 0, 88 | "hide_seconds": 0, 89 | "idx": 12, 90 | "ignore_user_permissions": 0, 91 | "ignore_xss_filter": 0, 92 | "in_global_search": 0, 93 | "in_list_view": 0, 94 | "in_preview": 0, 95 | "in_standard_filter": 0, 96 | "insert_after": "set_rate_of_sub_assembly_item_based_on_bom", 97 | "is_system_generated": 0, 98 | "is_virtual": 0, 99 | "label": "Create Job Card(s) Automatically", 100 | "length": 0, 101 | "mandatory_depends_on": null, 102 | "modified": "2024-01-09 18:02:12.685077", 103 | "modified_by": "Administrator", 104 | "module": "Inventory Tools", 105 | "name": "BOM-create_job_cards_automatically", 106 | "no_copy": 0, 107 | "non_negative": 0, 108 | "options": "\nYes\nNo", 109 | "owner": "Administrator", 110 | "permlevel": 0, 111 | "precision": "", 112 | "print_hide": 0, 113 | "print_hide_if_no_value": 0, 114 | "print_width": null, 115 | "read_only": 0, 116 | "read_only_depends_on": null, 117 | "report_hide": 0, 118 | "reqd": 0, 119 | "search_index": 0, 120 | "sort_options": 0, 121 | "translatable": 0, 122 | "unique": 0, 123 | "width": null 124 | }, 125 | { 126 | "_assign": null, 127 | "_comments": null, 128 | "_liked_by": null, 129 | "_user_tags": null, 130 | "allow_in_quick_entry": 0, 131 | "allow_on_submit": 1, 132 | "bold": 0, 133 | "collapsible": 0, 134 | "collapsible_depends_on": null, 135 | "columns": 0, 136 | "creation": "2024-01-11 10:05:40.558053", 137 | "default": null, 138 | "depends_on": null, 139 | "description": null, 140 | "docstatus": 0, 141 | "dt": "BOM", 142 | "fetch_from": null, 143 | "fetch_if_empty": 0, 144 | "fieldname": "overproduction_percentage_for_work_order", 145 | "fieldtype": "Percent", 146 | "hidden": 0, 147 | "hide_border": 0, 148 | "hide_days": 0, 149 | "hide_seconds": 0, 150 | "idx": 12, 151 | "ignore_user_permissions": 0, 152 | "ignore_xss_filter": 0, 153 | "in_global_search": 0, 154 | "in_list_view": 0, 155 | "in_preview": 0, 156 | "in_standard_filter": 0, 157 | "insert_after": "create_job_cards_automatically", 158 | "is_system_generated": 0, 159 | "is_virtual": 0, 160 | "label": "Overproduction Percentage For Work Order", 161 | "length": 0, 162 | "mandatory_depends_on": null, 163 | "modified": "2024-01-11 10:07:40.558053", 164 | "modified_by": "Administrator", 165 | "module": "Inventory Tools", 166 | "name": "BOM-overproduction_percentage_for_work_order", 167 | "no_copy": 0, 168 | "non_negative": 0, 169 | "options": null, 170 | "owner": "Administrator", 171 | "permlevel": 0, 172 | "precision": "", 173 | "print_hide": 0, 174 | "print_hide_if_no_value": 0, 175 | "print_width": null, 176 | "read_only": 0, 177 | "read_only_depends_on": null, 178 | "report_hide": 0, 179 | "reqd": 0, 180 | "search_index": 0, 181 | "sort_options": 0, 182 | "translatable": 0, 183 | "unique": 0, 184 | "width": null 185 | } 186 | ], 187 | "custom_perms": [], 188 | "doctype": "BOM", 189 | "links": [], 190 | "property_setters": [], 191 | "sync_on_migrate": 1 192 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/item.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [], 3 | "custom_perms": [], 4 | "doctype": "Item", 5 | "links": [], 6 | "property_setters": [ 7 | { 8 | "_assign": null, 9 | "_comments": null, 10 | "_liked_by": null, 11 | "_user_tags": null, 12 | "creation": "2024-07-28 04:36:24.732734", 13 | "default_value": null, 14 | "doc_type": "Item", 15 | "docstatus": 0, 16 | "doctype_or_field": "DocField", 17 | "field_name": "weight_uom", 18 | "idx": 0, 19 | "is_system_generated": 0, 20 | "modified": "2024-07-28 04:36:24.732734", 21 | "modified_by": "Administrator", 22 | "module": null, 23 | "name": "Item-weight_uom-mandatory_depends_on", 24 | "owner": "Administrator", 25 | "property": "mandatory_depends_on", 26 | "property_type": "Data", 27 | "row_name": null, 28 | "value": "eval:doc.weight_per_unit" 29 | }, 30 | { 31 | "_assign": null, 32 | "_comments": null, 33 | "_liked_by": null, 34 | "_user_tags": null, 35 | "creation": "2024-07-28 04:36:24.552718", 36 | "default_value": null, 37 | "doc_type": "Item", 38 | "docstatus": 0, 39 | "doctype_or_field": "DocField", 40 | "field_name": "weight_per_unit", 41 | "idx": 0, 42 | "is_system_generated": 0, 43 | "modified": "2024-07-28 04:36:24.552718", 44 | "modified_by": "Administrator", 45 | "module": null, 46 | "name": "Item-weight_per_unit-mandatory_depends_on", 47 | "owner": "Administrator", 48 | "property": "mandatory_depends_on", 49 | "property_type": "Data", 50 | "row_name": null, 51 | "value": "eval:doc.weight_uom" 52 | } 53 | ], 54 | "sync_on_migrate": 1 55 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/item_supplier.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2023-07-28 06:45:43.330833", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Item Supplier", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "requires_rfq", 23 | "fieldtype": "Check", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 3, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "supplier_part_no", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Requires RFQ?", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2023-07-28 06:45:43.330833", 42 | "modified_by": "Administrator", 43 | "module": "Inventory Tools", 44 | "name": "Item Supplier-requires_rfq", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": null, 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 0, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "translatable": 0, 60 | "unique": 0, 61 | "width": null 62 | } 63 | ], 64 | "custom_perms": [], 65 | "doctype": "Item Supplier", 66 | "links": [], 67 | "property_setters": [], 68 | "sync_on_migrate": 1 69 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/operation.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-02-16 05:04:42.422068", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Operation", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "alternative_workstations", 23 | "fieldtype": "Table MultiSelect", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 1, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "workstation", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Alternative Workstations", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2024-02-16 05:04:42.422068", 42 | "modified_by": "Administrator", 43 | "module": "Inventory Tools", 44 | "name": "Operation-alternative_workstations", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": "Alternative Workstation", 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 0, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "sort_options": 0, 60 | "translatable": 0, 61 | "unique": 0, 62 | "width": null 63 | } 64 | ], 65 | "custom_perms": [], 66 | "doctype": "Operation", 67 | "links": [], 68 | "property_setters": [], 69 | "sync_on_migrate": 1 70 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/purchase_invoice_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [], 3 | "custom_perms": [], 4 | "doctype": "Purchase Invoice Item", 5 | "links": [], 6 | "property_setters": [ 7 | { 8 | "_assign": null, 9 | "_comments": null, 10 | "_liked_by": null, 11 | "_user_tags": null, 12 | "creation": "2023-06-26 15:52:33.804721", 13 | "default_value": null, 14 | "doc_type": "Purchase Invoice Item", 15 | "docstatus": 0, 16 | "doctype_or_field": "DocField", 17 | "field_name": "from_warehouse", 18 | "idx": 0, 19 | "is_system_generated": 0, 20 | "modified": "2023-06-26 15:52:33.804721", 21 | "modified_by": "Administrator", 22 | "module": "Inventory Tools", 23 | "name": "Purchase Invoice Item-from_warehouse-hidden", 24 | "owner": "Administrator", 25 | "property": "hidden", 26 | "property_type": "Check", 27 | "row_name": null, 28 | "value": "1" 29 | } 30 | ], 31 | "sync_on_migrate": 1 32 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/purchase_order_item.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-06-04 14:06:25.331707", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Purchase Order Item", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "material_request_company", 23 | "fieldtype": "Link", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 60, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "references_section", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Material Request Company", 39 | "length": 0, 40 | "mandatory_depends_on": "", 41 | "modified": "2024-06-04 15:44:22.874371", 42 | "modified_by": "Administrator", 43 | "module": "Inventory Tools", 44 | "name": "Purchase Order Item-material_request_company", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": "Company", 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 1, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "sort_options": 0, 60 | "translatable": 0, 61 | "unique": 0, 62 | "width": null 63 | } 64 | ], 65 | "custom_perms": [], 66 | "doctype": "Purchase Order Item", 67 | "links": [], 68 | "property_setters": [], 69 | "sync_on_migrate": 1 70 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/sales_order.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2024-03-14 10:10:39.991377", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Sales Order", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "multi_company_sales_order", 23 | "fieldtype": "Check", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 9, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "order_type", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Multi-Company Sales Order", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2024-03-14 10:09:39.991377", 42 | "modified_by": "Administrator", 43 | "module": "Inventory Tools", 44 | "name": "Sales Order-multi_company_sales_order", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": null, 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 1, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "sort_options": 0, 60 | "translatable": 0, 61 | "unique": 0, 62 | "width": null 63 | } 64 | ], 65 | "custom_perms": [], 66 | "doctype": "Sales Order", 67 | "links": [], 68 | "property_setters": [], 69 | "sync_on_migrate": 1 70 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/stock_entry_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2023-07-05 16:09:54.479727", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Stock Entry Detail", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "paid_qty", 23 | "fieldtype": "Float", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 66, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "job_card_item", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Paid Qty", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2023-07-05 16:09:54.479727", 42 | "modified_by": "Administrator", 43 | "module": "Inventory Tools", 44 | "name": "Stock Entry Detail-paid_qty", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": null, 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 1, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "translatable": 0, 60 | "unique": 0, 61 | "width": null 62 | } 63 | ], 64 | "custom_perms": [], 65 | "doctype": "Stock Entry Detail", 66 | "links": [], 67 | "property_setters": [ 68 | { 69 | "_assign": null, 70 | "_comments": null, 71 | "_liked_by": null, 72 | "_user_tags": null, 73 | "creation": "2023-06-26 15:52:33.190835", 74 | "default_value": null, 75 | "doc_type": "Stock Entry Detail", 76 | "docstatus": 0, 77 | "doctype_or_field": "DocField", 78 | "field_name": "barcode", 79 | "idx": 0, 80 | "is_system_generated": 1, 81 | "modified": "2023-06-26 15:52:33.190835", 82 | "modified_by": "Administrator", 83 | "module": "Inventory Tools", 84 | "name": "Stock Entry Detail-barcode-hidden", 85 | "owner": "Administrator", 86 | "property": "hidden", 87 | "property_type": "Check", 88 | "row_name": null, 89 | "value": "0" 90 | } 91 | ], 92 | "sync_on_migrate": 1 93 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/supplier.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 0, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2023-07-27 15:45:43.601325", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Supplier", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "irs_1099", 23 | "fieldtype": "Check", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 28, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "tax_id", 36 | "is_system_generated": 1, 37 | "is_virtual": 0, 38 | "label": "Is IRS 1099 reporting required for supplier?", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2023-08-01 10:25:59.395190", 42 | "modified_by": "Administrator", 43 | "module": "Inventory Tools", 44 | "name": "Supplier-irs_1099", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": null, 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 0, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "translatable": 1, 60 | "unique": 0, 61 | "width": null 62 | }, 63 | { 64 | "_assign": null, 65 | "_comments": null, 66 | "_liked_by": null, 67 | "_user_tags": null, 68 | "allow_in_quick_entry": 0, 69 | "allow_on_submit": 0, 70 | "bold": 0, 71 | "collapsible": 0, 72 | "collapsible_depends_on": null, 73 | "columns": 0, 74 | "creation": "2023-08-01 10:37:52.840344", 75 | "default": null, 76 | "depends_on": null, 77 | "description": null, 78 | "docstatus": 0, 79 | "dt": "Supplier", 80 | "fetch_from": null, 81 | "fetch_if_empty": 0, 82 | "fieldname": "subcontracting_defaults", 83 | "fieldtype": "Table", 84 | "hidden": 0, 85 | "hide_border": 0, 86 | "hide_days": 0, 87 | "hide_seconds": 0, 88 | "idx": 62, 89 | "ignore_user_permissions": 0, 90 | "ignore_xss_filter": 0, 91 | "in_global_search": 0, 92 | "in_list_view": 0, 93 | "in_preview": 0, 94 | "in_standard_filter": 0, 95 | "insert_after": "section_break_suus6", 96 | "is_system_generated": 0, 97 | "is_virtual": 0, 98 | "label": "", 99 | "length": 0, 100 | "mandatory_depends_on": null, 101 | "modified": "2023-08-01 10:37:52.840344", 102 | "modified_by": "Administrator", 103 | "module": "Inventory Tools", 104 | "name": "Supplier-subcontracting_defaults", 105 | "no_copy": 0, 106 | "non_negative": 0, 107 | "options": "Subcontracting Default", 108 | "owner": "Administrator", 109 | "permlevel": 0, 110 | "precision": "", 111 | "print_hide": 0, 112 | "print_hide_if_no_value": 0, 113 | "print_width": null, 114 | "read_only": 0, 115 | "read_only_depends_on": null, 116 | "report_hide": 0, 117 | "reqd": 0, 118 | "search_index": 0, 119 | "translatable": 0, 120 | "unique": 0, 121 | "width": null 122 | } 123 | ], 124 | "custom_perms": [], 125 | "doctype": "Supplier", 126 | "links": [], 127 | "property_setters": [], 128 | "sync_on_migrate": 1 129 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/custom/work_order.json: -------------------------------------------------------------------------------- 1 | { 2 | "custom_fields": [ 3 | { 4 | "_assign": null, 5 | "_comments": null, 6 | "_liked_by": null, 7 | "_user_tags": null, 8 | "allow_in_quick_entry": 0, 9 | "allow_on_submit": 1, 10 | "bold": 0, 11 | "collapsible": 0, 12 | "collapsible_depends_on": null, 13 | "columns": 0, 14 | "creation": "2023-07-25 14:44:39.211400", 15 | "default": null, 16 | "depends_on": null, 17 | "description": null, 18 | "docstatus": 0, 19 | "dt": "Work Order", 20 | "fetch_from": null, 21 | "fetch_if_empty": 0, 22 | "fieldname": "supplier", 23 | "fieldtype": "Link", 24 | "hidden": 0, 25 | "hide_border": 0, 26 | "hide_days": 0, 27 | "hide_seconds": 0, 28 | "idx": 16, 29 | "ignore_user_permissions": 0, 30 | "ignore_xss_filter": 0, 31 | "in_global_search": 0, 32 | "in_list_view": 0, 33 | "in_preview": 0, 34 | "in_standard_filter": 0, 35 | "insert_after": "project", 36 | "is_system_generated": 0, 37 | "is_virtual": 0, 38 | "label": "Subcontract Supplier", 39 | "length": 0, 40 | "mandatory_depends_on": null, 41 | "modified": "2023-07-25 14:44:39.211400", 42 | "modified_by": "Administrator", 43 | "module": "Inventory Tools", 44 | "name": "Work Order-supplier", 45 | "no_copy": 0, 46 | "non_negative": 0, 47 | "options": "Supplier", 48 | "owner": "Administrator", 49 | "permlevel": 0, 50 | "precision": "", 51 | "print_hide": 0, 52 | "print_hide_if_no_value": 0, 53 | "print_width": null, 54 | "read_only": 1, 55 | "read_only_depends_on": null, 56 | "report_hide": 0, 57 | "reqd": 0, 58 | "search_index": 0, 59 | "translatable": 0, 60 | "unique": 0, 61 | "width": null 62 | } 63 | ], 64 | "custom_perms": [], 65 | "doctype": "Work Order", 66 | "links": [ 67 | { 68 | "creation": "2023-08-09 15:31:20.121332", 69 | "custom": 1, 70 | "docstatus": 0, 71 | "group": null, 72 | "hidden": 0, 73 | "idx": 1, 74 | "is_child_table": 1, 75 | "link_doctype": "Purchase Invoice Subcontracting Detail", 76 | "link_fieldname": "work_order", 77 | "modified": "2023-08-09 15:34:47.812937", 78 | "modified_by": "Administrator", 79 | "name": "2bb6c50f93", 80 | "owner": "Administrator", 81 | "parent": "Work Order", 82 | "parent_doctype": "Purchase Invoice", 83 | "parentfield": "links", 84 | "parenttype": "Customize Form", 85 | "table_fieldname": "subcontracting" 86 | }, 87 | { 88 | "creation": "2023-08-09 15:29:40.533005", 89 | "custom": 1, 90 | "docstatus": 0, 91 | "group": null, 92 | "hidden": 0, 93 | "idx": 0, 94 | "is_child_table": 1, 95 | "link_doctype": "Purchase Order Subcontracting Detail", 96 | "link_fieldname": "work_order", 97 | "modified": "2023-08-09 15:34:47.802753", 98 | "modified_by": "Administrator", 99 | "name": "6c25a06ffd", 100 | "owner": "Administrator", 101 | "parent": "Work Order", 102 | "parent_doctype": "Purchase Order", 103 | "parentfield": "links", 104 | "parenttype": "Customize Form", 105 | "table_fieldname": "subcontracting" 106 | } 107 | ], 108 | "property_setters": [ 109 | { 110 | "_assign": null, 111 | "_comments": null, 112 | "_liked_by": null, 113 | "_user_tags": null, 114 | "creation": "2023-08-09 15:34:47.822577", 115 | "default_value": null, 116 | "doc_type": "Work Order", 117 | "docstatus": 0, 118 | "doctype_or_field": "DocType", 119 | "field_name": null, 120 | "idx": 0, 121 | "is_system_generated": 0, 122 | "modified": "2023-08-09 15:34:47.822577", 123 | "modified_by": "Administrator", 124 | "module": "Inventory Tools", 125 | "name": "Work Order-main-links_order", 126 | "owner": "Administrator", 127 | "property": "links_order", 128 | "property_type": "Small Text", 129 | "row_name": null, 130 | "value": "[\"6c25a06ffd\", \"2bb6c50f93\"]" 131 | } 132 | ], 133 | "sync_on_migrate": 1 134 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/doctype/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/alternative_workstation/alternative_workstation.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2024-02-01 04:03:58.033322", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "workstation" 10 | ], 11 | "fields": [ 12 | { 13 | "fieldname": "workstation", 14 | "fieldtype": "Link", 15 | "in_list_view": 1, 16 | "label": "Workstation", 17 | "options": "Workstation" 18 | } 19 | ], 20 | "index_web_pages_for_search": 1, 21 | "istable": 1, 22 | "links": [], 23 | "modified": "2024-02-01 07:04:30.059469", 24 | "modified_by": "Administrator", 25 | "module": "Inventory Tools", 26 | "name": "Alternative Workstation", 27 | "owner": "Administrator", 28 | "permissions": [], 29 | "sort_field": "modified", 30 | "sort_order": "DESC", 31 | "states": [] 32 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/alternative_workstation/alternative_workstation.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, AgriTheory and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class AlternativeWorkstation(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/inventory_tools_settings/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/doctype/inventory_tools_settings/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2023, AgriTheory and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.ui.form.on('Inventory Tools Settings', { 5 | // refresh: function(frm) { 6 | // } 7 | }) 8 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "autoname": "field:company", 4 | "creation": "2023-05-30 20:24:26.832647", 5 | "default_view": "List", 6 | "doctype": "DocType", 7 | "editable_grid": 1, 8 | "engine": "InnoDB", 9 | "field_order": [ 10 | "company", 11 | "material_demand_section", 12 | "purchase_order_aggregation_company", 13 | "column_break_vgsiq", 14 | "aggregated_purchasing_warehouse", 15 | "quotation_demand_section", 16 | "sales_order_aggregation_company", 17 | "column_break_ljy4o", 18 | "aggregated_sales_warehouse", 19 | "work_order_subcontracting_section", 20 | "enable_work_order_subcontracting", 21 | "create_purchase_orders", 22 | "bom_column", 23 | "create_job_cards_automatically", 24 | "column_break_ilobm", 25 | "overproduction_percentage_for_work_order", 26 | "section_break_0", 27 | "update_warehouse_path", 28 | "column_break_ddssn", 29 | "allow_alternative_workstations", 30 | "uoms_section", 31 | "enforce_uoms" 32 | ], 33 | "fields": [ 34 | { 35 | "fieldname": "company", 36 | "fieldtype": "Link", 37 | "label": "Company", 38 | "options": "Company", 39 | "unique": 1 40 | }, 41 | { 42 | "fieldname": "material_demand_section", 43 | "fieldtype": "Section Break", 44 | "label": "Material Demand" 45 | }, 46 | { 47 | "description": "Leave this field blank to disallow aggregation for this company", 48 | "fieldname": "purchase_order_aggregation_company", 49 | "fieldtype": "Link", 50 | "label": "Purchase Order Aggregation Company", 51 | "options": "Company" 52 | }, 53 | { 54 | "fieldname": "column_break_vgsiq", 55 | "fieldtype": "Column Break" 56 | }, 57 | { 58 | "description": "When set, this will override the Material Requests Receiving Warehouse. If not set, this will map Warehouses from Material Request into Purchase Order.", 59 | "fieldname": "aggregated_purchasing_warehouse", 60 | "fieldtype": "Link", 61 | "label": "Aggregated Purchasing Warehouse", 62 | "options": "Warehouse" 63 | }, 64 | { 65 | "fieldname": "work_order_subcontracting_section", 66 | "fieldtype": "Section Break", 67 | "label": "Work Order Subcontracting" 68 | }, 69 | { 70 | "default": "0", 71 | "fieldname": "enable_work_order_subcontracting", 72 | "fieldtype": "Check", 73 | "label": "Enable Work Order Subcontracting" 74 | }, 75 | { 76 | "default": "0", 77 | "fieldname": "create_purchase_orders", 78 | "fieldtype": "Check", 79 | "label": "Create Purchase Orders in Production Plan" 80 | }, 81 | { 82 | "fieldname": "section_break_0", 83 | "fieldtype": "Section Break", 84 | "label": "Warehouses and Workstations" 85 | }, 86 | { 87 | "default": "0", 88 | "fieldname": "update_warehouse_path", 89 | "fieldtype": "Check", 90 | "label": "Update Warehouse Path" 91 | }, 92 | { 93 | "fieldname": "uoms_section", 94 | "fieldtype": "Section Break", 95 | "label": "UOMs" 96 | }, 97 | { 98 | "default": "0", 99 | "fieldname": "enforce_uoms", 100 | "fieldtype": "Check", 101 | "label": "Enforce UOMs" 102 | }, 103 | { 104 | "fieldname": "bom_column", 105 | "fieldtype": "Section Break", 106 | "label": "Work Order" 107 | }, 108 | { 109 | "default": "Yes", 110 | "fieldname": "create_job_cards_automatically", 111 | "fieldtype": "Select", 112 | "label": "Create Job Card(s) Automatically", 113 | "options": "Yes\nNo" 114 | }, 115 | { 116 | "fieldname": "column_break_ilobm", 117 | "fieldtype": "Column Break" 118 | }, 119 | { 120 | "fieldname": "overproduction_percentage_for_work_order", 121 | "fieldtype": "Percent", 122 | "label": "Overproduction Percentage For Work Order" 123 | }, 124 | { 125 | "fieldname": "quotation_demand_section", 126 | "fieldtype": "Section Break", 127 | "label": "Quotation Demand" 128 | }, 129 | { 130 | "description": "Leave this field blank to disallow aggregation for this company", 131 | "fieldname": "sales_order_aggregation_company", 132 | "fieldtype": "Link", 133 | "label": "Sales Order Aggregation Company", 134 | "options": "Company" 135 | }, 136 | { 137 | "fieldname": "column_break_ljy4o", 138 | "fieldtype": "Column Break" 139 | }, 140 | { 141 | "description": "When set, this will override the Quotation Warehouse. If not set, this will map Warehouses from Quotation into Sales Order.", 142 | "fieldname": "aggregated_sales_warehouse", 143 | "fieldtype": "Link", 144 | "label": "Aggregated Sales Warehouse", 145 | "options": "Warehouse" 146 | }, 147 | { 148 | "fieldname": "column_break_ddssn", 149 | "fieldtype": "Column Break" 150 | }, 151 | { 152 | "default": "0", 153 | "fieldname": "allow_alternative_workstations", 154 | "fieldtype": "Check", 155 | "label": "Allow Alternative Workstations" 156 | } 157 | ], 158 | "index_web_pages_for_search": 1, 159 | "links": [], 160 | "modified": "2024-05-13 13:19:00.665445", 161 | "modified_by": "Administrator", 162 | "module": "Inventory Tools", 163 | "name": "Inventory Tools Settings", 164 | "naming_rule": "Expression (old style)", 165 | "owner": "Administrator", 166 | "permissions": [ 167 | { 168 | "create": 1, 169 | "delete": 1, 170 | "email": 1, 171 | "export": 1, 172 | "print": 1, 173 | "read": 1, 174 | "report": 1, 175 | "role": "System Manager", 176 | "share": 1, 177 | "write": 1 178 | }, 179 | { 180 | "create": 1, 181 | "delete": 1, 182 | "email": 1, 183 | "export": 1, 184 | "print": 1, 185 | "read": 1, 186 | "report": 1, 187 | "role": "Stock Manager", 188 | "share": 1, 189 | "write": 1 190 | }, 191 | { 192 | "create": 1, 193 | "delete": 1, 194 | "email": 1, 195 | "export": 1, 196 | "print": 1, 197 | "read": 1, 198 | "report": 1, 199 | "role": "Purchase Master Manager", 200 | "share": 1, 201 | "write": 1 202 | } 203 | ], 204 | "sort_field": "modified", 205 | "sort_order": "DESC", 206 | "states": [] 207 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/inventory_tools_settings/inventory_tools_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class InventoryToolsSettings(Document): 9 | def validate(self): 10 | self.create_warehouse_path_custom_field() 11 | self.validate_single_aggregation_company() 12 | 13 | def create_warehouse_path_custom_field(self): 14 | if frappe.db.exists("Custom Field", "Warehouse-warehouse_path"): 15 | if not self.update_warehouse_path: 16 | frappe.set_value("Custom Field", "Warehouse-warehouse_path", "hidden", 1) 17 | frappe.set_value( 18 | "Property Setter", 19 | {"doctype_or_field": "DocType", "doc_type": "Warehouse", "property": "search_fields"}, 20 | "search_fields", 21 | "", 22 | ) 23 | return 24 | cf = frappe.new_doc("Custom Field") 25 | cf.dt = "Warehouse" 26 | cf.fieldname = "warehouse_path" 27 | cf.fieldtype = "Data" 28 | cf.label = "Warehouse Path" 29 | cf.module = "Inventory Tools" 30 | cf.insert_after = "disabled" 31 | cf.no_copy = 1 32 | cf.save() 33 | 34 | ps = frappe.new_doc("Property Setter") 35 | ps.doctype_or_field = "DocType" 36 | ps.doc_type = "Warehouse" 37 | ps.property = "search_fields" 38 | ps.module = "Inventory Tools" 39 | ps.property_type = "Data" 40 | ps.value = "warehouse_path" 41 | ps.save() 42 | 43 | for warehouse in frappe.get_all("Warehouse"): 44 | wh = frappe.get_doc("Warehouse", warehouse) 45 | wh.save() 46 | 47 | def validate_single_aggregation_company(self): 48 | if not self.purchase_order_aggregation_company and not self.sales_order_aggregation_company: 49 | return 50 | 51 | itsl = [ 52 | frappe.get_doc("Inventory Tools Settings", i) 53 | for i in frappe.get_all("Inventory Tools Settings") 54 | ] 55 | 56 | if self.purchase_order_aggregation_company: 57 | for its in itsl: 58 | if its.name == self.name or not its.purchase_order_aggregation_company: 59 | continue 60 | if self.purchase_order_aggregation_company != its.purchase_order_aggregation_company: 61 | frappe.throw( 62 | f"Purchase Order Aggregation Company in {its.name} Inventory Tools Settings is set to {its.purchase_order_aggregation_company}" 63 | ) 64 | if self.aggregated_purchasing_warehouse != its.aggregated_purchasing_warehouse: 65 | frappe.throw( 66 | f"Purchase Order Aggregation Company in {its.name} Inventory Tools Settings is set to {its.aggregated_purchasing_warehouse}" 67 | ) 68 | 69 | if self.sales_order_aggregation_company: 70 | for its in itsl: 71 | if its.name == self.name or not its.sales_order_aggregation_company: 72 | continue 73 | if self.sales_order_aggregation_company != its.sales_order_aggregation_company: 74 | frappe.throw( 75 | f"Sales Order Aggregation Company in {its.name} Inventory Tools Settings is set to {its.sales_order_aggregation_company}" 76 | ) 77 | if self.sales_order_aggregation_company != its.sales_order_aggregation_company: 78 | frappe.throw( 79 | f"Sales Order Aggregation Company in {its.name} Inventory Tools Settings is set to {its.sales_order_aggregation_company}" 80 | ) 81 | 82 | 83 | @frappe.whitelist() 84 | def create_inventory_tools_settings(doc, method=None) -> None: 85 | if not frappe.db.exists("Company", doc.name) or frappe.db.exists( 86 | "Inventory Tools Settings", {"company": doc.name} 87 | ): 88 | return 89 | its = frappe.new_doc("Inventory Tools Settings") 90 | its.company = doc.name 91 | its.save() 92 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/inventory_tools_settings/test_inventory_tools_settings.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and Contributors 2 | # See license.txt 3 | 4 | # import frappe 5 | from frappe.tests.utils import FrappeTestCase 6 | 7 | 8 | class TestInventoryToolsSettings(FrappeTestCase): 9 | pass 10 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/purchase_invoice_subcontracting_detail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/doctype/purchase_invoice_subcontracting_detail/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/purchase_invoice_subcontracting_detail/purchase_invoice_subcontracting_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2023-06-27 14:58:07.361859", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "work_order", 10 | "stock_entry", 11 | "purchase_order", 12 | "se_detail_name", 13 | "item_code", 14 | "item_name", 15 | "qty", 16 | "transfer_qty", 17 | "paid_qty", 18 | "to_pay_qty", 19 | "uom", 20 | "stock_uom", 21 | "conversion_factor", 22 | "valuation_rate" 23 | ], 24 | "fields": [ 25 | { 26 | "columns": 2, 27 | "fieldname": "work_order", 28 | "fieldtype": "Link", 29 | "in_list_view": 1, 30 | "label": "Work Order", 31 | "options": "Work Order" 32 | }, 33 | { 34 | "columns": 2, 35 | "fieldname": "stock_entry", 36 | "fieldtype": "Link", 37 | "in_list_view": 1, 38 | "label": "Stock Entry", 39 | "options": "Stock Entry" 40 | }, 41 | { 42 | "fieldname": "purchase_order", 43 | "fieldtype": "Link", 44 | "in_list_view": 1, 45 | "label": "Purchase Order", 46 | "options": "Purchase Order" 47 | }, 48 | { 49 | "fieldname": "se_detail_name", 50 | "fieldtype": "Data", 51 | "hidden": 1, 52 | "label": "Stock Entry Detail Name" 53 | }, 54 | { 55 | "fieldname": "item_code", 56 | "fieldtype": "Link", 57 | "label": "Item Code", 58 | "options": "Item" 59 | }, 60 | { 61 | "fieldname": "item_name", 62 | "fieldtype": "Data", 63 | "in_list_view": 1, 64 | "label": "Item Name" 65 | }, 66 | { 67 | "fieldname": "qty", 68 | "fieldtype": "Float", 69 | "in_list_view": 1, 70 | "label": "Total Qty" 71 | }, 72 | { 73 | "fieldname": "transfer_qty", 74 | "fieldtype": "Float", 75 | "label": "Qty as per Stock UOM" 76 | }, 77 | { 78 | "fieldname": "uom", 79 | "fieldtype": "Link", 80 | "label": "UOM", 81 | "options": "UOM" 82 | }, 83 | { 84 | "fieldname": "stock_uom", 85 | "fieldtype": "Link", 86 | "label": "Stock UOM", 87 | "options": "UOM" 88 | }, 89 | { 90 | "fieldname": "conversion_factor", 91 | "fieldtype": "Float", 92 | "label": "Conversion Factor" 93 | }, 94 | { 95 | "fieldname": "valuation_rate", 96 | "fieldtype": "Currency", 97 | "label": "Valuation Rate", 98 | "options": "Company:company:default_currency" 99 | }, 100 | { 101 | "fieldname": "paid_qty", 102 | "fieldtype": "Float", 103 | "in_list_view": 1, 104 | "label": "Paid Qty" 105 | }, 106 | { 107 | "fieldname": "to_pay_qty", 108 | "fieldtype": "Float", 109 | "in_list_view": 1, 110 | "label": "To Pay Qty" 111 | } 112 | ], 113 | "index_web_pages_for_search": 1, 114 | "istable": 1, 115 | "links": [], 116 | "modified": "2023-08-09 15:38:04.549747", 117 | "modified_by": "Administrator", 118 | "module": "Inventory Tools", 119 | "name": "Purchase Invoice Subcontracting Detail", 120 | "owner": "Administrator", 121 | "permissions": [], 122 | "sort_field": "modified", 123 | "sort_order": "DESC", 124 | "states": [] 125 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/purchase_invoice_subcontracting_detail/purchase_invoice_subcontracting_detail.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class PurchaseInvoiceSubcontractingDetail(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/purchase_order_subcontracting_detail/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/doctype/purchase_order_subcontracting_detail/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/purchase_order_subcontracting_detail/purchase_order_subcontracting_detail.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2023-07-06 18:45:16.830715", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "work_order", 10 | "warehouse", 11 | "item_name", 12 | "fg_item", 13 | "fg_item_qty", 14 | "bom", 15 | "stock_uom" 16 | ], 17 | "fields": [ 18 | { 19 | "fieldname": "work_order", 20 | "fieldtype": "Link", 21 | "in_list_view": 1, 22 | "label": "Work Order", 23 | "options": "Work Order", 24 | "reqd": 1 25 | }, 26 | { 27 | "fieldname": "item_name", 28 | "fieldtype": "Data", 29 | "label": "Item Name" 30 | }, 31 | { 32 | "fieldname": "fg_item", 33 | "fieldtype": "Link", 34 | "in_list_view": 1, 35 | "label": "Finished Good", 36 | "options": "Item", 37 | "reqd": 1 38 | }, 39 | { 40 | "columns": 1, 41 | "fieldname": "fg_item_qty", 42 | "fieldtype": "Float", 43 | "in_list_view": 1, 44 | "label": "Finished Good Qty", 45 | "reqd": 1 46 | }, 47 | { 48 | "fieldname": "bom", 49 | "fieldtype": "Link", 50 | "label": "BOM", 51 | "options": "BOM", 52 | "read_only": 1 53 | }, 54 | { 55 | "fieldname": "stock_uom", 56 | "fieldtype": "Link", 57 | "label": "Stock UOM", 58 | "options": "UOM", 59 | "read_only": 1 60 | }, 61 | { 62 | "fieldname": "warehouse", 63 | "fieldtype": "Link", 64 | "in_list_view": 1, 65 | "label": "Warehouse", 66 | "options": "Warehouse", 67 | "reqd": 1 68 | } 69 | ], 70 | "index_web_pages_for_search": 1, 71 | "istable": 1, 72 | "links": [], 73 | "modified": "2023-08-01 08:17:24.132999", 74 | "modified_by": "Administrator", 75 | "module": "Inventory Tools", 76 | "name": "Purchase Order Subcontracting Detail", 77 | "owner": "Administrator", 78 | "permissions": [], 79 | "sort_field": "modified", 80 | "sort_order": "DESC", 81 | "states": [] 82 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/purchase_order_subcontracting_detail/purchase_order_subcontracting_detail.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class PurchaseOrderSubcontractingDetail(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/subcontracting_default/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/doctype/subcontracting_default/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/subcontracting_default/subcontracting_default.json: -------------------------------------------------------------------------------- 1 | { 2 | "actions": [], 3 | "allow_rename": 1, 4 | "creation": "2023-08-01 08:31:15.167625", 5 | "doctype": "DocType", 6 | "editable_grid": 1, 7 | "engine": "InnoDB", 8 | "field_order": [ 9 | "company", 10 | "wip_warehouse", 11 | "return_warehouse" 12 | ], 13 | "fields": [ 14 | { 15 | "fieldname": "company", 16 | "fieldtype": "Link", 17 | "in_list_view": 1, 18 | "label": "Company", 19 | "options": "Company", 20 | "reqd": 1 21 | }, 22 | { 23 | "fieldname": "wip_warehouse", 24 | "fieldtype": "Link", 25 | "in_list_view": 1, 26 | "label": "Work In Process Warehouse", 27 | "options": "Warehouse", 28 | "reqd": 1 29 | }, 30 | { 31 | "fieldname": "return_warehouse", 32 | "fieldtype": "Link", 33 | "in_list_view": 1, 34 | "label": "Return Warehouse", 35 | "options": "Warehouse", 36 | "reqd": 1 37 | } 38 | ], 39 | "index_web_pages_for_search": 1, 40 | "istable": 1, 41 | "links": [], 42 | "modified": "2023-08-01 10:55:44.125187", 43 | "modified_by": "Administrator", 44 | "module": "Inventory Tools", 45 | "name": "Subcontracting Default", 46 | "owner": "Administrator", 47 | "permissions": [], 48 | "sort_field": "modified", 49 | "sort_order": "DESC", 50 | "states": [] 51 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/doctype/subcontracting_default/subcontracting_default.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and contributors 2 | # For license information, please see license.txt 3 | 4 | # import frappe 5 | from frappe.model.document import Document 6 | 7 | 8 | class SubcontractingDefault(Document): 9 | pass 10 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/job_card.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from erpnext.manufacturing.doctype.job_card.job_card import JobCard 3 | from frappe import _, bold 4 | from frappe.utils import get_link_to_form 5 | 6 | from inventory_tools.inventory_tools.overrides.work_order import get_allowance_percentage 7 | 8 | 9 | class InventoryToolsJobCard(JobCard): 10 | def validate_job_card(self): 11 | """ 12 | HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 13 | REPO: https://github.com/frappe/erpnext/ 14 | PATH: erpnext/manufacturing/doctype/job_card/job_card.py 15 | METHOD: validate_job_card 16 | """ 17 | 18 | if ( 19 | self.work_order 20 | and frappe.get_cached_value("Work Order", self.work_order, "status") == "Stopped" 21 | ): 22 | frappe.throw( 23 | _("Transaction not allowed against stopped Work Order {0}").format( 24 | get_link_to_form("Work Order", self.work_order) 25 | ) 26 | ) 27 | 28 | if not self.time_logs: 29 | frappe.throw( 30 | _("Time logs are required for {0} {1}").format( 31 | bold("Job Card"), get_link_to_form("Job Card", self.name) 32 | ) 33 | ) 34 | 35 | # don't validate mfg qty so partial consumption can take place 36 | # PATCH: use manufacturing settings overproduction percentage to allow overproduction on Job Card 37 | overproduction_percentage = get_allowance_percentage(self.company, self.bom_no) 38 | allowed_qty = self.for_quantity * (1 + overproduction_percentage / 100) 39 | if self.for_quantity and self.total_completed_qty > allowed_qty: 40 | total_completed_qty = frappe.bold(frappe._("Total Completed Qty")) 41 | qty_to_manufacture = frappe.bold(frappe._("Qty to Manufacture")) 42 | frappe.throw( 43 | frappe._("The {0} ({1}) must be equal to {2} ({3})").format( 44 | total_completed_qty, 45 | frappe.bold(self.total_completed_qty), 46 | qty_to_manufacture, 47 | frappe.bold(self.for_quantity), 48 | ) 49 | ) 50 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/operation.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | 3 | 4 | def validate_alternative_workstation(self, method=None): 5 | if self.workstation: 6 | for row in self.alternative_workstations: 7 | if row.workstation == self.workstation: 8 | frappe.throw(frappe._("Default Workstation should not be selected as alternative workstation")) 9 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/production_plan.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import frappe 4 | from erpnext.manufacturing.doctype.production_plan.production_plan import ProductionPlan 5 | from erpnext.manufacturing.doctype.work_order.work_order import get_default_warehouse 6 | 7 | 8 | class InventoryToolsProductionPlan(ProductionPlan): 9 | @frappe.whitelist() 10 | def make_work_order(self): 11 | """ 12 | HASH: b087fb3d549462ea8c9d1e65e8622e952d4039f6 13 | REPO: https://github.com/frappe/erpnext/ 14 | PATH: erpnext/manufacturing/doctype/production_plan/production_plan.py 15 | METHOD: make_work_order 16 | """ 17 | 18 | wo_list, po_list = [], [] 19 | subcontracted_po = {} 20 | default_warehouses = get_default_warehouse() 21 | 22 | self.make_work_order_for_finished_goods(wo_list, default_warehouses) 23 | self.make_work_order_for_subassembly_items(wo_list, subcontracted_po, default_warehouses) 24 | if frappe.get_value("Inventory Tools Settings", self.company, "create_purchase_orders"): 25 | self.make_subcontracted_purchase_order(subcontracted_po, po_list) 26 | self.show_list_created_message("Work Order", wo_list) 27 | self.show_list_created_message("Purchase Order", po_list) 28 | 29 | def make_work_order_for_subassembly_items(self, wo_list, subcontracted_po, default_warehouses): 30 | """ 31 | HASH: b087fb3d549462ea8c9d1e65e8622e952d4039f6 32 | REPO: https://github.com/frappe/erpnext/ 33 | PATH: erpnext/manufacturing/doctype/production_plan/production_plan.py 34 | METHOD: make_work_order_for_subassembly_items 35 | """ 36 | 37 | for row in self.sub_assembly_items: 38 | if row.type_of_manufacturing == "Subcontract": 39 | subcontracted_po.setdefault(row.supplier, []).append(row) 40 | if not frappe.get_value( 41 | "Inventory Tools Settings", self.company, "enable_work_order_subcontracting" 42 | ): 43 | continue 44 | 45 | if row.type_of_manufacturing == "Material Request": 46 | continue 47 | 48 | work_order_data = { 49 | "wip_warehouse": default_warehouses.get("wip_warehouse"), 50 | "fg_warehouse": default_warehouses.get("fg_warehouse"), 51 | "company": self.get("company"), 52 | } 53 | 54 | self.prepare_data_for_sub_assembly_items(row, work_order_data) 55 | work_order = self.create_work_order(work_order_data) 56 | if work_order: 57 | wo_list.append(work_order) 58 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/purchase_invoice.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and Contributors 2 | # See license.txt 3 | 4 | import datetime 5 | import json 6 | 7 | import frappe 8 | from erpnext.accounts.doctype.purchase_invoice.purchase_invoice import PurchaseInvoice 9 | from frappe import _ 10 | from frappe.utils.data import cint 11 | 12 | 13 | class InventoryToolsPurchaseInvoice(PurchaseInvoice): 14 | def validate_with_previous_doc(self): 15 | """ 16 | HASH: e7432fc60d4b5b82363212ae003cb7d2d4e8f294 17 | REPO: https://github.com/frappe/erpnext/ 18 | PATH: erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py 19 | METHOD: validate_with_previous_doc 20 | """ 21 | config = { 22 | "Purchase Order": { 23 | "ref_dn_field": "purchase_order", 24 | "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], 25 | }, 26 | "Purchase Order Item": { 27 | "ref_dn_field": "po_detail", 28 | "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], 29 | "is_child_table": True, 30 | "allow_duplicate_prev_row_id": True, 31 | }, 32 | "Purchase Receipt": { 33 | "ref_dn_field": "purchase_receipt", 34 | "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], 35 | }, 36 | "Purchase Receipt Item": { 37 | "ref_dn_field": "pr_detail", 38 | "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], 39 | "is_child_table": True, 40 | }, 41 | } 42 | pos = list({r.purchase_order for r in self.items}) 43 | if len(pos) == 1 and frappe.get_value("Purchase Order", pos[0], "multi_company_purchase_order"): 44 | config["Purchase Order"]["compare_fields"] = [["currency", "="]] 45 | 46 | super(PurchaseInvoice, self).validate_with_previous_doc(config) 47 | 48 | if ( 49 | cint(frappe.get_cached_value("Buying Settings", "None", "maintain_same_rate")) 50 | and not self.is_return 51 | and not self.is_internal_supplier 52 | ): 53 | self.validate_rate_with_reference_doc( 54 | [ 55 | ["Purchase Order", "purchase_order", "po_detail"], 56 | ["Purchase Receipt", "purchase_receipt", "pr_detail"], 57 | ] 58 | ) 59 | 60 | def validate(self): 61 | if self.is_work_order_subcontracting_enabled() and self.is_subcontracted: 62 | if not self.supplier_warehouse: 63 | self.supplier_warehouse = fetch_supplier_warehouse(self.company, self.supplier) 64 | self.validate_subcontracting_to_pay_qty() 65 | return super().validate() 66 | 67 | def on_submit(self): 68 | if self.is_work_order_subcontracting_enabled() and self.is_subcontracted: 69 | self.on_submit_save_se_paid_qty() 70 | return super().on_submit() 71 | 72 | def on_cancel(self): 73 | if self.is_work_order_subcontracting_enabled() and self.is_subcontracted: 74 | self.on_cancel_revert_se_paid_qty() 75 | return super().on_cancel() 76 | 77 | def is_work_order_subcontracting_enabled(self): 78 | settings = frappe.get_doc("Inventory Tools Settings", {"company": self.company}) 79 | return bool(settings and settings.enable_work_order_subcontracting) 80 | 81 | def validate_subcontracting_to_pay_qty(self): 82 | # Checks the qty the invoice will cover is not more than the outstanding qty 83 | for subc in self.get("subcontracting"): 84 | if subc.to_pay_qty > (subc.qty - subc.paid_qty): 85 | frappe.throw( 86 | _( 87 | f"The To Pay Qty in Subcontracting Detail row {subc.idx} cannot be more than Total Qty less the already Paid Qty." 88 | ) 89 | ) 90 | 91 | def on_submit_save_se_paid_qty(self): 92 | # Saves the invoiced quantity for the Stock Entry Detail row into paid_qty field 93 | for ste in self.get("subcontracting"): 94 | frappe.db.set_value( 95 | "Stock Entry Detail", ste.se_detail_name, "paid_qty", ste.paid_qty + ste.to_pay_qty 96 | ) 97 | 98 | def on_cancel_revert_se_paid_qty(self): 99 | # Reduces the Stock Entry Detail item's paid_qty by the to_pay_qty amount in the invoice 100 | for ste in self.get("subcontracting"): 101 | cur_paid = frappe.db.get_value("Stock Entry Detail", ste.se_detail_name, "paid_qty") 102 | frappe.db.set_value( 103 | "Stock Entry Detail", ste.se_detail_name, "paid_qty", cur_paid - ste.to_pay_qty 104 | ) 105 | 106 | 107 | @frappe.whitelist() 108 | def get_stock_entries(purchase_orders, from_date=None, to_date=None): 109 | # # Commented code is useful if having PO and attaching WOs to them is enforced 110 | # if isinstance(purchase_orders, str): 111 | # purchase_orders = json.loads(purchase_orders) 112 | 113 | if not from_date: 114 | from_date = datetime.date(1900, 1, 1) 115 | 116 | if not to_date: 117 | to_date = datetime.date(2100, 12, 31) 118 | 119 | # work_orders, fg_items = [], set() 120 | # for po in purchase_orders: 121 | # work_orders.extend( 122 | # frappe.get_all( 123 | # "Purchase Order Subcontracting Detail", 124 | # fields="work_order", 125 | # filters={"parent": po}, 126 | # pluck="work_order" 127 | # ) 128 | # ) 129 | # for item in frappe.get_doc("Purchase Order", po).get("items"): 130 | # fg_items.add(item.get("fg_item")) 131 | 132 | stock_entry = frappe.qb.DocType("Stock Entry") 133 | se_detail = frappe.qb.DocType("Stock Entry Detail") 134 | po_sub = frappe.qb.DocType("Purchase Order Subcontracting Detail") 135 | po = frappe.qb.DocType("Purchase Order") 136 | item = frappe.qb.DocType("Item") 137 | 138 | query = ( 139 | frappe.qb.from_(stock_entry) 140 | .inner_join(se_detail) 141 | .on(stock_entry.name == se_detail.parent) 142 | .left_join(po_sub) 143 | .on(stock_entry.work_order == po_sub.work_order) 144 | .left_join(item) 145 | .on(se_detail.item_code == item.item_code) 146 | .left_join(po) 147 | .on(po_sub.parent == po.name) 148 | .select( 149 | stock_entry.work_order, 150 | (stock_entry.name).as_("stock_entry"), 151 | (se_detail.name).as_("se_detail_name"), 152 | (po_sub.parent).as_("purchase_order"), 153 | se_detail.item_code, 154 | se_detail.item_name, 155 | se_detail.qty, 156 | se_detail.transfer_qty, 157 | se_detail.uom, 158 | se_detail.stock_uom, 159 | se_detail.conversion_factor, 160 | se_detail.valuation_rate, 161 | se_detail.paid_qty, 162 | ) 163 | .where(stock_entry.docstatus == 1) 164 | .where(stock_entry.stock_entry_type == "Manufacture") 165 | .where(stock_entry.posting_date >= from_date) 166 | .where(stock_entry.posting_date <= to_date) 167 | # .where(stock_entry.work_order.isin(work_orders)) 168 | # .where(se_detail.item_code.isin(fg_items)) 169 | .where(se_detail.is_finished_item == 1) 170 | .where(se_detail.paid_qty < se_detail.qty) 171 | .where(item.is_sub_contracted_item == 1) 172 | .where(po.docstatus != 2) 173 | ) 174 | 175 | return frappe.db.sql( 176 | query.get_sql(), 177 | { 178 | "from_date": from_date, 179 | "to_date": to_date, 180 | # "work_orders": work_orders, 181 | # "fg_items": fg_items, 182 | }, 183 | as_dict=1, 184 | ) 185 | 186 | 187 | @frappe.whitelist() 188 | def fetch_supplier_warehouse(company, supplier): 189 | return frappe.db.get_value( 190 | "Subcontracting Default", 191 | {"parent": supplier, "company": company}, 192 | ["return_warehouse"], 193 | ) 194 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/purchase_receipt.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and Contributors 2 | # See license.txt 3 | 4 | import json 5 | 6 | import frappe 7 | from erpnext.stock.doctype.purchase_receipt.purchase_receipt import PurchaseReceipt 8 | from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company 9 | from frappe.utils.data import cint 10 | 11 | 12 | class InventoryToolsPurchaseReceipt(PurchaseReceipt): 13 | def validate_with_previous_doc(self): 14 | """ 15 | HASH: 106c154a16efce956357524309215cd62cc3c3ec 16 | REPO: https://github.com/frappe/erpnext/ 17 | PATH: erpnext/stock/doctype/purchase_receipt/purchase_receipt.py 18 | METHOD: validate_with_previous_doc 19 | """ 20 | config = { 21 | "Purchase Order": { 22 | "ref_dn_field": "purchase_order", 23 | "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], 24 | }, 25 | "Purchase Order Item": { 26 | "ref_dn_field": "purchase_order_item", 27 | "compare_fields": [["project", "="], ["uom", "="], ["item_code", "="]], 28 | "is_child_table": True, 29 | "allow_duplicate_prev_row_id": True, 30 | }, 31 | } 32 | pos = list({r.purchase_order for r in self.items}) 33 | if len(pos) == 1 and frappe.get_value("Purchase Order", pos[0], "multi_company_purchase_order"): 34 | config["Purchase Order"]["compare_fields"] = [["supplier", "="], ["currency", "="]] 35 | super(PurchaseReceipt, self).validate_with_previous_doc(config) 36 | 37 | if ( 38 | cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) 39 | and not self.is_return 40 | and not self.is_internal_supplier 41 | ): 42 | self.validate_rate_with_reference_doc( 43 | [["Purchase Order", "purchase_order", "purchase_order_item"]] 44 | ) 45 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/sales_order.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and Contributors 2 | # See license.txt 3 | import frappe 4 | from erpnext.selling.doctype.sales_order.sales_order import SalesOrder, WarehouseRequired 5 | from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company 6 | from frappe import _ 7 | from frappe.utils import cint 8 | 9 | 10 | class InventoryToolsSalesOrder(SalesOrder): 11 | def validate_with_previous_doc(self): 12 | config = {"Quotation": {"ref_dn_field": "prevdoc_docname", "compare_fields": [["company", "="]]}} 13 | if self.multi_company_sales_order: 14 | config.pop("Quotation") 15 | super(SalesOrder, self).validate_with_previous_doc(config) 16 | 17 | def validate_warehouse(self): 18 | warehouses = list({d.warehouse for d in self.get("items") if getattr(d, "warehouse", None)}) 19 | 20 | target_warehouses = list( 21 | {d.target_warehouse for d in self.get("items") if getattr(d, "target_warehouse", None)} 22 | ) 23 | 24 | warehouses.extend(target_warehouses) 25 | 26 | from_warehouse = list( 27 | {d.from_warehouse for d in self.get("items") if getattr(d, "from_warehouse", None)} 28 | ) 29 | 30 | warehouses.extend(from_warehouse) 31 | 32 | for w in warehouses: 33 | validate_disabled_warehouse(w) 34 | if not self.multi_company_sales_order: 35 | validate_warehouse_company(w, self.company) 36 | 37 | for d in self.get("items"): 38 | if ( 39 | ( 40 | frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 41 | or (self.has_product_bundle(d.item_code) and self.product_bundle_has_stock_item(d.item_code)) 42 | ) 43 | and not d.warehouse 44 | and not cint(d.delivered_by_supplier) 45 | ): 46 | frappe.throw( 47 | _("Delivery warehouse required for stock item {0}").format(d.item_code), WarehouseRequired 48 | ) 49 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/uom.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.desk.reportview import execute 6 | from frappe.desk.search import search_link 7 | 8 | 9 | @frappe.whitelist() 10 | def uom_restricted_query(doctype, txt, searchfield, start, page_len, filters): 11 | company = frappe.defaults.get_defaults().get("company") 12 | if frappe.get_cached_value("Inventory Tools Settings", company, "enforce_uoms"): 13 | return execute( 14 | "UOM Conversion Detail", 15 | filters=filters, 16 | fields=["uom", "conversion_factor"], 17 | limit_start=start, 18 | limit_page_length=page_len, 19 | as_list=True, 20 | ) 21 | if "parent" in filters: 22 | filters.pop("parent") 23 | return execute( 24 | "UOM", 25 | filters=filters, 26 | fields=[searchfield], 27 | limit_start=start, 28 | limit_page_length=page_len, 29 | as_list=True, 30 | ) 31 | 32 | 33 | @frappe.whitelist() 34 | def validate_uom_has_conversion(doc, method=None): 35 | company = doc.company if doc.get("company") else frappe.defaults.get_defaults().get("company") 36 | if not frappe.get_cached_value("Inventory Tools Settings", company, "enforce_uoms"): 37 | return 38 | uom_enforcement = get_uom_enforcement() 39 | if doc.doctype not in uom_enforcement: 40 | return 41 | invalid_data = [] 42 | for form_doctype, config in uom_enforcement.get(doc.doctype).items(): 43 | if doc.doctype == form_doctype: 44 | for field in config: 45 | invalid_data.append(validate_uom_conversion(doc, field)) 46 | else: 47 | for child_table_field, fields in config.items(): 48 | for row in doc.get(child_table_field): 49 | for field in fields: 50 | invalid_data.append(validate_uom_conversion(row, field)) 51 | 52 | if not any(invalid_data): 53 | return 54 | 55 | error_msg = '' 56 | error_msg += ( 57 | "" 66 | ) 67 | for row in invalid_data: 68 | if not row: 69 | continue 70 | error_msg += f"" 71 | error_msg += "
" 58 | + frappe._("Row") 59 | + "" 60 | + frappe._("Item") 61 | + "" 62 | + frappe._("Invalid UOM") 63 | + "" 64 | + frappe._("Valid UOMs") 65 | + "
{row.index} {row.item_code}:{row.item_name} {row.invalid_uom} {row.valid_uoms}
" 72 | frappe.msgprint( 73 | title=frappe._("This Document contains invalid UOMs"), 74 | msg=error_msg, 75 | indicator="red", 76 | raise_exception=True, 77 | ) 78 | 79 | 80 | def validate_uom_conversion(doc, field): 81 | if not doc.get(field): 82 | return 83 | if doc.doctype == "Item": 84 | valid_uoms = [u.get("uom") for u in doc.uoms] 85 | else: 86 | valid_uoms = [ 87 | u["uom"] 88 | for u in frappe.get_all("UOM Conversion Detail", {"parent": doc.get("item_code")}, "uom") 89 | ] 90 | if not valid_uoms: 91 | return 92 | item_name = doc.item_code 93 | if hasattr(doc, "item_name"): 94 | item_name = doc.item_name 95 | if doc.get(field) not in valid_uoms: 96 | return frappe._dict( 97 | { 98 | "index": f"{frappe._('Row')} {doc.idx}" if doc.idx else doc.name, 99 | "item_code": doc.item_code, 100 | "item_name": item_name, 101 | "valid_uoms": (", ").join(valid_uoms), 102 | "invalid_uom": doc.get(field), 103 | } 104 | ) 105 | 106 | 107 | @frappe.whitelist() 108 | def duplicate_weight_to_uom_conversion(doc, method=None): 109 | if not (doc.weight_per_unit and doc.weight_uom): 110 | return 111 | if len(list(filter(lambda x: x.uom == doc.weight_uom, doc.uoms))) == 1: 112 | return 113 | 114 | doc.append( 115 | "uoms", 116 | { 117 | "uom": doc.weight_uom, 118 | "conversion_factor": doc.weight_per_unit, 119 | }, 120 | ) 121 | 122 | 123 | @frappe.whitelist() 124 | def get_uom_enforcement(): 125 | return frappe.get_hooks("inventory_tools_uom_enforcement") 126 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/warehouse.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2023, AgriTheory and contributors 2 | # For license information, please see license.txt 3 | 4 | import frappe 5 | from frappe.desk.reportview import get_filters_cond, get_match_cond 6 | from frappe.desk.search import search_link 7 | 8 | 9 | @frappe.whitelist() 10 | def update_warehouse_path(doc, method=None) -> None: 11 | if not frappe.db.exists("Inventory Tools Settings", doc.company): 12 | return 13 | warehouse_path = frappe.db.get_value( 14 | "Inventory Tools Settings", doc.company, "update_warehouse_path" 15 | ) 16 | if not warehouse_path: 17 | return 18 | 19 | def get_parents(doc): 20 | parents = [doc.warehouse_name] 21 | parent = doc.parent_warehouse 22 | while parent: 23 | parent_name = frappe.get_value("Warehouse", parent, "warehouse_name") 24 | if parent_name != "All Warehouses": 25 | parents.append(parent_name) 26 | parent = frappe.get_value("Warehouse", parent, "parent_warehouse") 27 | else: 28 | break 29 | return parents 30 | 31 | def _update_warehouse_path(doc): 32 | parents = get_parents(doc) 33 | if parents: 34 | if len(parents) > 1: 35 | if parents[1] in parents[0]: 36 | parents[0] = parents[0].replace(parents[1], "") 37 | parents[0] = parents[0].replace(" - ", "") 38 | return " \u21D2 ".join(parents[::-1]) 39 | else: 40 | return "" 41 | 42 | doc.warehouse_path = _update_warehouse_path(doc) 43 | 44 | 45 | @frappe.whitelist() 46 | def warehouse_query(doctype, txt, searchfield, start, page_len, filters): 47 | """ 48 | HASH: 4d34b1ead73baf4c5430a2ecbe44b9e8468d7626 49 | REPO: https://github.com/frappe/erpnext/ 50 | PATH: erpnext/controllers/queries.py 51 | METHOD: warehouse_query 52 | """ 53 | 54 | company = frappe.defaults.get_defaults().get("company") 55 | if not company: 56 | return search_link(doctype, txt, searchfield, start, page_len, filters) 57 | if not frappe.db.exists("Inventory Tools Settings", company) and frappe.db.get_value( 58 | "Inventory Tools Settings", company, "update_warehouse_path" 59 | ): 60 | return search_link(doctype, txt, searchfield, start, page_len, filters) 61 | else: 62 | doctype = "Warehouse" 63 | conditions = [] 64 | searchfields = frappe.get_meta(doctype).get_search_fields() 65 | searchfields.remove("name") 66 | searchfields = ["name"] + searchfields 67 | 68 | return frappe.db.sql( 69 | f"""SELECT {', '.join(searchfields)} 70 | FROM `tabWarehouse` 71 | WHERE `tabWarehouse`.`{searchfield}` like %(txt)s 72 | {get_filters_cond(doctype, filters, conditions).replace("%", "%%")} 73 | {get_match_cond(doctype).replace("%", "%%")} 74 | ORDER BY 75 | IF(LOCATE(%(_txt)s, name), LOCATE(%(_txt)s, name), 99999), 76 | idx DESC, name 77 | LIMIT %(start)s, %(page_len)s""", 78 | { 79 | "txt": "%" + txt + "%", 80 | "_txt": txt.replace("%", ""), 81 | "start": start or 0, 82 | "page_len": page_len or 20, 83 | }, 84 | ) 85 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/overrides/workstation.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.desk.reportview import execute 3 | from frappe.desk.search import search_link 4 | 5 | """ 6 | This function fetch workstation of the document operation. 7 | In Operation you can select multiple workstations in Alternative Workstation field. 8 | In the Work Order, Operation table, and Jobcard, there exists an operation field. 9 | When selecting an operation, this function is responsible for fetching the workstations 10 | both from the Alternative Workstation and the default workstation. 11 | 12 | Example : Operation : Cool Pie Op 13 | Default Workstation: Cooling Racks Station 14 | Alternative Workstation: 15 | ````````````````````````````````````````````````````` 16 | : Cooling Station , Refrigerator Station , : 17 | : : 18 | : : 19 | `````````````````````````````````````````````````````` 20 | In work order and job card when you select operation Cool Pie Op then you find below workstation in workstation field 21 | : Cooling Station : 22 | : Refrigerator Station : 23 | : Cooling Racks Station : 24 | """ 25 | 26 | 27 | @frappe.whitelist() 28 | @frappe.read_only() 29 | @frappe.validate_and_sanitize_search_inputs 30 | def get_alternative_workstations(doctype, txt, searchfield, start, page_len, filters): 31 | company = filters.get("company") or frappe.defaults.get_defaults().get("company") 32 | if not frappe.get_cached_value( 33 | "Inventory Tools Settings", company, "allow_alternative_workstations" 34 | ): 35 | filters.pop("operation") if "operation" in filters else True 36 | filters.pop("company") if "company" in filters else True 37 | return execute( 38 | "Workstation", 39 | filters=filters, 40 | fields=[searchfield], 41 | limit_start=start, 42 | limit_page_length=page_len, 43 | as_list=True, 44 | ) 45 | 46 | operation = filters.get("operation") 47 | if not operation: 48 | frappe.throw("Please select a Operation first.") 49 | 50 | searchfields = list(reversed(frappe.get_meta(doctype).get_search_fields())) 51 | select = ",\n".join([f"`tabWorkstation`.{field}" for field in searchfields]) 52 | search_text = "AND `tabAlternative Workstation`.workstation LIKE %(txt)s" if txt else "" 53 | 54 | workstation = frappe.db.sql( 55 | f""" 56 | SELECT DISTINCT {select} 57 | FROM `tabOperation`, `tabWorkstation`, `tabAlternative Workstation` 58 | WHERE `tabWorkstation`.name = `tabAlternative Workstation`.workstation 59 | AND `tabAlternative Workstation`.parent = %(operation)s 60 | {search_text} 61 | """, 62 | {"operation": operation, "txt": f"%{txt}%"}, 63 | as_list=True, 64 | ) 65 | 66 | default_workstation_name = frappe.db.get_value("Operation", operation, "workstation") 67 | default_workstation_fields = frappe.db.get_values( 68 | "Workstation", default_workstation_name, searchfields, as_dict=True 69 | ) 70 | if default_workstation_name not in [row[0] for row in workstation]: 71 | _default = tuple( 72 | [ 73 | default_workstation_fields[0].name, 74 | f"{frappe.bold('Default')} - {','.join([v for k, v in default_workstation_fields[0].items() if k != 'name'])}", 75 | ] 76 | ) 77 | workstation.insert(0, _default) 78 | return workstation 79 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/report/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/manufacturing_capacity/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/report/manufacturing_capacity/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/manufacturing_capacity/manufacturing_capacity.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, AgriTheory and contributors 2 | // For license information, please see license.txt 3 | /* eslint-disable */ 4 | 5 | frappe.query_reports['Manufacturing Capacity'] = { 6 | filters: [ 7 | { 8 | fieldname: 'bom', 9 | label: __('BOM'), 10 | fieldtype: 'Link', 11 | options: 'BOM', 12 | reqd: 1, 13 | }, 14 | { 15 | fieldname: 'warehouse', 16 | label: __('Warehouse'), 17 | fieldtype: 'Link', 18 | options: 'Warehouse', 19 | reqd: 1, 20 | }, 21 | ], 22 | formatter: function (value, row, column, data, default_formatter) { 23 | value = default_formatter(value, row, column, data) 24 | 25 | if (data && data.is_selected_bom) { 26 | value = value.bold() 27 | } 28 | return value 29 | }, 30 | } 31 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/manufacturing_capacity/manufacturing_capacity.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "columns": [], 4 | "creation": "2024-02-16 12:17:16.951700", 5 | "disable_prepared_report": 0, 6 | "disabled": 0, 7 | "docstatus": 0, 8 | "doctype": "Report", 9 | "filters": [], 10 | "idx": 0, 11 | "is_standard": "Yes", 12 | "modified": "2024-02-16 12:17:16.951700", 13 | "modified_by": "Administrator", 14 | "module": "Inventory Tools", 15 | "name": "Manufacturing Capacity", 16 | "owner": "Administrator", 17 | "prepared_report": 0, 18 | "query": "", 19 | "ref_doctype": "BOM", 20 | "report_name": "Manufacturing Capacity", 21 | "report_type": "Script Report", 22 | "roles": [ 23 | { 24 | "role": "Manufacturing Manager" 25 | }, 26 | { 27 | "role": "Manufacturing User" 28 | } 29 | ] 30 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/material_demand/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/report/material_demand/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/material_demand/material_demand.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "columns": [], 4 | "creation": "2023-05-24 15:34:18.140339", 5 | "disable_prepared_report": 0, 6 | "disabled": 0, 7 | "docstatus": 0, 8 | "doctype": "Report", 9 | "filters": [], 10 | "idx": 0, 11 | "is_standard": "Yes", 12 | "modified": "2023-05-24 15:34:18.140339", 13 | "modified_by": "Administrator", 14 | "module": "Inventory Tools", 15 | "name": "Material Demand", 16 | "owner": "Administrator", 17 | "prepared_report": 0, 18 | "ref_doctype": "Material Request", 19 | "report_name": "Material Demand", 20 | "report_type": "Script Report", 21 | "roles": [ 22 | { 23 | "role": "Purchase Manager" 24 | }, 25 | { 26 | "role": "Stock Manager" 27 | }, 28 | { 29 | "role": "Stock User" 30 | }, 31 | { 32 | "role": "Purchase User" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/quotation_demand/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/inventory_tools/report/quotation_demand/__init__.py -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/quotation_demand/quotation_demand.js: -------------------------------------------------------------------------------- 1 | // Copyright (c) 2024, AgriTheory and contributors 2 | // For license information, please see license.txt 3 | /* eslint-disable */ 4 | 5 | frappe.query_reports['Quotation Demand'] = { 6 | filters: [ 7 | { 8 | fieldname: 'company', 9 | label: __('Company'), 10 | fieldtype: 'Link', 11 | options: 'Company', 12 | }, 13 | { 14 | fieldname: 'start_date', 15 | label: __('Start Date'), 16 | fieldtype: 'Date', 17 | }, 18 | { 19 | fieldname: 'end_date', 20 | label: __('End Date'), 21 | fieldtype: 'Date', 22 | default: moment(), 23 | }, 24 | ], 25 | on_report_render: reportview => { 26 | reportview.datatable.options.columns[9].editable = true 27 | reportview.render_datatable() 28 | // these don't seem to be working 29 | $(".btn-default:contains('Create Card')").addClass('hidden') 30 | $(".btn-default:contains('Set Chart')").addClass('hidden') 31 | }, 32 | get_datatable_options(options) { 33 | return Object.assign(options, { 34 | treeView: true, 35 | checkedRowStatus: false, 36 | checkboxColumn: true, 37 | events: { 38 | onCheckRow: row => { 39 | update_selection(row) 40 | }, 41 | }, 42 | }) 43 | }, 44 | onload: reportview => { 45 | manage_buttons(reportview) 46 | }, 47 | refresh: reportview => { 48 | manage_buttons(reportview) 49 | }, 50 | } 51 | 52 | function manage_buttons(reportview) { 53 | reportview.page.add_inner_button( 54 | __('Create SO(s)'), 55 | function () { 56 | create() 57 | }, 58 | __('Create') 59 | ) 60 | } 61 | 62 | function update_selection(row) { 63 | if (row !== undefined && !row[7].content) { 64 | const toggle = frappe.query_report.datatable.rowmanager.checkMap[row[0].rowIndex] 65 | select_all_customer_items(row, toggle).then(() => { 66 | update_selected_qty() 67 | }) 68 | } else { 69 | update_selected_qty() 70 | } 71 | } 72 | 73 | function update_selected_qty() { 74 | // iterate all rows for selected items 75 | let item_map = {} 76 | frappe.query_report.datatable.datamanager.data.forEach((customer_row, index) => { 77 | if (frappe.query_report.datatable.rowmanager.checkMap[index]) { 78 | console.log(customer_row) 79 | if (customer_row.item_code && !item_map[customer_row.item_code]) { 80 | item_map[customer_row.item_code] = customer_row.qty 81 | } else if (customer_row.item_code && item_map[customer_row.item_code]) { 82 | item_map[customer_row.item_code] += customer_row.qty 83 | } 84 | } 85 | }) 86 | frappe.query_report.datatable.datamanager.data.forEach((customer_row, index) => { 87 | if (customer_row.item_code in item_map) { 88 | let rate = Number(String(customer_row.rate).replace(/[^0-9\.-]+/g, '')) 89 | let total_selected = item_map[customer_row.item_code] 90 | let selected_price = item_map[customer_row.item_code] * (rate || 0) 91 | selected_price = format_currency(selected_price, customer_row.currency, 2) 92 | frappe.query_report.datatable.cellmanager.updateCell(9, index, total_selected, true) 93 | frappe.query_report.datatable.cellmanager.updateCell(12, index, selected_price, true) 94 | } else { 95 | if (customer_row.quotation) { 96 | // don't update indent 0 rows 97 | let rate = Number(String(customer_row.rate).replace(/[^0-9\.-]+/g, '')) 98 | selected_price = format_currency(rate, customer_row.currency, 2) 99 | frappe.query_report.datatable.cellmanager.updateCell(9, index, '', true) 100 | frappe.query_report.datatable.cellmanager.updateCell(12, index, selected_price, true) 101 | } 102 | } 103 | }) 104 | } 105 | 106 | async function select_all_customer_items(row, toggle) { 107 | return new Promise(resolve => { 108 | if (frappe.query_report.datatable.datamanager._filteredRows) { 109 | frappe.query_report.datatable.datamanager._filteredRows.forEach(f => { 110 | if (f[2].content === row[1].content) { 111 | frappe.query_report.datatable.rowmanager.checkMap.splice(row[0].rowIndex, 0, toggle ? 1 : 0) 112 | $(row[0].content).find('input').check = toggle 113 | } else { 114 | frappe.query_report.datatable.rowmanager.checkMap.splice(f[0].rowIndex, 0, 0) 115 | } 116 | }) 117 | } else { 118 | frappe.query_report.datatable.datamanager.rows.forEach(f => { 119 | if (f[2].content === row[2].content) { 120 | frappe.query_report.datatable.rowmanager.checkMap.splice(row[0].rowIndex, 0, toggle ? 1 : 0) 121 | let input = $(frappe.query_report.datatable.rowmanager.getRow$(f[0].rowIndex)).find('input') 122 | if (input[0]) { 123 | input[0].checked = toggle 124 | } 125 | } else { 126 | frappe.query_report.datatable.rowmanager.checkMap.splice(f[0].rowIndex, 0, 0) 127 | } 128 | }) 129 | } 130 | resolve() 131 | }) 132 | } 133 | 134 | async function create() { 135 | let filters = frappe.query_report.get_filter_values() 136 | if (!filters.company) { 137 | company = await select_company() 138 | } else { 139 | company = filters.company 140 | } 141 | let selected_rows = frappe.query_report.datatable.rowmanager.getCheckedRows() 142 | let selected_items = frappe.query_report.datatable.datamanager.data.filter((row, index) => { 143 | return selected_rows.includes(String(index)) && row.indent == 1 ? row : false 144 | }) 145 | 146 | // Update split_qty with the edited value 147 | let selected_raw_rows = frappe.query_report.datatable.datamanager.rows.filter((row, index) => { 148 | return selected_rows.includes(String(index)) && row[0]['indent'] == 1 ? row : false 149 | }) 150 | for (let i = 0; i < selected_items.length; i++) { 151 | selected_items[i]['split_qty'] = selected_raw_rows[i][11].content 152 | } 153 | 154 | if (!selected_items.length) { 155 | frappe.show_alert({ message: __('Please select one or more rows.'), seconds: 5, indicator: 'red' }) 156 | } else { 157 | await frappe 158 | .xcall('inventory_tools.inventory_tools.report.quotation_demand.quotation_demand.create', { 159 | company: company, 160 | filters: filters, 161 | rows: selected_items, 162 | }) 163 | .then(r => {}) 164 | } 165 | } 166 | 167 | async function select_company() { 168 | return new Promise(resolve => { 169 | let dialog = new frappe.ui.Dialog({ 170 | title: __('Select a Company'), 171 | fields: [ 172 | { 173 | fieldtype: 'Link', 174 | fieldname: 'company', 175 | label: 'Company', 176 | options: 'Company', 177 | reqd: 1, 178 | }, 179 | ], 180 | primary_action: () => { 181 | let values = dialog.get_values() 182 | dialog.hide() 183 | return resolve(values.company) 184 | }, 185 | primary_action_label: __('Select'), 186 | }) 187 | dialog.show() 188 | dialog.get_close_btn() 189 | }) 190 | } 191 | -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/quotation_demand/quotation_demand.json: -------------------------------------------------------------------------------- 1 | { 2 | "add_total_row": 0, 3 | "columns": [], 4 | "creation": "2024-04-11 07:05:15.429002", 5 | "disable_prepared_report": 0, 6 | "disabled": 0, 7 | "docstatus": 0, 8 | "doctype": "Report", 9 | "filters": [], 10 | "idx": 0, 11 | "is_standard": "Yes", 12 | "modified": "2024-04-11 07:05:15.429002", 13 | "modified_by": "Administrator", 14 | "module": "Inventory Tools", 15 | "name": "Quotation Demand", 16 | "owner": "Administrator", 17 | "prepared_report": 0, 18 | "ref_doctype": "Quotation", 19 | "report_name": "Quotation Demand", 20 | "report_type": "Script Report", 21 | "roles": [ 22 | { 23 | "role": "Sales User" 24 | }, 25 | { 26 | "role": "Sales Manager" 27 | }, 28 | { 29 | "role": "Maintenance Manager" 30 | }, 31 | { 32 | "role": "Maintenance User" 33 | } 34 | ] 35 | } -------------------------------------------------------------------------------- /inventory_tools/inventory_tools/report/quotation_demand/quotation_demand.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2024, AgriTheory and contributors 2 | # For license information, please see license.txt 3 | 4 | import json 5 | from itertools import groupby 6 | 7 | import frappe 8 | from frappe.query_builder import DocType 9 | from frappe.utils.data import fmt_money 10 | 11 | 12 | def execute(filters=None): 13 | if (filters.start_date and filters.end_date) and (filters.start_date > filters.end_date): 14 | frappe.throw(frappe._("Start date cannot be before end date")) 15 | return get_columns(), get_data(filters) 16 | 17 | 18 | def get_data(filters): 19 | Quotation = DocType("Quotation") 20 | QuotationItem = DocType("Quotation Item") 21 | query = ( 22 | frappe.qb.from_(Quotation) 23 | .inner_join(QuotationItem) 24 | .on(Quotation.name == QuotationItem.parent) 25 | .select( 26 | QuotationItem.name.as_("quotation_item"), 27 | Quotation.name.as_("quotation"), 28 | Quotation.company, 29 | Quotation.currency, 30 | Quotation.party_name.as_("customer"), 31 | Quotation.transaction_date, 32 | QuotationItem.item_code, 33 | QuotationItem.item_name, 34 | QuotationItem.qty, 35 | QuotationItem.uom, 36 | QuotationItem.warehouse, 37 | QuotationItem.rate, 38 | ) 39 | .where(Quotation.docstatus < 2) 40 | .where(Quotation.quotation_to == "Customer") 41 | .where( 42 | Quotation.transaction_date[filters.start_date or "1900-01-01" : filters.en_date or "2100-12-31"] 43 | ) 44 | .orderby(Quotation.party_name, Quotation.name, QuotationItem.item_name) 45 | ) 46 | 47 | if filters.company: 48 | query = query.where(Quotation.company == filters.company) 49 | 50 | data = query.run(as_dict=1) 51 | 52 | output = [] 53 | for customer, _rows in groupby(data, lambda x: x.get("customer")): 54 | rows = list(_rows) 55 | output.append({"customer": customer, "indent": 0}) 56 | for r in rows: 57 | r.split_qty = r["qty"] 58 | r.price = fmt_money(r.get("rate"), 2, r.get("currency")).replace(" ", "") 59 | r.draft_so = frappe.db.get_value( 60 | "Sales Order Item", 61 | {"quotation_item": r.quotation_item, "docstatus": 0}, 62 | "sum(qty) as qty", 63 | ) 64 | r.draft_so = f'{r.draft_so}' if r.draft_so else None 65 | output.append({**r, "indent": 1}) 66 | return output 67 | 68 | 69 | def get_columns(): 70 | hide_company = True if len(frappe.get_all("Company")) == 1 else False 71 | return [ 72 | { 73 | "label": "Customer", 74 | "fieldname": "customer", 75 | "fieldtype": "Link", 76 | "options": "Customer", 77 | "width": "250px", 78 | }, 79 | { 80 | "fieldname": "quotation", 81 | "fieldtype": "Link", 82 | "options": "Quotation", 83 | "label": "Quotation", 84 | "width": "170px", 85 | }, 86 | { 87 | "fieldname": "company", 88 | "label": "Company", 89 | "fieldtype": "Data", 90 | "width": "200px", 91 | "hidden": hide_company, 92 | }, 93 | { 94 | "fieldname": "transaction_date", 95 | "label": "Date", 96 | "fieldtype": "Date", 97 | "width": "100px", 98 | }, 99 | { 100 | "fieldname": "quotation_item", 101 | "fieldtype": "Data", 102 | "hidden": 1, 103 | }, 104 | { 105 | "fieldname": "warehouse", 106 | "label": "Warehouse", 107 | "fieldtype": "Link", 108 | "options": "Warehouse", 109 | "width": "200px", 110 | }, 111 | { 112 | "fieldname": "item_code", 113 | "label": "Item", 114 | "fieldtype": "Link", 115 | "options": "Item", 116 | "width": "250px", 117 | }, 118 | {"fieldname": "item_name", "fieldtype": "Data", "hidden": 1}, 119 | { 120 | "label": "Draft SOs", 121 | "fieldname": "draft_so", 122 | "fieldtype": "Data", 123 | "width": "90px", 124 | "align": "right", 125 | }, 126 | { 127 | "label": "Total Selected", 128 | "fieldname": "total_selected", 129 | "fieldtype": "Data", 130 | "width": "90px", 131 | "align": "right", 132 | }, 133 | { 134 | "label": "Qty", 135 | "fieldname": "qty", 136 | "fieldtype": "Data", 137 | "width": "50px", 138 | "align": "right", 139 | }, 140 | { 141 | "label": "Split Qty", 142 | "fieldname": "split_qty", 143 | "fieldtype": "Data", 144 | "width": "70px", 145 | "align": "right", 146 | }, 147 | {"fieldname": "currency", "fieldtype": "Link", "options": "Currency", "hidden": 1}, 148 | { 149 | "label": "Price", 150 | "fieldname": "price", 151 | "fieldtype": "Data", 152 | "width": "90px", 153 | "align": "right", 154 | }, 155 | {"fieldname": "rate", "fieldtype": "Data", "hidden": 1}, 156 | ] 157 | 158 | 159 | @frappe.whitelist() 160 | def create(company, filters, rows): 161 | filters = frappe._dict(json.loads(filters)) if isinstance(filters, str) else filters 162 | rows = [frappe._dict(r) for r in json.loads(rows)] if isinstance(rows, str) else rows 163 | if not rows: 164 | return 165 | counter = 0 166 | settings = frappe.get_doc("Inventory Tools Settings", company) 167 | requesting_companies = list({row.company for row in rows}) 168 | if settings.sales_order_aggregation_company == company: 169 | requesting_companies = [company] 170 | 171 | for requesting_company in requesting_companies: 172 | for customer, _rows in groupby(rows, lambda x: x.get("customer")): 173 | rows = list(_rows) 174 | so = frappe.new_doc("Sales Order") 175 | so.transaction_date = rows[0].get("transaction_date") 176 | so.customer = customer 177 | if settings.sales_order_aggregation_company and len(requesting_companies) == 1: 178 | so.multi_company_sales_order = True 179 | so.company = settings.sales_order_aggregation_company 180 | else: 181 | so.company = requesting_company 182 | for row in rows: 183 | if not row.get("item_code"): 184 | continue 185 | 186 | if settings.sales_order_aggregation_company == so.company or so.company == row.company: 187 | if ( 188 | settings.sales_order_aggregation_company == so.company 189 | and settings.aggregated_sales_warehouse 190 | ): 191 | warehouse = settings.aggregated_sales_warehouse 192 | else: 193 | warehouse = frappe.get_value("Quotation Item", row.quotation_item, "warehouse") 194 | 195 | so.append( 196 | "items", 197 | { 198 | "item_code": row.get("item_code"), 199 | "item_name": row.get("item_name"), 200 | "delivery_date": row.get("transaction_date"), 201 | "uom": row.get("uom"), 202 | "qty": row.get("split_qty"), 203 | "rate": row.get("rate"), 204 | "warehouse": warehouse, 205 | "quotation_item": row.get("quotation_item"), 206 | "prevdoc_docname": row.get("quotation"), 207 | }, 208 | ) 209 | 210 | if so.items: 211 | so.save() 212 | counter += 1 213 | 214 | frappe.msgprint(frappe._(f"{counter} Sales Orders created"), alert=True, indicator="green") 215 | -------------------------------------------------------------------------------- /inventory_tools/modules.txt: -------------------------------------------------------------------------------- 1 | Inventory Tools -------------------------------------------------------------------------------- /inventory_tools/patches.txt: -------------------------------------------------------------------------------- 1 | inventory_tools.patches.rename_alternative_workstation # Tyler Matteson 5/13/24 -------------------------------------------------------------------------------- /inventory_tools/patches/rename_alternative_workstation.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from frappe.model.rename_doc import rename_doc 3 | 4 | 5 | def execute(): 6 | if frappe.db.exists("DocType", "Alternative Workstations"): 7 | rename_doc( 8 | "DocType", "Alternative Workstations", "Alternative Workstation", ignore_if_exists=True 9 | ) 10 | 11 | frappe.reload_doc("inventory_tools", "doctype", "alternative_workstation", force=True) 12 | -------------------------------------------------------------------------------- /inventory_tools/public/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/public/.gitkeep -------------------------------------------------------------------------------- /inventory_tools/public/js/custom/item.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on('Item', { 2 | validate: frm => { 3 | if (frm.doc.weight_uom && frm.doc.weight_per_unit == 0) { 4 | frappe.throw(__("Please mention 'Weight Per Unit' along with Weight UOM.")) 5 | } 6 | }, 7 | }) 8 | -------------------------------------------------------------------------------- /inventory_tools/public/js/custom/job_card_custom.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on('Job Card', { 2 | refresh: frm => { 3 | if (frm.doc.operation) { 4 | set_workstation_query(frm) 5 | } 6 | }, 7 | operation: frm => { 8 | set_workstation_query(frm) 9 | }, 10 | }) 11 | 12 | function set_workstation_query(frm) { 13 | frm.set_query('workstation', doc => { 14 | return { 15 | query: 'inventory_tools.inventory_tools.overrides.workstation.get_alternative_workstations', 16 | filters: { 17 | operation: frm.doc.operation, 18 | company: frm.doc.company, 19 | }, 20 | } 21 | }) 22 | } 23 | -------------------------------------------------------------------------------- /inventory_tools/public/js/custom/operation_custom.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on('Operation', { 2 | refresh: frm => { 3 | get_filter_workstations(frm) 4 | }, 5 | workstation: frm => { 6 | get_filter_workstations(frm) 7 | }, 8 | }) 9 | 10 | function get_filter_workstations(frm) { 11 | cur_frm.fields_dict.alternative_workstations.get_query = function (doc) { 12 | return { 13 | filters: { 14 | workstation_name: ['!=', frm.doc.workstation], 15 | }, 16 | } 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /inventory_tools/public/js/custom/purchase_invoice_custom.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on('Purchase Invoice', { 2 | refresh: function (frm) { 3 | show_subcontracting_fields(frm) 4 | frm.remove_custom_button(__('Fetch Stock Entries')) 5 | fetch_stock_entry_dialog(frm) 6 | setup_item_queries(frm) 7 | fetch_supplier_warehouse(frm) 8 | }, 9 | is_subcontracted: function (frm) { 10 | if (frm.doc.is_subcontracted) { 11 | show_subcontracting_fields(frm) 12 | } 13 | }, 14 | company: frm => { 15 | setup_item_queries(frm) 16 | fetch_supplier_warehouse(frm) 17 | }, 18 | supplier: frm => { 19 | fetch_supplier_warehouse(frm) 20 | }, 21 | }) 22 | 23 | function show_subcontracting_fields(frm) { 24 | if (!frm.doc.company || !frm.doc.is_subcontracted) { 25 | hide_field('subcontracting') 26 | return 27 | } 28 | if ( 29 | frappe.boot.inventory_tools_settings && 30 | frappe.boot.inventory_tools_settings[frm.doc.company] && 31 | frappe.boot.inventory_tools_settings[frm.doc.company].enable_work_order_subcontracting 32 | ) { 33 | unhide_field('subcontracting') 34 | hide_field('update_stock') 35 | setTimeout(() => { 36 | frm.remove_custom_button('Purchase Receipt', 'Create') 37 | }, 1000) 38 | } else { 39 | hide_field('subcontracting') 40 | unhide_field('update_stock') 41 | } 42 | toggle_subcontracting_columns(frm) 43 | } 44 | 45 | function add_stock_entry_row(frm, row) { 46 | frm.add_child('subcontracting', { 47 | work_order: row.work_order, 48 | stock_entry: row.stock_entry, 49 | purchase_order: row.purchase_order, 50 | se_detail_name: row.se_detail_name, 51 | item_code: row.item_code, 52 | item_name: row.item_name, 53 | qty: row.qty, 54 | transfer_qty: row.transfer_qty, 55 | uom: row.uom, 56 | stock_uom: row.stock_uom, 57 | conversion_factor: row.conversion_factor, 58 | valuation_rate: row.valuation_rate, 59 | paid_qty: row.paid_qty, 60 | to_pay_qty: row.qty - row.paid_qty, 61 | }) 62 | frm.refresh_field('subcontracting') 63 | } 64 | 65 | function fetch_stock_entry_dialog(frm) { 66 | if (!frm.is_new() && frm.doc.docstatus > 0) { 67 | return 68 | } 69 | let fetch_button = frm.get_field('subcontracting').grid.add_custom_button('Fetch Stock Entries', () => { 70 | let d = new frappe.ui.Dialog({ 71 | title: __('Fetch Stock Entries'), 72 | fields: [ 73 | { 74 | label: __('From'), 75 | fieldname: 'from_date', 76 | fieldtype: 'Date', 77 | }, 78 | { 79 | fieldtype: 'Column Break', 80 | fieldname: 'col_break_1', 81 | }, 82 | { 83 | label: __('To'), 84 | fieldname: 'to_date', 85 | fieldtype: 'Date', 86 | }, 87 | ], 88 | primary_action: function () { 89 | let data = d.get_values() 90 | let po = [] 91 | frm.get_field('items').grid.grid_rows.forEach(item => { 92 | po.push(item.doc.purchase_order) 93 | }) 94 | frappe 95 | .xcall('inventory_tools.inventory_tools.overrides.purchase_invoice.get_stock_entries', { 96 | purchase_orders: po, 97 | from_date: data.from_date, 98 | to_date: data.to_date, 99 | }) 100 | .then(r => { 101 | if (r.length > 0) { 102 | frm.clear_table('subcontracting') 103 | r.forEach(d => { 104 | add_stock_entry_row(frm, d) 105 | }) 106 | } else { 107 | frappe.msgprint(__('No Stock Entries found with the selected filters.')) 108 | } 109 | d.hide() 110 | }) 111 | }, 112 | primary_action_label: __('Get Stock Entries'), 113 | }) 114 | d.show() 115 | }) 116 | $(fetch_button).removeClass('btn-secondary').addClass('btn-primary') 117 | } 118 | 119 | function toggle_subcontracting_columns(frm) { 120 | if (!frm.doc.is_subcontracted) { 121 | // hide columns 122 | frm.get_field('subcontracting').grid.reset_grid() 123 | frm.get_field('subcontracting').grid.visible_columns.forEach((column, index) => { 124 | if (index >= frm.get_field('subcontracting').grid.visible_columns.length - 2) { 125 | column[0].columns = 1 126 | column[1] = 1 127 | } 128 | }) 129 | for (let row of frm.get_field('subcontracting').grid.grid_rows) { 130 | if (row.open_form_button) { 131 | row.open_form_button.parent().remove() 132 | delete row.open_form_button 133 | } 134 | 135 | for (let field in row.columns) { 136 | if (row.columns[field] !== undefined) { 137 | row.columns[field].remove() 138 | } 139 | } 140 | delete row.columns 141 | row.columns = [] 142 | row.render_row() 143 | } 144 | } else { 145 | // show subcontracting fields 146 | frm.get_field('items').grid.reset_grid() 147 | let user_defined_columns = frm.get_field('subcontracting').grid.visible_columns.map(col => { 148 | return col[0] 149 | }) 150 | user_defined_columns.forEach((column, index) => { 151 | if (index > 2) { 152 | // leave first two columns alone 153 | column.columns = 1 154 | } 155 | }) 156 | let paid_qty = frappe.meta.get_docfield(frm.get_field('subcontracting').grid.doctype, 'paid_qty') 157 | paid_qty.in_list_view = 1 158 | user_defined_columns.push(paid_qty) 159 | let to_pay_qty = frappe.meta.get_docfield(frm.get_field('subcontracting').grid.doctype, 'to_pay_qty') 160 | to_pay_qty.in_list_view = 1 161 | user_defined_columns.push(to_pay_qty) 162 | frm.get_field('subcontracting').grid.visible_columns = user_defined_columns.map(col => { 163 | return [col, col.columns] 164 | }) 165 | for (let row of frm.get_field('subcontracting').grid.grid_rows) { 166 | if (row.open_form_button) { 167 | row.open_form_button.parent().remove() 168 | delete row.open_form_button 169 | } 170 | 171 | for (let field in row.columns) { 172 | if (row.columns[field] !== undefined) { 173 | row.columns[field].remove() 174 | } 175 | } 176 | delete row.columns 177 | row.columns = [] 178 | row.render_row() 179 | } 180 | } 181 | frm.get_field('subcontracting').refresh() 182 | } 183 | 184 | function setup_item_queries(frm) { 185 | frm.set_query('item_code', 'items', () => { 186 | if (me.frm.doc.is_subcontracted) { 187 | var filters = { supplier: me.frm.doc.supplier } 188 | if (me.frm.doc.is_old_subcontracting_flow) { 189 | filters['is_sub_contracted_item'] = 1 190 | } else { 191 | if ( 192 | frappe.boot.inventory_tools_settings && 193 | frappe.boot.inventory_tools_settings[frm.doc.company] && 194 | frappe.boot.inventory_tools_settings[frm.doc.company].enable_work_order_subcontracting 195 | ) { 196 | filters['is_stock_item'] = 0 197 | } 198 | } 199 | return { 200 | query: 'erpnext.controllers.queries.item_query', 201 | filters: filters, 202 | } 203 | } else { 204 | return { 205 | query: 'erpnext.controllers.queries.item_query', 206 | filters: { supplier: me.frm.doc.supplier, is_purchase_item: 1, has_variants: 0 }, 207 | } 208 | } 209 | }) 210 | } 211 | 212 | function fetch_supplier_warehouse(frm) { 213 | if (!frm.doc.company || !frm.doc.supplier) { 214 | return 215 | } 216 | frappe 217 | .xcall('inventory_tools.inventory_tools.overrides.purchase_invoice.fetch_supplier_warehouse', { 218 | company: frm.doc.company, 219 | supplier: frm.doc.supplier, 220 | }) 221 | .then(r => { 222 | if (r && r.message) { 223 | frm.set_value('supplier_warehouse', r.message.supplier_warehouse) 224 | } 225 | }) 226 | } 227 | 228 | function setup_supplier_warehouse_query(frm) { 229 | frm.set_query('supplier_warehouse', () => { 230 | return { 231 | filters: { is_group: 0 }, 232 | } 233 | }) 234 | } 235 | -------------------------------------------------------------------------------- /inventory_tools/public/js/custom/stock_entry_custom.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on('Stock Entry', { 2 | on_submit: frm => { 3 | if (frm.doc.docstatus === 1) { 4 | frappe 5 | .call( 6 | 'inventory_tools.inventory_tools.overrides.stock_entry.get_production_item_if_work_orders_for_required_item_exists', 7 | { stock_entry_name: frm.doc.name } 8 | ) 9 | .then(r => { 10 | if (r.message) { 11 | frappe.msgprint({ 12 | title: __(`There are open work orders that ${r.message} is in the BOM`), 13 | message: __(`Do you want to view the work orders that use "${r.message}"?`), 14 | primary_action_label: __('Yes'), 15 | primary_action: { 16 | action(values) { 17 | frappe.set_route('list', 'Work Order', { 18 | 'Work Order Item.item_code': r.message, 19 | status: 'Not Started', 20 | }) 21 | }, 22 | }, 23 | }) 24 | } 25 | }) 26 | } 27 | }, 28 | }) 29 | -------------------------------------------------------------------------------- /inventory_tools/public/js/custom/work_order_custom.js: -------------------------------------------------------------------------------- 1 | frappe.ui.form.on('Work Order', { 2 | setup: frm => { 3 | frm.custom_make_buttons = { 4 | 'New Subcontract PO': 'Create Subcontract PO', 5 | 'Add to Existing PO': 'Add to Existing PO', 6 | } 7 | }, 8 | refresh: frm => { 9 | manage_subcontracting_buttons(frm) 10 | get_workstations(frm) 11 | }, 12 | operation: frm => { 13 | get_workstations(frm) 14 | }, 15 | }) 16 | 17 | function get_workstations(frm) { 18 | frm.set_query('workstation', 'operations', (doc, cdt, cdn) => { 19 | var d = locals[cdt][cdn] 20 | if (!d.operation) { 21 | frappe.throw('Please select a Operation first.') 22 | } 23 | return { 24 | query: 'inventory_tools.inventory_tools.overrides.workstation.get_alternative_workstations', 25 | filters: { 26 | operation: d.operation, 27 | company: frm.doc.company, 28 | }, 29 | } 30 | }) 31 | } 32 | 33 | function manage_subcontracting_buttons(frm) { 34 | if (frm.doc.company) { 35 | frappe.db.get_value('BOM', { name: frm.doc.bom_no }, 'is_subcontracted').then(r => { 36 | if (r && r.message && r.message.is_subcontracted) { 37 | if ( 38 | frm.doc.docstatus && 39 | frappe.boot.inventory_tools_settings && 40 | frappe.boot.inventory_tools_settings[frm.doc.company] && 41 | frappe.boot.inventory_tools_settings[frm.doc.company].enable_work_order_subcontracting 42 | ) { 43 | frm.add_custom_button(__('Create Subcontract PO'), () => make_subcontracting_po(frm), __('Subcontracting')) 44 | frm.add_custom_button(__('Add to Existing PO'), () => add_to_existing_po(frm), __('Subcontracting')) 45 | } 46 | } 47 | }) 48 | } 49 | } 50 | 51 | function make_subcontracting_po(frm) { 52 | let d = new frappe.ui.Dialog({ 53 | title: __('Select Supplier'), 54 | fields: [ 55 | { 56 | label: __('Supplier'), 57 | fieldname: 'supplier', 58 | fieldtype: 'Link', 59 | options: 'Supplier', 60 | reqd: 1, 61 | }, 62 | ], 63 | primary_action: async () => { 64 | let data = await d.get_values() 65 | frappe 66 | .xcall('inventory_tools.inventory_tools.overrides.work_order.make_subcontracted_purchase_order', { 67 | wo_name: frm.doc.name, 68 | supplier: data.supplier, 69 | }) 70 | .then(r => { 71 | d.hide() 72 | }) 73 | }, 74 | primary_action_label: __('Create PO'), 75 | }) 76 | d.show() 77 | } 78 | 79 | function add_to_existing_po(frm) { 80 | let d = new frappe.ui.Dialog({ 81 | title: __('Add to Purchase Order'), 82 | fields: [ 83 | { 84 | label: __('Purchase Order'), 85 | fieldname: 'purchase_order', 86 | fieldtype: 'Link', 87 | options: 'Purchase Order', 88 | reqd: 1, 89 | }, 90 | ], 91 | primary_action: function () { 92 | let data = d.get_values() 93 | frappe 94 | .xcall('inventory_tools.inventory_tools.overrides.work_order.add_to_existing_purchase_order', { 95 | wo_name: frm.doc.name, 96 | po_name: data.purchase_order, 97 | }) 98 | .then(r => { 99 | d.hide() 100 | }) 101 | }, 102 | primary_action_label: __('Add to PO'), 103 | }) 104 | d.show() 105 | d.fields_dict.purchase_order.get_query = () => { 106 | return { 107 | filters: { 108 | is_subcontracted: 1, 109 | docstatus: ['!=', 2], 110 | // TODO: can date filters in dialog be used to filter purchase orders? 111 | }, 112 | } 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /inventory_tools/public/js/inventory_tools.bundle.js: -------------------------------------------------------------------------------- 1 | import './uom_enforcement.js' 2 | import './utils.js' 3 | -------------------------------------------------------------------------------- /inventory_tools/public/js/uom_enforcement.js: -------------------------------------------------------------------------------- 1 | // Copyright(c) 2023, AgriTheory and contributors 2 | // For license information, please see license.txt 3 | 4 | frappe.provide('frappe.ui.form') 5 | 6 | $(document).on('page-change', () => { 7 | page_changed() 8 | }) 9 | 10 | function page_changed() { 11 | frappe.after_ajax(() => { 12 | const route = frappe.get_route() 13 | frappe.call('inventory_tools.inventory_tools.overrides.uom.get_uom_enforcement').then(r => { 14 | frappe.uom_enforcement = r.message 15 | if (route[0] == 'Form' && Object.keys(frappe.uom_enforcement).includes(route[1])) { 16 | frappe.ui.form.on(route[1], { 17 | onload_post_render: frm => { 18 | // onload runs too soon 19 | setup_uom_enforcement(frm) 20 | }, 21 | }) 22 | frappe.ui.form.on(route[1], { 23 | refresh: frm => { 24 | setup_uom_enforcement(frm) 25 | }, 26 | }) 27 | } 28 | }) 29 | }) 30 | } 31 | 32 | function setup_uom_enforcement(frm) { 33 | for (const [form_doctype, config] of Object.entries(frappe.uom_enforcement[frm.doc.doctype])) { 34 | // form setup 35 | if (frm.doc.doctype == form_doctype) { 36 | config.forEach(field => { 37 | frm.set_query(field, (_frm, cdt, cdn) => { 38 | let item_code_field = 'item_code' 39 | if (frm.doc.doctype == 'BOM') { 40 | item_code_field = 'item' 41 | } else if (frm.doc.doctype == 'Job Card') { 42 | item_code_field = 'production_item' 43 | } 44 | if (!frm.doc[item_code_field]) { 45 | return {} 46 | } 47 | return { 48 | query: 'inventory_tools.inventory_tools.overrides.uom.uom_restricted_query', 49 | filters: { parent: frm.doc.item_code }, 50 | } 51 | }) 52 | }) 53 | } else { 54 | // child table setup 55 | for (const [table_field, fields] of Object.entries(config)) { 56 | fields.forEach(field => { 57 | frm.set_query(field, table_field, (_frm, cdt, cdn) => { 58 | if (!locals[cdt][cdn].item_code) { 59 | return {} 60 | } 61 | return { 62 | query: 'inventory_tools.inventory_tools.overrides.uom.uom_restricted_query', 63 | filters: { parent: locals[cdt][cdn].item_code }, 64 | } 65 | }) 66 | }) 67 | } 68 | } 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /inventory_tools/public/js/utils.js: -------------------------------------------------------------------------------- 1 | frappe.provide('frappe.query_report') 2 | 3 | // required to add onload triggers to report view 4 | frappe.views.ReportView.prototype.setup_new_doc_event = function () { 5 | this.$no_result.find('.btn-new-doc').click(() => { 6 | if (this.settings.primary_action) { 7 | this.settings.primary_action() 8 | } else { 9 | this.make_new_doc() 10 | } 11 | }) 12 | } 13 | 14 | // add onload trigger to report view 15 | frappe.views.ReportView.prototype.setup_view = function () { 16 | if (this.report_settings && this.settings.onload) { 17 | this.settings.onload(this) 18 | } 19 | this.setup_columns() 20 | this.setup_new_doc_event() // patched from above 21 | this.page.main.addClass('report-view') 22 | this.page.body[0].style.setProperty('--report-filter-height', this.page.page_form.css('height')) 23 | this.page.body.parent().css('margin-bottom', 'unset') 24 | } 25 | 26 | // add 'on report render' event 27 | frappe.views.QueryReport.prototype.hide_loading_screen = function () { 28 | this.$loading.hide() 29 | if (this.report_settings && this.report_settings.on_report_render && this.data && this.data.length > 0) { 30 | this.report_settings.on_report_render(this) 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /inventory_tools/tests/conftest.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from unittest.mock import MagicMock 3 | 4 | import frappe 5 | import pytest 6 | from frappe.utils import get_bench_path 7 | 8 | 9 | def _get_logger(*args, **kwargs): 10 | from frappe.utils.logger import get_logger 11 | 12 | return get_logger( 13 | module=None, 14 | with_more_info=False, 15 | allow_site=True, 16 | filter=None, 17 | max_size=100_000, 18 | file_count=20, 19 | stream_only=True, 20 | ) 21 | 22 | 23 | @pytest.fixture(scope="module") 24 | def monkeymodule(): 25 | with pytest.MonkeyPatch.context() as mp: 26 | yield mp 27 | 28 | 29 | @pytest.fixture(scope="session", autouse=True) 30 | def db_instance(): 31 | frappe.logger = _get_logger 32 | 33 | currentsite = "test_site" 34 | sites = Path(get_bench_path()) / "sites" 35 | if (sites / "currentsite.txt").is_file(): 36 | currentsite = (sites / "currentsite.txt").read_text() 37 | 38 | frappe.init(site=currentsite, sites_path=sites) 39 | frappe.connect() 40 | frappe.db.commit = MagicMock() 41 | yield frappe.db 42 | -------------------------------------------------------------------------------- /inventory_tools/tests/test_aggregated_purchasing.py: -------------------------------------------------------------------------------- 1 | import json 2 | 3 | import frappe 4 | import pytest 5 | 6 | from inventory_tools.inventory_tools.overrides.purchase_order import ( 7 | make_purchase_invoices, 8 | make_purchase_receipts, 9 | ) 10 | 11 | 12 | @pytest.mark.order(25) 13 | def test_purchase_receipt_aggregation(): 14 | # this should be called immediately after 'test_report_po_with_aggregation_and_no_aggregation_warehouse' 15 | settings = frappe.get_doc("Inventory Tools Settings", "Chelsea Fruit Co") 16 | 17 | pos = [ 18 | frappe.get_doc("Purchase Order", p) for p in frappe.get_all("Purchase Order", pluck="name") 19 | ] 20 | for po in pos: 21 | items = [row.name for row in po.items] 22 | make_purchase_receipts(po.name, frappe.as_json(items)) 23 | 24 | prs = [ 25 | frappe.get_doc("Purchase Receipt", p) for p in frappe.get_all("Purchase Receipt", pluck="name") 26 | ] 27 | for pr in prs: 28 | pr.submit() 29 | for row in pr.items: 30 | mr_company = frappe.get_value("Material Request", row.material_request, "company") 31 | po_company = frappe.get_value("Purchase Order", row.purchase_order, "company") 32 | assert mr_company == pr.company 33 | assert po.company == settings.purchase_order_aggregation_company 34 | 35 | 36 | @pytest.mark.order(26) 37 | def test_purchase_invoice_aggregation(): 38 | settings = frappe.get_doc("Inventory Tools Settings", "Chelsea Fruit Co") 39 | 40 | pos = [ 41 | frappe.get_doc("Purchase Order", p) for p in frappe.get_all("Purchase Order", pluck="name") 42 | ] 43 | for po in pos: 44 | items = [row.name for row in po.items] 45 | make_purchase_invoices(po.name, frappe.as_json(items)) 46 | 47 | pis = [ 48 | frappe.get_doc("Purchase Invoice", p) for p in frappe.get_all("Purchase Invoice", pluck="name") 49 | ] 50 | for pi in pis: 51 | pi.submit() 52 | for row in pi.items: 53 | material_request = frappe.get_value("Purchase Order Item", row.po_detail, "material_request") 54 | mr_company = frappe.get_value("Material Request", material_request, "company") 55 | po_company = frappe.get_value("Purchase Order", row.purchase_order, "company") 56 | assert mr_company == pi.company 57 | assert po.company == settings.purchase_order_aggregation_company 58 | # NOTE: PO company MAY BE different from MR and PI 59 | -------------------------------------------------------------------------------- /inventory_tools/tests/test_alternative_workstation.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | import pytest 3 | 4 | 5 | @pytest.mark.order(45) 6 | def test_alternative_workstation_query(): 7 | # test default settings 8 | frappe.call( 9 | "frappe.desk.search.search_link", 10 | **{ 11 | "doctype": "Workstation", 12 | "txt": "", 13 | "reference_doctype": "Job Card", 14 | }, 15 | ) 16 | assert len(frappe.response.results) == 16 # all workstations 17 | 18 | # test with inventory tools settings 19 | inventory_tools_settings = frappe.get_doc( 20 | "Inventory Tools Settings", frappe.defaults.get_defaults().get("company") 21 | ) 22 | inventory_tools_settings.allow_alternative_workstations = True 23 | inventory_tools_settings.save() 24 | frappe.call( 25 | "frappe.desk.search.search_link", 26 | **{ 27 | "doctype": "Workstation", 28 | "txt": "", 29 | "query": "inventory_tools.inventory_tools.overrides.workstation.get_alternative_workstations", 30 | "filters": {"operation": "Gather Pie Filling Ingredients"}, 31 | "reference_doctype": "Job Card", 32 | }, 33 | ) 34 | assert len(frappe.response.results) == 2 35 | assert frappe.response.results[0].get("value") == "Food Prep Table 1" # default returns first 36 | assert "Default" in frappe.response.results[0].get("description") 37 | assert frappe.response.results[1].get("value") == "Food Prep Table 2" 38 | -------------------------------------------------------------------------------- /inventory_tools/tests/test_manufacturing_capacity.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | import pytest 3 | from erpnext.manufacturing.doctype.work_order.work_order import ( 4 | create_job_card, 5 | make_stock_entry, 6 | make_work_order, 7 | ) 8 | from frappe.exceptions import ValidationError 9 | from frappe.utils import getdate 10 | 11 | from inventory_tools.inventory_tools.report.manufacturing_capacity.manufacturing_capacity import ( 12 | get_total_demand, 13 | ) 14 | 15 | 16 | @pytest.mark.order(10) 17 | def test_total_demand(): 18 | pocketful_bom_no = frappe.get_value( 19 | "BOM", {"item": "Pocketful of Bay", "is_active": 1, "is_default": 1} 20 | ) 21 | tower_bom_no = frappe.get_value( 22 | "BOM", {"item": "Tower of Bay-bel", "is_active": 1, "is_default": 1} 23 | ) 24 | 25 | assert 10 == get_total_demand(pocketful_bom_no) # test data of 10 26 | assert 20 == get_total_demand(tower_bom_no) # test data of 20 27 | 28 | # Create a Sales Order that hasn't generated a Work Order 29 | so = frappe.new_doc("Sales Order") 30 | so.company = frappe.defaults.get_defaults().get("company") 31 | so.transaction_date = getdate() 32 | so.customer = "TransAmerica Bank Cafeteria" 33 | so.order_type = "Sales" 34 | so.currency = "USD" 35 | so.selling_price_list = "Bakery Wholesale" 36 | so.append( 37 | "items", 38 | { 39 | "item_code": "Pocketful of Bay", 40 | "delivery_date": so.transaction_date, 41 | "qty": 5, 42 | "warehouse": "Refrigerated Display - APC", 43 | }, 44 | ) 45 | so.append( 46 | "items", 47 | { 48 | "item_code": "Tower of Bay-bel", 49 | "delivery_date": so.transaction_date, 50 | "qty": 10, 51 | "warehouse": "Refrigerated Display - APC", 52 | }, 53 | ) 54 | so.save() 55 | so.submit() 56 | so.reload() 57 | assert so.items[0].work_order_qty == 0.0 58 | assert so.items[1].work_order_qty == 0.0 59 | assert 15 == get_total_demand(pocketful_bom_no) # test data of 10 + SO of 5 60 | assert 30 == get_total_demand(tower_bom_no) # test data of 20 + SO of 10 61 | 62 | # Create a Material Request for Manufacture 63 | mr = frappe.new_doc("Material Request") 64 | mr.transaction_date = mr.schedule_date = getdate() 65 | mr.material_request_type = "Manufacture" 66 | mr.title = "Tower and Pocketful" 67 | mr.company = frappe.defaults.get_defaults().get("company") 68 | mr.append( 69 | "items", 70 | { 71 | "item_code": "Pocketful of Bay", 72 | "delivery_date": mr.schedule_date, 73 | "qty": 15, 74 | "warehouse": "Refrigerated Display - APC", 75 | }, 76 | ) 77 | mr.append( 78 | "items", 79 | { 80 | "item_code": "Tower of Bay-bel", 81 | "delivery_date": mr.schedule_date, 82 | "qty": 5, 83 | "warehouse": "Refrigerated Display - APC", 84 | }, 85 | ) 86 | mr.save() 87 | mr.submit() 88 | 89 | assert 30 == get_total_demand(pocketful_bom_no) # test data of 10 + SO of 5 + MR of 15 90 | assert 35 == get_total_demand(tower_bom_no) # test data of 20 + SO of 10 + MR of 5 91 | 92 | # cancel MR to test change in demand 93 | mr.cancel() 94 | assert 15 == get_total_demand(pocketful_bom_no) # test data of 10 + SO of 5 95 | assert 30 == get_total_demand(tower_bom_no) # test data of 20 + SO of 10 96 | 97 | # amend and stop to test "Stop" criteria 98 | _mr = frappe.copy_doc(mr) 99 | _mr.amended_from = mr.name 100 | _mr.save() 101 | _mr.submit() 102 | _mr.set_status(update=True, status="Stopped") 103 | 104 | assert _mr.status == "Stopped" 105 | assert 15 == get_total_demand(pocketful_bom_no) # test data of 10 + SO of 5 106 | assert 30 == get_total_demand(tower_bom_no) # test data of 20 + SO of 10 107 | -------------------------------------------------------------------------------- /inventory_tools/tests/test_overproduction.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | import pytest 3 | from erpnext.manufacturing.doctype.work_order.work_order import create_job_card, make_stock_entry 4 | from frappe.exceptions import ValidationError 5 | from frappe.utils import now, strip_html 6 | 7 | from inventory_tools.inventory_tools.overrides.work_order import get_allowance_percentage 8 | 9 | 10 | @pytest.mark.order(30) 11 | def test_get_allowance_percentage(): 12 | work_order = frappe.get_doc("Work Order", {"item_name": "Gooseberry Pie"}) 13 | bom = frappe.get_doc("BOM", work_order.bom_no) 14 | 15 | inventory_tools_settings = frappe.get_doc( 16 | "Inventory Tools Settings", {"company": work_order.company} 17 | ) 18 | # No value set 19 | inventory_tools_settings.overproduction_percentage_for_work_order = 0.00 20 | inventory_tools_settings.save() 21 | bom.overproduction_percentage_for_work_order = 0.0 22 | bom.save() 23 | assert get_allowance_percentage(work_order.company, bom.name) == 0.0 24 | 25 | # Uses value from inventory tools settings 26 | inventory_tools_settings.overproduction_percentage_for_work_order = 50.0 27 | inventory_tools_settings.save() 28 | bom.overproduction_percentage_for_work_order = 0.0 29 | bom.save() 30 | assert get_allowance_percentage(work_order.company, bom.name) == 50.0 31 | 32 | # Uses value from BOM 33 | inventory_tools_settings.overproduction_percentage_for_work_order = 50.0 34 | inventory_tools_settings.save() 35 | bom.overproduction_percentage_for_work_order = 100.0 36 | bom.save() 37 | assert get_allowance_percentage(work_order.company, bom.name) == 100.0 38 | 39 | 40 | @pytest.mark.order(31) 41 | def test_check_if_operations_completed(): 42 | 43 | # BOM with overproduction_percentage_for_work_order configured 44 | work_order = frappe.get_doc("Work Order", {"item_name": "Ambrosia Pie"}) 45 | se = make_stock_entry( 46 | work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=work_order.qty 47 | ) 48 | stock_entry = frappe.new_doc("Stock Entry") 49 | stock_entry.update(se) 50 | 51 | assert stock_entry.check_if_operations_completed() is None 52 | 53 | overproduction_percentage_for_work_order = frappe.db.get_value( 54 | "BOM", stock_entry.bom_no, "overproduction_percentage_for_work_order" 55 | ) 56 | qty = work_order.qty * (1 + overproduction_percentage_for_work_order / 100) 57 | se = make_stock_entry( 58 | work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=qty 59 | ) 60 | stock_entry = frappe.new_doc("Stock Entry") 61 | stock_entry.update(se) 62 | assert stock_entry.check_if_operations_completed() is None 63 | 64 | with pytest.raises(ValidationError) as exc_info: 65 | qty = qty + 1 66 | se = make_stock_entry( 67 | work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=qty 68 | ) 69 | stock_entry = frappe.new_doc("Stock Entry") 70 | stock_entry.update(se) 71 | stock_entry.check_if_operations_completed() 72 | 73 | assert ( 74 | f"is greater than the Work Order's quantity to manufacture of {work_order.qty} plus the overproduction allowance of {overproduction_percentage_for_work_order}%" 75 | in exc_info.value.args[0] 76 | ) 77 | 78 | # BOM without overproduction_percentage_for_work_order configured 79 | work_order = frappe.get_doc("Work Order", {"item_name": "Double Plum Pie"}) 80 | overproduction_percentage_for_work_order = frappe.db.get_value( 81 | "BOM", work_order.bom_no, "overproduction_percentage_for_work_order" 82 | ) 83 | assert overproduction_percentage_for_work_order == 0.0 84 | 85 | overproduction_percentage_for_work_order = frappe.get_value( 86 | "Inventory Tools Settings", work_order.company, "overproduction_percentage_for_work_order" 87 | ) 88 | assert overproduction_percentage_for_work_order != 0.0 89 | 90 | qty = work_order.qty * (1 + overproduction_percentage_for_work_order / 100) 91 | se = make_stock_entry( 92 | work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=qty 93 | ) 94 | stock_entry = frappe.new_doc("Stock Entry") 95 | stock_entry.update(se) 96 | assert stock_entry.check_if_operations_completed() is None 97 | 98 | with pytest.raises(ValidationError) as exc_info: 99 | qty = qty + 1 100 | se = make_stock_entry( 101 | work_order_id=work_order.name, purpose="Material Transfer for Manufacture", qty=qty 102 | ) 103 | stock_entry = frappe.new_doc("Stock Entry") 104 | stock_entry.update(se) 105 | stock_entry.check_if_operations_completed() 106 | 107 | assert ( 108 | f"is greater than the Work Order's quantity to manufacture of {work_order.qty} plus the overproduction allowance of {overproduction_percentage_for_work_order}%" 109 | in exc_info.value.args[0] 110 | ) 111 | 112 | 113 | @pytest.mark.order(32) 114 | def test_validate_finished_goods(): 115 | work_order = frappe.get_doc("Work Order", {"item_name": "Ambrosia Pie"}) 116 | se = make_stock_entry(work_order_id=work_order.name, purpose="Manufacture", qty=work_order.qty) 117 | stock_entry = frappe.new_doc("Stock Entry") 118 | stock_entry.update(se) 119 | assert stock_entry.validate_finished_goods() is None 120 | 121 | with pytest.raises(ValidationError) as exc_info: 122 | stock_entry.fg_completed_qty = work_order.qty * 10 123 | stock_entry.validate_finished_goods() 124 | 125 | assert ( 126 | f"For quantity {work_order.qty * 10} should not be greater than work order quantity {work_order.qty}" 127 | in exc_info.value.args[0] 128 | ) 129 | 130 | 131 | @pytest.mark.order(33) 132 | def test_validate_job_card(): 133 | work_order = frappe.get_doc("Work Order", {"item_name": "Ambrosia Pie"}) 134 | jc = frappe.get_doc( 135 | "Job Card", {"work_order": work_order.name, "operation": work_order.operations[0].operation} 136 | ) 137 | jc.cancel() 138 | job_card = create_job_card(work_order, work_order.operations[0].as_dict(), auto_create=True) 139 | job_card.append( 140 | "time_logs", 141 | { 142 | "from_time": now(), 143 | "to_time": now(), 144 | "completed_qty": work_order.qty, 145 | }, 146 | ) 147 | job_card.save() 148 | assert job_card.validate_job_card() == None 149 | 150 | overproduction_percentage_for_work_order = frappe.db.get_value( 151 | "BOM", work_order.bom_no, "overproduction_percentage_for_work_order" 152 | ) 153 | over_production_qty = work_order.qty * (1 + overproduction_percentage_for_work_order / 100) 154 | job_card.time_logs[0].completed_qty = over_production_qty 155 | job_card.save() 156 | 157 | assert job_card.validate_job_card() == None 158 | 159 | job_card.time_logs[0].completed_qty = over_production_qty + 10 160 | job_card.save() 161 | 162 | with pytest.raises(ValidationError) as exc_info: 163 | job_card.validate_job_card() 164 | 165 | assert ( 166 | f"The Total Completed Qty ({over_production_qty + 10}) must be equal to Qty to Manufacture ({job_card.for_quantity})" 167 | in strip_html(exc_info.value.args[0]) 168 | ) 169 | -------------------------------------------------------------------------------- /inventory_tools/tests/test_quotation_demand_report.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | import pytest 3 | from frappe.utils import flt, getdate 4 | 5 | from inventory_tools.inventory_tools.report.quotation_demand.quotation_demand import ( 6 | execute as execute_quotation_demand, 7 | ) 8 | 9 | 10 | @pytest.mark.order(50) 11 | def test_report_without_aggregation(): 12 | filters = frappe._dict({"end_date": getdate()}) 13 | columns, rows = execute_quotation_demand(filters) 14 | assert len(rows) == 10 15 | assert rows[1].get("customer") == "Almacs Food Group" 16 | 17 | selected_rows = [ 18 | row for row in rows if row.get("customer") == "Almacs Food Group" and row.get("company") 19 | ] 20 | 21 | frappe.call( 22 | "inventory_tools.inventory_tools.report.quotation_demand.quotation_demand.create", 23 | **{ 24 | "company": "Ambrosia Pie Company", 25 | "filters": filters, 26 | "rows": frappe.as_json(selected_rows), 27 | }, 28 | ) 29 | 30 | sos = [ 31 | frappe.get_doc("Sales Order", so) 32 | for so in frappe.get_all("Sales Order", {"docstatus": 0}, pluck="name") 33 | ] 34 | assert "Donwtown Deli" not in [so.get("customer") for so in sos] 35 | assert len(sos) == 2 36 | assert len(list(filter(lambda d: d.company == "Chelsea Fruit Co", sos))) == 1 37 | assert len(list(filter(lambda d: d.company == "Ambrosia Pie Company", sos))) == 1 38 | for so in sos: 39 | if so.company == "Almacs Food Group": 40 | assert so.grand_total == flt(144.84, 2) 41 | elif so.company == "Chelsea Fruit Co": 42 | assert so.grand_total == flt(160.00, 2) 43 | 44 | for item in so.items: 45 | quotation_wh = frappe.get_value("Quotation Item", item.quotation_item, "warehouse") 46 | assert item.warehouse == quotation_wh 47 | frappe.delete_doc("Sales Order", so.name) 48 | 49 | 50 | @pytest.mark.order(51) 51 | def test_report_with_aggregation_and_no_aggregation_warehouse(): 52 | settings = frappe.get_doc("Inventory Tools Settings", "Chelsea Fruit Co") 53 | settings.sales_order_aggregation_company = settings.name 54 | settings.aggregated_sales_warehouse = None 55 | settings.update_warehouse_path = True 56 | settings.save() 57 | 58 | filters = frappe._dict({"end_date": getdate()}) 59 | columns, rows = execute_quotation_demand(filters) 60 | assert len(rows) == 10 61 | assert rows[1].get("customer") == "Almacs Food Group" 62 | 63 | selected_rows = [ 64 | row for row in rows if row.get("customer") == "Almacs Food Group" and row.get("company") 65 | ] 66 | 67 | frappe.call( 68 | "inventory_tools.inventory_tools.report.quotation_demand.quotation_demand.create", 69 | **{ 70 | "company": "Chelsea Fruit Co", 71 | "filters": filters, 72 | "rows": frappe.as_json(selected_rows), 73 | }, 74 | ) 75 | 76 | sos = [ 77 | frappe.get_doc("Sales Order", so) 78 | for so in frappe.get_all("Sales Order", {"docstatus": 0}, pluck="name") 79 | ] 80 | assert len(sos) == 1 81 | so = sos[0] 82 | assert so.customer == "Almacs Food Group" 83 | assert so.company == "Chelsea Fruit Co" 84 | assert so.grand_total == flt(304.84, 2) 85 | for item in so.items: 86 | quotation_wh = frappe.get_value("Quotation Item", item.quotation_item, "warehouse") 87 | assert item.warehouse == quotation_wh 88 | frappe.delete_doc("Sales Order", so.name) 89 | 90 | 91 | @pytest.mark.order(52) 92 | def test_report_with_aggregation_and_aggregation_warehouse(): 93 | settings = frappe.get_doc("Inventory Tools Settings", "Chelsea Fruit Co") 94 | settings.sales_order_aggregation_company = settings.name 95 | settings.aggregated_sales_warehouse = "Stores - CFC" 96 | settings.update_warehouse_path = True 97 | settings.save() 98 | 99 | filters = frappe._dict({"end_date": getdate()}) 100 | columns, rows = execute_quotation_demand(filters) 101 | assert len(rows) == 10 102 | assert rows[1].get("customer") == "Almacs Food Group" 103 | 104 | selected_rows = [ 105 | row for row in rows if row.get("customer") == "Almacs Food Group" and row.get("company") 106 | ] 107 | 108 | frappe.call( 109 | "inventory_tools.inventory_tools.report.quotation_demand.quotation_demand.create", 110 | **{ 111 | "company": "Chelsea Fruit Co", 112 | "filters": filters, 113 | "rows": frappe.as_json(selected_rows), 114 | }, 115 | ) 116 | 117 | sos = [ 118 | frappe.get_doc("Sales Order", so) 119 | for so in frappe.get_all("Sales Order", {"docstatus": 0}, pluck="name") 120 | ] 121 | assert len(sos) == 1 122 | so = sos[0] 123 | assert so.customer == "Almacs Food Group" 124 | assert so.company == "Chelsea Fruit Co" 125 | assert so.grand_total == flt(304.84, 2) 126 | for item in so.items: 127 | assert item.warehouse == "Stores - CFC" 128 | frappe.delete_doc("Sales Order", so.name) 129 | -------------------------------------------------------------------------------- /inventory_tools/tests/test_uom.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | import pytest 3 | from frappe.exceptions import ValidationError 4 | 5 | 6 | @pytest.mark.order(40) 7 | def test_uom_enforcement_validation(): 8 | _so = frappe.get_last_doc("Sales Order") 9 | inventory_tools_settings = frappe.get_doc("Inventory Tools Settings", _so.company) 10 | inventory_tools_settings.enforce_uoms = True 11 | inventory_tools_settings.save() 12 | 13 | so = frappe.copy_doc(_so) 14 | assert so.items[0].uom == "Nos" 15 | so.items[0].uom = "Box" 16 | with pytest.raises(ValidationError) as exc_info: 17 | so.save() 18 | 19 | assert "Invalid UOM" in exc_info.value.args[0] 20 | 21 | 22 | @pytest.mark.order(41) 23 | def test_uom_enforcement_query(): 24 | inventory_tools_settings = frappe.get_doc( 25 | "Inventory Tools Settings", frappe.defaults.get_defaults().get("company") 26 | ) 27 | inventory_tools_settings.enforce_uoms = True 28 | inventory_tools_settings.save() 29 | frappe.call( 30 | "frappe.desk.search.search_link", 31 | **{ 32 | "doctype": "UOM", 33 | "txt": "", 34 | "query": "inventory_tools.inventory_tools.overrides.uom.uom_restricted_query", 35 | "filters": {"parent": "Parchment Paper"}, 36 | "reference_doctype": "Purchase Order Item", 37 | }, 38 | ) 39 | assert len(frappe.response.results) == 2 40 | assert frappe.response.results[0].get("value") == "Nos" 41 | assert frappe.response.results[0].get("description") == "1.0" 42 | assert frappe.response.results[1].get("value") == "Box" 43 | assert frappe.response.results[1].get("description") == "100.0" 44 | -------------------------------------------------------------------------------- /inventory_tools/tests/test_warehouse_path.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | import pytest 3 | 4 | 5 | @pytest.mark.order(1) 6 | def test_warehouse_path(): 7 | """ 8 | In the setup script this feature is turned on 9 | """ 10 | wh = frappe.get_doc("Warehouse", "Bakery Display - APC") 11 | assert wh.warehouse_path == "Finished Goods ⇒ Bakery Display" 12 | wh.parent_warehouse = "All Warehouses - APC" 13 | wh.save() 14 | assert wh.warehouse_path == "Bakery Display" 15 | wh.parent_warehouse = "Baked Goods - APC" 16 | wh.save() 17 | assert wh.warehouse_path == "Finished Goods ⇒ Bakery Display" 18 | -------------------------------------------------------------------------------- /inventory_tools/www/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/inventory_tools/8e2198e395aa92c9790fb439dc5f1f0648c8cffd/inventory_tools/www/__init__.py -------------------------------------------------------------------------------- /inventory_tools/www/bulk-order.html: -------------------------------------------------------------------------------- 1 | {% extends "templates/web.html" %} 2 | 3 | {% block page_content %} 4 |
5 |

Bulk Order

6 |
10 |
11 | 18 |
19 | 20 |
21 | 22 |
23 | 24 | 33 | {% endblock %} 34 | -------------------------------------------------------------------------------- /inventory_tools/www/bulk_order.py: -------------------------------------------------------------------------------- 1 | import frappe 2 | from erpnext.e_commerce.shopping_cart.cart import update_cart 3 | 4 | 5 | def get_context(context): 6 | pass 7 | 8 | 9 | @frappe.whitelist() 10 | def create_quotation(bulk_paste: str): 11 | """ 12 | This function is for bulk orders: 13 | 14 | :param bulk_paste: string that contains item(s) and quantity(ies) 15 | """ 16 | 17 | if bulk_paste: 18 | bulk_paste.strip() 19 | for line_item in bulk_paste.split("\n"): 20 | if not line_item: 21 | continue 22 | item_code, qty = line_item.split("\t") 23 | frappe.enqueue(update_cart, item_code=item_code, qty=qty) 24 | frappe.local.response["type"] = "redirect" 25 | frappe.local.response["location"] = "/cart" 26 | -------------------------------------------------------------------------------- /license.txt: -------------------------------------------------------------------------------- 1 | License: MIT -------------------------------------------------------------------------------- /mypy.ini: -------------------------------------------------------------------------------- 1 | [mypy] 2 | ignore_missing_imports = True 3 | disable_error_code = annotation-unchecked 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "inventory_tools", 3 | "scripts": {}, 4 | "dependencies": { 5 | "onscan.js": "^1.5.2" 6 | }, 7 | "devDependencies": {}, 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/agritheory/invnetory_tools.git" 11 | }, 12 | "publishConfig": { 13 | "access": "restricted" 14 | }, 15 | "private": true, 16 | "release": { 17 | "branches": [ 18 | "version-14" 19 | ] 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "inventory_tools" 3 | version = "14.6.1" 4 | authors = ["AgriTheory "] 5 | description = "Inventory Tools for ERPNext" 6 | readme = "README.md" 7 | 8 | [tool.poetry.dependencies] 9 | python = "^3.10" 10 | 11 | [tool.poetry.group.dev.dependencies] 12 | pytest = "^8.2.2" 13 | pytest-cov = "^5.0.0" 14 | pytest-order = "^1.2.1" 15 | mypy = "^1.10.0" 16 | 17 | [build-system] 18 | requires = ["poetry-core"] 19 | build-backend = "poetry.core.masonry.api" 20 | 21 | [tool.pytest.ini_options] 22 | addopts = "--cov=inventory_tools --cov-report term-missing" 23 | 24 | [tool.codespell] 25 | skip = "CHANGELOG.md" 26 | 27 | [tool.black] 28 | line-length = 99 29 | 30 | [tool.isort] 31 | line_length = 99 32 | multi_line_output = 3 33 | include_trailing_comma = true 34 | force_grid_wrap = 0 35 | use_parentheses = true 36 | ensure_newline_before_comments = true 37 | indent = "\t" 38 | 39 | [tool.semantic_release] 40 | version_toml = ["pyproject.toml:tool.poetry.version"] 41 | 42 | [tool.semantic_release.branches.version] 43 | match = "version-14" 44 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from setuptools import setup 2 | 3 | # TODO: Remove this file when bench >=v5.11.0 is adopted / v15.0.0 is released 4 | name = "inventory_tools" 5 | 6 | setup() 7 | -------------------------------------------------------------------------------- /yarn.lock: -------------------------------------------------------------------------------- 1 | # THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. 2 | # yarn lockfile v1 3 | 4 | 5 | onscan.js@^1.5.2: 6 | version "1.5.2" 7 | resolved "https://registry.yarnpkg.com/onscan.js/-/onscan.js-1.5.2.tgz#14ed636e5f4c3f0a78bacbf9a505dad3140ee341" 8 | integrity sha512-9oGYy2gXYRjvXO9GYqqVca0VuCTAmWhbmX3egBSBP13rXiMNb+dKPJzKFEeECGqPBpf0m40Zoo+GUQ7eCackdw== 9 | --------------------------------------------------------------------------------