├── .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 |  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 |  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 |  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 |  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 |  23 | 24 | You can find the new documents in the Purchase Order listview. 25 | 26 |  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 |  31 | 32 | ### Create Request for Quotation 33 |  34 | 35 | If you select the Create RFQ(s) option, a dialog window will appear to select the Company and Email Template. 36 | 37 |  38 | 39 | You can find the new documents in the Request for Quotation listview and make edits as needed before submitting them. 40 | 41 |  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 |  49 | 50 | The selection process works the same as the other options. 51 | 52 |  53 | 54 | The report displays a banner to notify you of how many of each document were created. 55 | 56 |  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 |  61 | 62 |  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 |  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 |  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 |  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 |  21 | 22 | You can find the new documents in the Sales Order listview. 23 | 24 |  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 |  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 |  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 |  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 |  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 |  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 |  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 |  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 |  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 |  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 |  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 |  48 | 49 |  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 |  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 |  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 = '
" 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} |