├── .editorconfig ├── .flake8 ├── .github ├── helper │ ├── install.sh │ ├── install_dependencies.sh │ └── site_config.json ├── validate_customizations.py └── workflows │ ├── backport.yml │ ├── generate_matrix.yaml │ ├── lint.yaml │ ├── overrides.yaml │ ├── pytest.yaml │ └── release.yaml ├── .gitignore ├── .pre-commit-config.yaml ├── .prettierignore ├── .prettierrc.js ├── CHANGELOG.md ├── MANIFEST.in ├── README.md ├── beam ├── __init__.py ├── beam │ ├── __init__.py │ ├── barcodes.py │ ├── boot.py │ ├── custom │ │ ├── bom_scrap_item.json │ │ ├── item.json │ │ ├── item_barcode.json │ │ ├── stock_entry_detail.json │ │ └── warehouse.json │ ├── doctype │ │ ├── __init__.py │ │ └── handling_unit │ │ │ ├── __init__.py │ │ │ ├── handling_unit.js │ │ │ ├── handling_unit.json │ │ │ ├── handling_unit.py │ │ │ └── test_handling_unit.py │ ├── handling_unit.py │ ├── overrides │ │ ├── stock_entry.py │ │ └── subcontracting_receipt.py │ ├── print_format │ │ ├── __init__.py │ │ ├── handling_unit_6x4_zpl_format │ │ │ ├── __init__.py │ │ │ └── handling_unit_6x4_zpl_format.json │ │ ├── handling_unit_label │ │ │ ├── __init__.py │ │ │ └── handling_unit_label.json │ │ ├── item_barcode │ │ │ ├── __init__.py │ │ │ └── item_barcode.json │ │ ├── labelary_print_preview │ │ │ ├── __init__.py │ │ │ └── labelary_print_preview.json │ │ └── warehouse_barcode │ │ │ ├── __init__.py │ │ │ └── warehouse_barcode.json │ ├── printing.py │ ├── report │ │ ├── __init__.py │ │ └── handling_unit_traceability │ │ │ ├── __init__.py │ │ │ ├── handling_unit_traceability.js │ │ │ ├── handling_unit_traceability.json │ │ │ └── handling_unit_traceability.py │ └── scan │ │ ├── __init__.py │ │ └── config.py ├── customize.py ├── docs │ ├── assets │ │ ├── bom_scrap_item.png │ │ ├── form_view_delivery_note.png │ │ ├── handling_unit_list.png │ │ ├── hu_trace_filters.png │ │ ├── hu_trace_report_output.png │ │ ├── listview_wh_navigation.png │ │ ├── network_printer_settings.png │ │ ├── print_hu.png │ │ ├── recombine.png │ │ ├── select_printer_dialog.png │ │ ├── stock_ledger_after_receipt.png │ │ ├── stock_ledger_after_sale.png │ │ ├── testing.png │ │ └── warehouse_barcodes.png │ ├── form.md │ ├── generate_matrix.py │ ├── handling_unit.md │ ├── hooks.md │ ├── hu_traceability_report.md │ ├── index.md │ ├── listview.md │ ├── matrix.md │ ├── print_server.md │ ├── testing.md │ └── zebra_printing.md ├── hooks.py ├── install.py ├── modules.txt ├── patches.txt ├── public │ ├── .gitkeep │ └── js │ │ ├── beam.bundle.js │ │ ├── example_custom_callback.js │ │ ├── print │ │ └── print.js │ │ ├── scan │ │ └── scan.js │ │ └── stock_entry_custom.js ├── tests │ ├── conftest.py │ ├── fixtures.py │ ├── setup.py │ ├── test_handling_unit.py │ ├── test_hooks_override.py │ └── test_scan.py └── www │ └── __init__.py ├── cups ├── .env.example ├── README.md ├── caddy │ ├── Caddyfile.localhost │ ├── Caddyfile.not_ssl │ ├── Caddyfile.ssl │ ├── Containerfile │ └── entrypoint.sh ├── cups │ ├── Containerfile │ └── cupsd.conf ├── docker-compose.yml └── podman-compose.yml ├── 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 | E711, 68 | E129, 69 | F841, 70 | E713, 71 | E712, 72 | B028, 73 | 74 | max-line-length = 200 75 | exclude=,test_*.py 76 | -------------------------------------------------------------------------------- /.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 | git clone https://github.com/frappe/frappe --branch version-14 32 | bench init frappe-bench --frappe-path ~/frappe --python "$(which python)" --skip-assets --ignore-exist 33 | 34 | mkdir ~/frappe-bench/sites/test_site 35 | cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/ 36 | 37 | cd ~/frappe-bench || exit 38 | 39 | sed -i 's/watch:/# watch:/g' Procfile 40 | sed -i 's/schedule:/# schedule:/g' Procfile 41 | sed -i 's/socketio:/# socketio:/g' Procfile 42 | sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile 43 | 44 | bench get-app https://github.com/frappe/erpnext --branch version-14 --resolve-deps --skip-assets 45 | bench get-app beam "${GITHUB_WORKSPACE}" --skip-assets --resolve-deps 46 | 47 | printf '%s\n' 'frappe' 'erpnext' 'beam' > ~/frappe-bench/sites/apps.txt 48 | bench setup requirements --python 49 | bench use test_site 50 | 51 | bench start &> bench_run_logs.txt & 52 | CI=Yes & 53 | bench --site test_site reinstall --yes --admin-password admin 54 | 55 | # bench --site test_site install-app erpnext beam 56 | bench setup requirements --dev 57 | 58 | echo "BENCH VERSION NUMBERS:" 59 | bench version 60 | echo "SITE LIST-APPS:" 61 | bench list-apps 62 | 63 | bench start &> bench_run_logs.txt & 64 | CI=Yes & 65 | bench execute 'beam.tests.setup.before_test' 66 | -------------------------------------------------------------------------------- /.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 | "install_apps": ["erpnext", "beam"], 16 | "throttle_user_limit": 100, 17 | "developer_mode": 1 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 | modules = (app_dir / this_app / "modules.txt").read_text().split("\n") 41 | for doctype, customize_files in customized_doctypes.items(): 42 | for customize_file in customize_files: 43 | if not this_app in str(customize_file): 44 | continue 45 | module = customize_file.parent.parent.stem 46 | file_contents = json.loads(customize_file.read_text()) 47 | if file_contents.get("custom_fields"): 48 | for custom_field in file_contents.get("custom_fields"): 49 | if set_module: 50 | custom_field["module"] = unscrub(module) 51 | continue 52 | if not custom_field.get("module"): 53 | exceptions.append( 54 | f"Custom Field for {custom_field.get('dt')} in {this_app} '{custom_field.get('fieldname')}' does not have a module key" 55 | ) 56 | continue 57 | elif custom_field.get("module") not in modules: 58 | exceptions.append( 59 | 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" 60 | ) 61 | continue 62 | if file_contents.get("property_setters"): 63 | for ps in file_contents.get("property_setters"): 64 | if set_module: 65 | ps["module"] = unscrub(module) 66 | continue 67 | if not ps.get("module"): 68 | exceptions.append( 69 | 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" 70 | ) 71 | continue 72 | elif ps.get("module") not in modules: 73 | exceptions.append( 74 | 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" 75 | ) 76 | continue 77 | if set_module: 78 | with customize_file.open("w", encoding="UTF-8") as target: 79 | json.dump(file_contents, target, sort_keys=True, indent=2) 80 | 81 | return exceptions 82 | 83 | 84 | def validate_no_custom_perms(customized_doctypes): 85 | exceptions = [] 86 | this_app = pathlib.Path(__file__).resolve().parent.parent.stem 87 | for doctype, customize_files in customized_doctypes.items(): 88 | for customize_file in customize_files: 89 | if not this_app in str(customize_file): 90 | continue 91 | file_contents = json.loads(customize_file.read_text()) 92 | if file_contents.get("custom_perms"): 93 | exceptions.append(f"Customization for {doctype} in {this_app} contains custom permissions") 94 | return exceptions 95 | 96 | 97 | def validate_duplicate_customizations(customized_doctypes): 98 | exceptions = [] 99 | common_fields = {} 100 | common_property_setters = {} 101 | app_dir = pathlib.Path(__file__).resolve().parent.parent 102 | this_app = app_dir.stem 103 | for doctype, customize_files in customized_doctypes.items(): 104 | if len(customize_files) == 1: 105 | continue 106 | common_fields[doctype] = {} 107 | common_property_setters[doctype] = {} 108 | for customize_file in customize_files: 109 | module = customize_file.parent.parent.stem 110 | app = customize_file.parent.parent.parent.parent.stem 111 | file_contents = json.loads(customize_file.read_text()) 112 | if file_contents.get("custom_fields"): 113 | fields = [cf.get("fieldname") for cf in file_contents.get("custom_fields")] 114 | common_fields[doctype][module] = fields 115 | if file_contents.get("property_setters"): 116 | ps = [ps.get("name") for ps in file_contents.get("property_setters")] 117 | common_property_setters[doctype][module] = ps 118 | 119 | for doctype, module_and_fields in common_fields.items(): 120 | if this_app not in module_and_fields.keys(): 121 | continue 122 | this_modules_fields = module_and_fields.pop(this_app) 123 | for module, fields in module_and_fields.items(): 124 | for field in fields: 125 | if field in this_modules_fields: 126 | exceptions.append( 127 | f"Custom Field for {unscrub(doctype)} in {this_app} '{field}' also appears in customizations for {module}" 128 | ) 129 | 130 | for doctype, module_and_ps in common_property_setters.items(): 131 | if this_app not in module_and_ps.keys(): 132 | continue 133 | this_modules_ps = module_and_ps.pop(this_app) 134 | for module, ps in module_and_ps.items(): 135 | for p in ps: 136 | if p in this_modules_ps: 137 | exceptions.append( 138 | f"Property Setter for {unscrub(doctype)} in {this_app} on '{p}' also appears in customizations for {module}" 139 | ) 140 | 141 | return exceptions 142 | 143 | 144 | def validate_customizations(set_module): 145 | customized_doctypes = get_customized_doctypes() 146 | exceptions = validate_no_custom_perms(customized_doctypes) 147 | exceptions += validate_module(customized_doctypes, set_module) 148 | exceptions += validate_duplicate_customizations(customized_doctypes) 149 | 150 | return exceptions 151 | 152 | 153 | if __name__ == "__main__": 154 | exceptions = [] 155 | set_module = False 156 | for arg in sys.argv: 157 | if arg == "--set-module": 158 | set_module = True 159 | exceptions.append(validate_customizations(set_module)) 160 | 161 | if exceptions: 162 | for exception in exceptions: 163 | [print(e) for e in exception] # TODO: colorize 164 | 165 | sys.exit(1) if all(exceptions) else sys.exit(0) 166 | -------------------------------------------------------------------------------- /.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/generate_matrix.yaml: -------------------------------------------------------------------------------- 1 | name: Generate Matrix 2 | 3 | on: 4 | push: 5 | branches: 6 | - version-14 7 | pull_request: 8 | 9 | env: 10 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 11 | 12 | jobs: 13 | generate: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v4 17 | 18 | - name: Setup Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: '3.10' 22 | 23 | - name: Generate Scanning Decision Matrix 24 | run: python3 ./beam/docs/generate_matrix.py -------------------------------------------------------------------------------- /.github/workflows/lint.yaml: -------------------------------------------------------------------------------- 1 | name: Linters 2 | 3 | on: 4 | push: 5 | branches: 6 | - version-15 7 | - version-14 8 | pull_request: 9 | 10 | env: 11 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | mypy: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v4 19 | 20 | - name: Setup Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: '3.10' 24 | 25 | - name: Install mypy 26 | run: pip install mypy 27 | 28 | - name: Install mypy types 29 | run: mypy ./beam/. --install-types --non-interactive 30 | 31 | - name: Run mypy 32 | uses: sasanquaneuf/mypy-github-action@releases/v1 33 | with: 34 | checkName: 'mypy' 35 | env: 36 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 37 | 38 | black: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - name: Checkout 42 | uses: actions/checkout@v4 43 | 44 | - name: Setup Python 45 | uses: actions/setup-python@v5 46 | with: 47 | python-version: '3.10' 48 | 49 | - name: Install Black (Frappe) 50 | run: pip install git+https://github.com/frappe/black.git 51 | 52 | - name: Run Black (Frappe) 53 | run: black --check . 54 | 55 | prettier: 56 | runs-on: ubuntu-latest 57 | steps: 58 | - name: Checkout 59 | uses: actions/checkout@v4 60 | 61 | - name: Prettify code 62 | uses: rutajdash/prettier-cli-action@v1.0.0 63 | with: 64 | config_path: ./.prettierrc.js 65 | ignore_path: ./.prettierignore 66 | 67 | - name: Prettier Output 68 | if: ${{ failure() }} 69 | shell: bash 70 | run: | 71 | echo "The following files are not formatted:" 72 | echo "${{steps.prettier-run.outputs.prettier_output}}" 73 | 74 | -------------------------------------------------------------------------------- /.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 }} -------------------------------------------------------------------------------- /.github/workflows/pytest.yaml: -------------------------------------------------------------------------------- 1 | name: Pytest CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - version-14 7 | pull_request: 8 | branches: 9 | - version-14 10 | 11 | # concurrency: 12 | # group: develop-cloud_storage-${{ github.event.number }} 13 | # cancel-in-progress: true 14 | 15 | jobs: 16 | tests: 17 | runs-on: ${{ matrix.os }} 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest] 21 | fail-fast: false 22 | name: Server 23 | 24 | services: 25 | mariadb: 26 | image: mariadb:10.6 27 | env: 28 | MYSQL_ALLOW_EMPTY_PASSWORD: YES 29 | MYSQL_ROOT_PASSWORD: 'admin' 30 | ports: 31 | - 3306:3306 32 | options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3 33 | 34 | steps: 35 | - name: Clone 36 | uses: actions/checkout@v4 37 | 38 | - name: Setup Python 39 | uses: actions/setup-python@v5 40 | with: 41 | python-version: '3.10' 42 | 43 | - name: Setup Node 44 | uses: actions/setup-node@v4 45 | with: 46 | node-version: 14 47 | check-latest: true 48 | cache: 'yarn' # Replaces `Get yarn cache directory path` and `yarn-cache` steps 49 | 50 | # Uncomment if running locally, remove after local testing (already available in github actions environment) 51 | # - name: Install Yarn 52 | # run: npm install -g yarn 53 | 54 | - name: Add to Hosts 55 | run: echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts 56 | 57 | - name: Cache pip 58 | uses: actions/cache@v4 59 | with: 60 | path: ~/.cache/pip 61 | key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} 62 | restore-keys: | 63 | ${{ runner.os }}-pip- 64 | ${{ runner.os }}- 65 | 66 | - name: Install Poetry 67 | uses: snok/install-poetry@v1 68 | 69 | # - name: Get yarn cache directory path 70 | # id: yarn-cache-dir-path 71 | # run: 'echo "::set-output name=dir::$(yarn cache dir)"' 72 | 73 | # - uses: actions/cache@v3 74 | # id: yarn-cache 75 | # with: 76 | # path: ${{ steps.yarn-cache-dir-path.outputs.dir }} 77 | # key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} 78 | # restore-keys: | 79 | # ${{ runner.os }}-yarn- 80 | 81 | - name: Install JS Dependencies 82 | run: yarn --prefer-offline 83 | 84 | - name: Install App Dependencies 85 | run: bash ${{ github.workspace }}/.github/helper/install_dependencies.sh 86 | 87 | - name: Install Bench Site and Apps 88 | env: 89 | MYSQL_HOST: 'localhost' 90 | MYSQL_PWD: 'admin' 91 | run: | 92 | bash ${{ github.workspace }}/.github/helper/install.sh 93 | 94 | - name: Run Tests 95 | working-directory: /home/runner/frappe-bench 96 | run: | 97 | source env/bin/activate 98 | cd apps/beam 99 | poetry install 100 | pytest --cov=beam --cov-report=xml --disable-warnings -s | tee pytest-coverage.txt 101 | 102 | - name: Pytest coverage comment 103 | uses: MishaKav/pytest-coverage-comment@main 104 | with: 105 | pytest-coverage-path: /home/runner/frappe-bench/apps/beam/pytest-coverage.txt 106 | pytest-xml-coverage-path: /home/runner/frappe-bench/apps/beam/coverage.xml -------------------------------------------------------------------------------- /.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@v3 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/frappe/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 . --write --ignore-path .prettierignore 38 | language: node 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: validate_copyright 63 | files: '\.(js|ts|py|md)$' 64 | args: ["--app", "beam"] 65 | - id: clean_customized_doctypes 66 | args: ["--app", "beam"] 67 | - id: validate_customizations 68 | - id: validate_python_dependencies 69 | - id: validate_javascript_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 | beam/docs/current 16 | beam/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 | ## BEAM 2 | 3 | Barcode Scanning 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.4/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, its prerequisite Payments, and the HR module 24 | ``` 25 | bench get-app erpnext --branch version-14 26 | ``` 27 | Download this application and install all apps 28 | ``` 29 | bench get-app beam git@github.com:agritheory/beam.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 'beam.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 'beam.tests.setup.before_test' 55 | ``` 56 | 57 | To run mypy and pytest 58 | ```shell 59 | source env/bin/activate 60 | mypy ./apps/beam/beam --ignore-missing-imports 61 | pytest ./apps/beam/beam/tests -s --disable-warnings 62 | ``` 63 | 64 | ### Printer Server setup 65 | ```shell 66 | sudo apt-get install gcc cups python3-dev libcups2-dev -y 67 | # for development it helps to have the CUPS PDF printer installed 68 | # sudo apt-get -y install printer-driver-cups-pdf 69 | 70 | bench pip install pycups 71 | sudo usermod -a -G lpadmin {username} # the "frappe" user in most installations 72 | ``` 73 | Go to `{server URL or localhost}:631` to access the CUPS web interface 74 | Configuration on a remote server will take extra steps to secure: 75 | https://askubuntu.com/questions/23936/how-do-you-administer-cups-remotely-using-the-web-interface 76 | -------------------------------------------------------------------------------- /beam/__init__.py: -------------------------------------------------------------------------------- 1 | __version__ = "14.8.6" 2 | -------------------------------------------------------------------------------- /beam/beam/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/agritheory/beam/6d162576438ee91b23781b96ce0d01d2653d6846/beam/beam/__init__.py -------------------------------------------------------------------------------- /beam/beam/barcodes.py: -------------------------------------------------------------------------------- 1 | import base64 2 | import uuid 3 | from io import BytesIO 4 | 5 | import frappe 6 | from barcode import Code128 7 | from barcode.writer import ImageWriter 8 | from zebra_zpl import Barcode, Label, Printable, Text 9 | 10 | 11 | @frappe.whitelist() 12 | def create_beam_barcode(doc, method=None): 13 | if doc.doctype == "Item" and doc.is_stock_item == 0: 14 | return 15 | if ( 16 | doc.get("item_group") 17 | and doc.doctype == "Item" 18 | and frappe.db.exists("Item Group", "Products") 19 | and doc.item_group 20 | in frappe.get_all("Item Group", {"name": ("descendants of", "Products")}, pluck="name") 21 | ): 22 | # TODO: refactor this to be configurable to "Products" or "sold" items that do not require handling units 23 | return 24 | if any([b for b in doc.barcodes if b.barcode_type == "Code128"]): 25 | return 26 | # move all other rows back 27 | for row_index, b in enumerate(doc.barcodes, start=1): 28 | b.idx = row_index + 1 29 | doc.append( 30 | "barcodes", 31 | {"barcode": f"{str(uuid.uuid4().int >> 64):020}", "barcode_type": "Code128", "idx": 1}, 32 | ) 33 | return doc 34 | 35 | 36 | @frappe.whitelist() 37 | @frappe.read_only() 38 | def barcode128(barcode_text: str) -> str: 39 | if not barcode_text: 40 | return "" 41 | temp = BytesIO() 42 | instance = Code128(barcode_text, writer=ImageWriter()) 43 | instance.write( 44 | temp, 45 | options={"module_width": 0.4, "module_height": 10, "font_size": 0, "compress": True}, 46 | ) 47 | encoded = base64.b64encode(temp.getvalue()).decode("ascii") 48 | return f'' 49 | 50 | 51 | @frappe.whitelist() 52 | @frappe.read_only() 53 | def formatted_zpl_barcode(barcode_text: str) -> str: 54 | bc = Barcode( 55 | barcode_text, 56 | type="C", 57 | human_readable="Y", 58 | width=4, 59 | height=260, 60 | ratio=1, 61 | justification="C", 62 | position=(20, 40), 63 | ) 64 | return bc.to_zpl() 65 | 66 | 67 | @frappe.whitelist() 68 | @frappe.read_only() 69 | def formatted_zpl_label( 70 | width: int, length: int, dpi: int = 203, print_speed: int = 2, copies: int = 1 71 | ) -> frappe._dict: 72 | l = frappe._dict() 73 | # ^XA Start format 74 | # ^LL