diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..9a01dea --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,42 @@ +--- +name: Bug Report +about: Report a bug to help us improve ERP Core +title: "[Bug]: " +labels: bug +assignees: '' +--- + +## Description + +A clear and concise description of the bug. + +## Steps to Reproduce + +1. Go to '...' +2. Click on '...' +3. Run '...' +4. See error + +## Expected Behavior + +What you expected to happen. + +## Actual Behavior + +What actually happened. + +## Error Logs / Traceback + +If applicable, paste the traceback or error logs (from `bench --site console` or the error log) to help explain the problem. + +## Environment + +- **ERP Core Version**: [e.g. 0.0.1] +- **Frappe Version**: [e.g. 16.x.x] +- **ERPNext Version**: [e.g. 16.x.x] +- **Python Version**: [e.g. 3.14] +- **OS**: [e.g. Ubuntu 22.04] + +## Additional Context + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..7eb6ea8 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,28 @@ +--- +name: Feature Request +about: Suggest a new feature or enhancement for ERP Core +title: "[Feature]: " +labels: enhancement +assignees: '' +--- + +## Problem Statement + +A clear description of the problem or limitation you're experiencing. +Example: "I'm always frustrated when [...]" + +## Proposed Solution + +A clear description of what you'd like to happen. + +## Alternatives Considered + +Any alternative solutions or features you've considered. + +## Use Case + +Describe the business scenario or compliance requirement where this feature would be useful. + +## Additional Context + +Add any references (e.g. ZATCA specifications, regulations) that help illustrate the request. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..8911d27 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,31 @@ +## Description + + +## Type of Change + + +- [ ] Bug fix (non-breaking change that fixes an issue) +- [ ] New feature (non-breaking change that adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to change) +- [ ] Documentation update +- [ ] Refactoring (no functional changes) + +## Related Issue + +Fixes # + +## How Has This Been Tested? + + +- [ ] Lint passes (`ruff check erpcore/`) +- [ ] Unit tests pass (`bench --site run-tests --app erpcore`) +- [ ] Manual testing on a site + +## Checklist + +- [ ] My code follows the project's coding style +- [ ] I have run `pre-commit` hooks locally +- [ ] My commits follow the [Conventional Commits](https://www.conventionalcommits.org/) format +- [ ] I have added/updated tests for my changes (if applicable) +- [ ] I have updated documentation (if applicable) +- [ ] My changes do not introduce new warnings or errors diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..a8c89ce --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,25 @@ +version: 2 +updates: + # GitHub Actions used by the workflows + - package-ecosystem: github-actions + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: ci + labels: + - dependencies + - github-actions + + # Python backend (Frappe app) + - package-ecosystem: pip + directory: "/" + schedule: + interval: weekly + open-pull-requests-limit: 5 + commit-message: + prefix: chore + labels: + - dependencies + - python diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d3a6872..42cf081 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: steps: - name: Clone - uses: actions/checkout@v6 + uses: actions/checkout@v7 - name: Find tests run: | @@ -49,13 +49,13 @@ jobs: python-version: '3.14' - name: Setup Node - uses: actions/setup-node@v6 + uses: actions/setup-node@v7 with: node-version: 24 check-latest: true - name: Cache pip - uses: actions/cache@v4 + uses: actions/cache@v6 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} @@ -67,7 +67,7 @@ jobs: id: yarn-cache-dir-path run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' - - uses: actions/cache@v4 + - uses: actions/cache@v6 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -88,9 +88,11 @@ jobs: - name: Install working-directory: /home/runner/frappe-bench run: | + bench get-app --branch version-16 erpnext bench get-app erpcore $GITHUB_WORKSPACE bench setup requirements --dev bench new-site --db-root-password root --admin-password admin test_site + bench --site test_site install-app erpnext bench --site test_site install-app erpcore bench build env: diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 3c810e5..a8ff663 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -18,12 +18,12 @@ jobs: if: github.event_name == 'pull_request' steps: - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - uses: actions/setup-python@v6 with: python-version: '3.14' cache: pip - - uses: pre-commit/action@v3.0.0 + - uses: pre-commit/action@v3.0.1 - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules @@ -42,10 +42,10 @@ jobs: with: python-version: '3.14' - - uses: actions/checkout@v6 + - uses: actions/checkout@v7 - name: Cache pip - uses: actions/cache@v5 + uses: actions/cache@v6 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dc89e74 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,58 @@ +name: Generate Semantic Release + +on: + push: + branches: + - develop + +permissions: + contents: write + issues: write + pull-requests: write + +concurrency: + group: release-erpcore + cancel-in-progress: false + +jobs: + release: + name: Release + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + RELEASE_AUTH_TOKEN: ${{ secrets.RELEASE_TOKEN != '' && secrets.RELEASE_TOKEN || github.token }} + steps: + - name: Checkout Entire Repository + uses: actions/checkout@v7 + with: + fetch-depth: 0 + persist-credentials: true + token: ${{ env.RELEASE_AUTH_TOKEN }} + + - name: Setup Node.js + uses: actions/setup-node@v7 + with: + node-version: 20 + + - name: Setup dependencies + run: | + npm install semantic-release @semantic-release/git @semantic-release/exec @semantic-release/github @semantic-release/commit-analyzer @semantic-release/release-notes-generator --no-save + + - name: Show token source + run: | + set -e + if [ -n "${{ secrets.RELEASE_TOKEN }}" ]; then + echo "Using RELEASE_TOKEN secret for semantic-release" + else + echo "RELEASE_TOKEN is not set; falling back to github.token" + fi + + - name: Create Release + env: + GH_TOKEN: ${{ env.RELEASE_AUTH_TOKEN }} + GITHUB_TOKEN: ${{ env.RELEASE_AUTH_TOKEN }} + GIT_AUTHOR_NAME: "ERP Core Bot" + GIT_AUTHOR_EMAIL: "hello@kodlyft.com" + GIT_COMMITTER_NAME: "ERP Core Bot" + GIT_COMMITTER_EMAIL: "hello@kodlyft.com" + run: npx semantic-release diff --git a/.github/workflows/semantic-commits.yml b/.github/workflows/semantic-commits.yml new file mode 100644 index 0000000..72b472c --- /dev/null +++ b/.github/workflows/semantic-commits.yml @@ -0,0 +1,30 @@ +name: Semantic Commits + +on: + pull_request: {} + +permissions: + contents: read + +concurrency: + group: commitcheck-erpcore-${{ github.event.number }} + cancel-in-progress: true + +jobs: + commitlint: + name: Check Commit Titles + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 200 + + - uses: actions/setup-node@v7 + with: + node-version: 20 + check-latest: true + + - name: Check commit titles + run: | + npm install @commitlint/cli @commitlint/config-conventional + npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }} diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 28c0628..b47d24f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -63,7 +63,37 @@ repos: erpcore/public/js/lib/.* )$ + - repo: local + hooks: + - id: frappe-semgrep-rules + name: "Run Frappe semgrep rules" + entry: semgrep + language: python + additional_dependencies: ['semgrep==1.168.0'] + args: + - '--config=.semgrep' + - '--error' + - '--quiet' + - '--metrics=off' + - '--disable-version-check' + types_or: [python, javascript] + pass_filenames: true + exclude: | + (?x)^( + erpcore/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + erpcore/public/js/lib/.* + )$ + + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook + rev: v9.18.0 + hooks: + - id: commitlint + stages: [commit-msg] + additional_dependencies: ['@commitlint/config-conventional'] + ci: autoupdate_schedule: weekly - skip: [] + skip: [frappe-semgrep-rules, commitlint] submodules: false diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..985e09a --- /dev/null +++ b/.prettierignore @@ -0,0 +1,10 @@ +node_modules +build +dist +*.min.js +*.min.css +erpcore/public/dist +erpcore/templates/includes +erpcore/public/js/lib +*.svg +*.md diff --git a/.prettierrc b/.prettierrc new file mode 100755 index 0000000..2a4aebc --- /dev/null +++ b/.prettierrc @@ -0,0 +1,21 @@ +{ + "printWidth": 110, + "tabWidth": 4, + "useTabs": true, + "semi": true, + "singleQuote": false, + "trailingComma": "all", + "bracketSpacing": true, + "arrowParens": "always", + "endOfLine": "lf", + "vueIndentScriptAndStyle": false, + "overrides": [ + { + "files": "*.json", + "options": { + "useTabs": false, + "tabWidth": 2 + } + } + ] +} diff --git a/.releaserc b/.releaserc new file mode 100644 index 0000000..391dd5b --- /dev/null +++ b/.releaserc @@ -0,0 +1,24 @@ +{ + "branches": ["develop"], + "plugins": [ + ["@semantic-release/commit-analyzer", { + "preset": "angular", + "releaseRules": [ + {"breaking": true, "release": "major"} + ] + }], + "@semantic-release/release-notes-generator", + [ + "@semantic-release/exec", { + "prepareCmd": "sed -ir \"s/[0-9]*\\.[0-9]*\\.[0-9]*/${nextRelease.version}/\" erpcore/__init__.py" + } + ], + [ + "@semantic-release/git", { + "assets": ["erpcore/__init__.py"], + "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}" + } + ], + "@semantic-release/github" + ] +} diff --git a/.semgrep/README.md b/.semgrep/README.md new file mode 100644 index 0000000..fdbe4ad --- /dev/null +++ b/.semgrep/README.md @@ -0,0 +1,28 @@ +# Frappe Semgrep rules (vendored) + +These are the static-analysis rules from +[frappe/frappe-semgrep-rules](https://github.com/frappe/frappe-semgrep-rules), +vendored here so they run as part of the erpcore `pre-commit` gate without +depending on network access or an upstream tag (the upstream repo ships no +`.pre-commit-hooks.yaml` and is not tagged). + +Wired into [`.pre-commit-config.yaml`](../.pre-commit-config.yaml) as the +`frappe-semgrep-rules` hook. + +## Run manually + +```bash +semgrep --config .semgrep --error erpcore +``` + +## Updating + +```bash +git clone --depth 1 https://github.com/frappe/frappe-semgrep-rules /tmp/fsr +cp /tmp/fsr/rules/*.yml .semgrep/ +cp /tmp/fsr/rules/security/*.yml .semgrep/security/ +semgrep --validate --config .semgrep +``` + +Only the `*.yml` rule files are vendored; upstream `.py`/`.js` files are +semgrep test fixtures (intentionally vulnerable) and are deliberately omitted. diff --git a/.semgrep/code_quality.yml b/.semgrep/code_quality.yml new file mode 100644 index 0000000..67e5daf --- /dev/null +++ b/.semgrep/code_quality.yml @@ -0,0 +1,52 @@ +rules: +- id: overusing-args + patterns: + - pattern: | + def $FUNC(args): + ... + message: | + Using args as argument in functions reduces readability and allows passing ill-specified arguments. Avoid using `args` as only argument unless absolutely necessary. + languages: [python] + severity: WARNING + +- id: use-vanilla-js-include + pattern: in_list($LIST, $ITEM) + fix: $LIST.includes($ITEM) + message: | + in_list(list, item) is simple wrapper around list.includes(item), avoid use of such wrappers. + languages: [javascript] + severity: WARNING + +- id: useless-get-doc-dict + pattern: frappe.get_doc(dict($X=$Y, $...OTHER_KEYS)) + fix: frappe.get_doc($X=$Y, $...OTHER_KEYS) + message: | + get_doc(dict(k=v)) is same as get_doc(k=v) + languages: [python] + severity: WARNING + +- id: unchecked-frappe-permission-call + languages: + - python + message: >- + Found a call to `frappe.has_permission` where the return value is ignored. + Unless `throw=True` is passed, this function only returns a boolean and + does not enforce security on its own. + severity: ERROR + patterns: + - pattern: frappe.has_permission(...) + - pattern-not: frappe.has_permission(..., throw=True, ...) + - pattern-not: frappe.has_permission(..., throw=$SOMETHING, ...) + - pattern-not-inside: $VAR = frappe.has_permission(...) + - pattern-not-inside: "return ..." + - pattern-not-inside: "if <... frappe.has_permission(...) ...> : ..." + - pattern-not-inside: "$PRED_TRUE if frappe.has_permission(...) else $PRED_FALSE" + - pattern-not-inside: assert frappe.has_permission(...) + # value used as a comprehension predicate (list / set / dict / generator) + - pattern-not-inside: "[$EL for $V in $IT if <... frappe.has_permission(...) ...>]" + - pattern-not-inside: "{$EL for $V in $IT if <... frappe.has_permission(...) ...>}" + - pattern-not-inside: "{$K: $VAL for $V in $IT if <... frappe.has_permission(...) ...>}" + - pattern-not-inside: "($EL for $V in $IT if <... frappe.has_permission(...) ...>)" + paths: + exclude: + - "**/test_*.py" diff --git a/.semgrep/frappe_correctness.yml b/.semgrep/frappe_correctness.yml new file mode 100644 index 0000000..d0b7f34 --- /dev/null +++ b/.semgrep/frappe_correctness.yml @@ -0,0 +1,368 @@ +rules: +- id: frappe-breaks-multitenancy + patterns: + - pattern-either: + - pattern: $VAR = <... frappe.db.$METHOD(...) ...> + - pattern: $VAR = <... frappe.get_all(...) ...> + - pattern: $VAR = <... frappe.get_list(...) ...> + - pattern: $VAR = <... frappe.get_doc(...) ...> + - pattern: $VAR = <... frappe.get_single(...) ...> + - pattern: $VAR = <... frappe.get_cached_doc(...) ...> + - pattern: $VAR = <... frappe.cache(...) ...> + - pattern: $VAR = <... frappe.cache ...> + - pattern: $VAR = <... frappe.get_meta(...) ...> + - pattern: $VAR = <... frappe.get_hooks(...) ...> + - pattern: $VAR = <... frappe.local.$ATTR ...> + - pattern: $VAR = <... frappe.conf ...> + - pattern: $VAR = <... frappe.flags ...> + - pattern: $VAR = <... frappe.lang ...> + - pattern: $VAR = <... frappe._(...) ...> + - pattern: $VAR = <... flt(...) ...> + - pattern: $VAR = <... frappe.qb ...> + - pattern-not-inside: | + def $FUNCTION(...): + ... + paths: + exclude: + - "**/test_*.py" + message: | + $VAR global variable does not respect database multitenancy, consider wrapping it in function or method call. + languages: [python] + severity: ERROR + +- id: frappe-cache-breaks-multitenancy + patterns: + - pattern-either: + - pattern: frappe.cache().set(...) + - pattern: | + $VAR = frappe.cache() + $VAR.set(...) + - pattern: frappe.cache().get(...) + - pattern: | + $VAR = frappe.cache() + $VAR.get(...) + - pattern: frappe.cache.set(...) + - pattern: frappe.cache.get(...) + message: | + redis.set and redis.get do not support multitenancy, use set_value/get_value instead. + languages: [python] + severity: ERROR + +- id: frappe-modifying-but-not-comitting + patterns: + - pattern: | + def $METHOD(self, ...): + ... + self.$ATTR = ... + - pattern-not: | + def $METHOD(self, ...): + ... + self.$ATTR = ... + ... + self.db_set(..., self.$ATTR, ...) + - pattern-not: | + def $METHOD(self, ...): + ... + self.$ATTR = $SOME_VAR + ... + self.db_set(..., $SOME_VAR, ...) + - pattern-not: | + def $METHOD(self, ...): + ... + self.$ATTR = $SOME_VAR + ... + self.save(...) + - metavariable-regex: + metavariable: '$ATTR' + # this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me) + regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$' + - metavariable-regex: + metavariable: "$METHOD" + regex: "(on_submit|on_cancel|after_insert|on_update|on_update_after_submit)" + message: | + DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database. + languages: [python] + severity: ERROR + +- id: frappe-modifying-but-not-comitting-other-method + patterns: + - pattern: | + class $DOCTYPE(...): + def $METHOD(self, ...): + ... + self.$ANOTHER_METHOD() + ... + + def $ANOTHER_METHOD(self, ...): + ... + self.$ATTR = ... + - pattern-not: | + class $DOCTYPE(...): + def $METHOD(self, ...): + ... + self.$ANOTHER_METHOD() + ... + + def $ANOTHER_METHOD(self, ...): + ... + self.$ATTR = ... + ... + self.db_set(..., self.$ATTR, ...) + - pattern-not: | + class $DOCTYPE(...): + def $METHOD(self, ...): + ... + self.$ANOTHER_METHOD() + ... + + def $ANOTHER_METHOD(self, ...): + ... + self.$ATTR = $SOME_VAR + ... + self.db_set(..., $SOME_VAR, ...) + - pattern-not: | + class $DOCTYPE(...): + def $METHOD(self, ...): + ... + self.$ANOTHER_METHOD() + ... + self.save(...) + def $ANOTHER_METHOD(self, ...): + ... + self.$ATTR = ... + - metavariable-regex: + metavariable: "$METHOD" + regex: "(on_submit|on_cancel|after_insert|on_update|on_update_after_submit)" + message: | + self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database. + languages: [python] + severity: ERROR + +- id: frappe-print-function-in-doctypes + pattern: print(...) + message: | + Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement. + languages: [python] + severity: WARNING + paths: + include: + - "**/doctype/*" + +- id: frappe-no-functional-code + pattern-either: + - pattern: "map(...)" + - pattern: "filter(...)" + message: | + Mixing functional programming usually yields in confusing code and bugs. Use list comprehensions or generators instead. + languages: [python] + severity: WARNING + +- id: frappe-query-debug-statement + pattern-either: + - pattern: $FUNC(..., debug=True, ...) + - pattern: $FUNC(..., debug=1, ...) + message: | + Did you mean to leave this debug statement in? + languages: [python] + severity: WARNING + + +- id: frappe-modifying-child-tables-while-iterating + pattern-either: + - pattern: | + for $ROW in self.$TABLE: + ... + self.remove(...) + - pattern: | + for $ROW in self.$TABLE: + ... + self.append(...) + message: | + Child table being modified while iterating on it. + languages: [python] + severity: ERROR + paths: + include: + - "**/doctype/*" + +- id: frappe-same-key-assigned-twice + pattern-either: + - pattern: | + {..., $X: $A, ..., $X: $B, ...} + - pattern: | + dict(..., ($X, $A), ..., ($X, $B), ...) + - pattern: | + _dict(..., ($X, $A), ..., ($X, $B), ...) + message: | + key `$X` is uselessly assigned twice. This could be a potential bug. + languages: [python] + severity: ERROR + +- id: frappe-manual-commit + patterns: + - pattern: frappe.db.commit() + - pattern-not-inside: | + try: + ... + except ...: + ... + message: | + Manually commiting a transaction is highly discouraged. Read about the transaction model implemented by Frappe Framework before adding manual commits: https://frappeframework.com/docs/user/en/api/database#database-transaction-model If you think manual commit is required then add a comment explaining why and `# nosemgrep` on the same line. + paths: + exclude: + - "**/patches/**" + - "**/demo/**" + languages: [python] + severity: WARNING + +- id: frappe-redis-flush + patterns: + - pattern-either: + - pattern: frappe.cache.flushall() + - pattern: frappe.cache().flushall() + message: | + flushall flushes all databases on Redis server, use flushdb instead. + languages: [python] + severity: ERROR + +- id: frappe-overriding-local-proxies + patterns: + - pattern: frappe.$ATTR = ... + - metavariable-regex: + metavariable: $ATTR + regex: ^(db|qb|conf|form|form_dict|request|response|session|user|flags|error_log|debug_log|message_log|lang)$ + message: | + frappe.$ATTR is a local proxy, assigning it to another object will remove the proxying and replace it with another object. Use frappe.local.$ATTR instead. + languages: [python] + severity: ERROR + + +- id: frappe-single-value-type-safety + patterns: + - pattern-either: + - pattern: frappe.db.get_value($DOCTYPE, $DOCTYPE, $...AFTER) + - pattern: frappe.db.get_value($DOCTYPE, None, $...AFTER) + - pattern-not: frappe.db.get_value($DOCTYPE, None, [...]) + - pattern-not: frappe.db.get_value($DOCTYPE, $DOCTYPE, [...]) + fix: frappe.db.get_single_value($DOCTYPE, $...AFTER) + message: | + If $DOCTYPE is a single doctype then using `frappe.db.get_value` is discouraged for fetching value from single doctypes. frappe.db.get_value for single doctype is not type safe, use `frappe.db.get_single_value` instead. + languages: [python] + severity: ERROR + +- id: frappe-set-value-semantics + patterns: + - pattern-either: + - pattern: frappe.db.set_value($DOCTYPE, None, $...AFTER) + - pattern: frappe.db.set_value($DOCTYPE, $DOCTYPE, $...AFTER) + fix: frappe.db.set_single_value($DOCTYPE, $...AFTER) + message: | + If $DOCTYPE is a single doctype then using `frappe.db.set_value` is discouraged for setting values in DB. Use db.set_single_value for single doctype instead. + languages: [python] + severity: ERROR + +- id: frappe-after-save-controller-hook + pattern: | + class $DOCTYPE($SOMEBASECLASS, ...): + def after_save(self): + ... + message: | + `after_save` is not a valid DocType controller hook. Please have a look at the hooks available: https://frappeframework.com/docs/v13/user/en/basics/doctypes/controllers#controller-hooks + languages: [python] + severity: ERROR + +- id: frappe-qb-incorrect-order-usage + patterns: + - pattern-either: + - pattern: $QUERY. ... .orderby(..., "asc") + - pattern: $QUERY. ... .orderby(..., "desc") + - pattern: $QUERY. ... .orderby(..., frappe.qb.desc) + - pattern: $QUERY. ... .orderby(..., Order.desc) + - pattern: $QUERY. ... .orderby(..., Order.asc) + - pattern-not: $QUERY. ... .orderby(..., order=$LASTVAR) + message: | + `order` in `orderby` has to be a keyword argument like `.orderby("time", order=frappe.qb.desc)`. Re-check the generated query. + languages: [python] + severity: ERROR + +- id: frappe-cur-frm-usage + pattern: cur_frm + message: | + `cur_frm` is deprecated and can introduce buggy behaviour. + languages: [javascript] + severity: WARNING + +- id: frappe-incorrect-debounce + pattern: frappe.utils.debounce(...)(...); + message: | + `debounce` should be used to create a debounced version of function and should only be called once. If the function is called multiple times then each time a new debounced version of function gets created and you don't actually achieve debounce behaviour. + languages: [javascript] + severity: ERROR + +- id: frappe-realtime-pick-room + patterns: + - pattern: frappe.publish_realtime(...) + - pattern-not: frappe.publish_realtime(..., doctype=$SOMETHING, ...) + - pattern-not: frappe.publish_realtime(..., docname=$SOMETHING, ...) + - pattern-not: frappe.publish_realtime(..., room=$SOMETHING, ...) + - pattern-not: frappe.publish_realtime(..., user=$SOMETHING, ...) + message: | + This call will publish message to everyone on site, do you really want that? Specify doctype, docname, room or user. + languages: [python] + severity: ERROR + +- id: frappe-monkey-patching-not-allowed + patterns: + - pattern-either: + - pattern: | + from $X import $Y + ... + $Y.$PROPERTY = ... + - pattern: | + from $X import $Y + ... + $Y.$Z.$PROPERTY = ... + - pattern: | + from $X import $Y + ... + $Y.$Z.$F.$PROPERTY = ... + - pattern: | + from $X import $Y + ... + $Y.$Z.$F.$K.$PROPERTY = ... + - pattern: | + from $X import $Y + ... + $Y.$Z.$F.$K.$J.$PROPERTY = ... + - pattern: | + from $X import $Y + ... + $Y.$Z.$F.$K.$J.$L.$PROPERTY = ... + message: | + $PROPERTY being monkey patched by app. Use hooks provided by framework instead of patching behaviour at runtime. + paths: + exclude: + - "**/test_*.py" + languages: [python] + severity: ERROR + +- id: frappe-test-whitelist-missing-protection + patterns: + - pattern: | + @frappe.whitelist(...) + def $FUNC(...): + ... + - pattern-not: | + @$ANYTHING.whitelist_for_tests(...) + def $FUNC(...): + ... + paths: + include: + - "**/test_*.py" + - "**/frappe/tests/ui_test_helpers.py" + message: | + Test endpoints should use @whitelist_for_tests() instead of @frappe.whitelist() to ensure they're only accessible in test mode. + languages: [python] + severity: ERROR + fix: | + @whitelist_for_tests(...) diff --git a/.semgrep/report.yml b/.semgrep/report.yml new file mode 100644 index 0000000..9678bd7 --- /dev/null +++ b/.semgrep/report.yml @@ -0,0 +1,36 @@ +rules: +- id: frappe-missing-translate-function-in-report-python + paths: + include: + - "**/report" + - "report.py" + exclude: + - "**/regional" + - "**/test_*" + pattern-either: + - patterns: + - pattern: | + {..., "label": "...", ...} + - pattern-not: | + {..., "label": _("..."), ...} + - patterns: + - pattern: dict(..., label="...", ...) + - pattern-not: dict(..., label=_("..."), ...) + message: | + All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations + languages: [python] + severity: WARNING + +- id: frappe-translated-values-in-business-logic + paths: + include: + - "**/report" + patterns: + - pattern-inside: | + {..., filters: [...], ...} + - pattern: | + {..., options: [..., __("..."), ...], ...} + message: | + Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"} + languages: [javascript] + severity: WARNING diff --git a/.semgrep/security/authorization.yml b/.semgrep/security/authorization.yml new file mode 100644 index 0000000..f6ec83c --- /dev/null +++ b/.semgrep/security/authorization.yml @@ -0,0 +1,25 @@ +rules: +- id: relaxed-permissions + patterns: + - pattern: | + { + "role": "All", + ... + } + message: | + Avoid using "All" role. It's available to every user, including website user. + languages: [json] + severity: WARNING + +- id: frappe-setuser + patterns: + - pattern-either: + - pattern: frappe.set_user(...) + paths: + exclude: + - "**/test_*.py" + message: | + Detected the use of functions that can be dangerous if used incorrectly. + This code should be manually audited by security team. + languages: [python] + severity: WARNING diff --git a/.semgrep/security/filesystem.yml b/.semgrep/security/filesystem.yml new file mode 100644 index 0000000..ca6ad88 --- /dev/null +++ b/.semgrep/security/filesystem.yml @@ -0,0 +1,13 @@ +rules: +- id: frappe-security-file-traversal + patterns: + - pattern-either: + - pattern: open(...) + message: | + Detected the file system access. This code should be manually audited by + security team to avoid file traversal issues. + languages: [python] + severity: WARNING + paths: + exclude: + - "**/test_*.py" diff --git a/.semgrep/security/format_string_injection.yml b/.semgrep/security/format_string_injection.yml new file mode 100644 index 0000000..db29479 --- /dev/null +++ b/.semgrep/security/format_string_injection.yml @@ -0,0 +1,32 @@ +rules: +- id: frappe-format-string-injection + patterns: + - pattern-either: + - pattern: _("...").format(..., $ERR, ...) + - pattern: _("...").format($ERR, ...) + - pattern: _("...").format($ERR) + - pattern-not: _("...").format(..., str($ERR), ...) + - pattern-not: _("...").format(str($ERR), ...) + - pattern-not: _("...").format(str($ERR)) + - pattern-not: _("...").format(..., cstr($ERR), ...) + - pattern-not: _("...").format(cstr($ERR), ...) + - pattern-not: _("...").format(cstr($ERR)) + - pattern-not: _("...").format(..., repr($ERR), ...) + - pattern-not: _("...").format(repr($ERR), ...) + - pattern-not: _("...").format(repr($ERR)) + - metavariable-pattern: + metavariable: $ERR + pattern: $ERR + - pattern-inside: | + try: + ... + except $ETYPE as $ERR: + ... + message: | + Exception object passed directly to `.format()` on a translated string. + Use `str($ERR)` to convert the exception to a plain string first. + languages: [python] + severity: ERROR + paths: + exclude: + - "**/test_*.py" diff --git a/.semgrep/security/rce.yml b/.semgrep/security/rce.yml new file mode 100644 index 0000000..c5ce50e --- /dev/null +++ b/.semgrep/security/rce.yml @@ -0,0 +1,30 @@ +rules: +- id: frappe-codeinjection-eval + patterns: + - pattern-either: + - pattern: eval(...) + - pattern: exec(...) + - pattern: safe_exec(...) + - pattern: safe_eval(...) + message: | + Detected the use of functions that can be dangerous if used to evaluate + dynamic content. This code should be manually audited by security team. + languages: [python] + severity: ERROR + paths: + exclude: + - "**/test_*.py" + +- id: frappe-ssti + patterns: + - pattern-either: + - pattern: render_template($ARG, ...) + - pattern: frappe.render_template($ARG, ...) + message: | + Detected the use of render_template, make sure $ARG comes from trusted + source. This code should be audited by security team. + languages: [python] + severity: ERROR + paths: + exclude: + - "**/test_*.py" diff --git a/.semgrep/security/sql.yml b/.semgrep/security/sql.yml new file mode 100644 index 0000000..7b2cf49 --- /dev/null +++ b/.semgrep/security/sql.yml @@ -0,0 +1,25 @@ +rules: + - id: frappe-sql-format-injection + languages: [python] + severity: WARNING + message: >- + Detected use of '.format()' or f-string in a Frappe SQL call. + This can lead to SQL injection. Use parameterized queries instead. + patterns: + - pattern-either: + - pattern: frappe.db.sql("...".format(...), ...) + - pattern: frappe.db.sql(f"...", ...) + - pattern: frappe.db.multisql("...".format(...), ...) + - pattern: frappe.db.multisql(f"...", ...) + # Matches cases where the string is formatted beforehand + - pattern: | + $QUERY = "...".format(...) + ... + frappe.db.sql($QUERY, ...) + - pattern: | + $QUERY = f"..." + ... + frappe.db.sql($QUERY, ...) + paths: + exclude: + - "**/test_*.py" diff --git a/.semgrep/security/whitelisted.yml b/.semgrep/security/whitelisted.yml new file mode 100644 index 0000000..14ca331 --- /dev/null +++ b/.semgrep/security/whitelisted.yml @@ -0,0 +1,24 @@ +rules: + - id: missing-argument-type-hint + languages: [python] + severity: WARNING + patterns: + - pattern: | + @frappe.whitelist(...) + def $FUNC(..., $ARG, ...): ... + - pattern-not: | + @frappe.whitelist(...) + def $FUNC(..., $ARG: $TYPE, ...): ... + - metavariable-regex: + metavariable: $ARG + # Exclude 'self', 'cls', and '*' as they typically don't take hints + regex: ^(?!self$|cls$|\*$).*$ + message: "The argument '$ARG' in function '$FUNC' is missing a type hint." + - id: guest-whitelisted-method + languages: [python] + severity: WARNING + patterns: + - pattern: | + @frappe.whitelist(..., allow_guest=True, ...) + def $FUNC(...): ... + message: "Whitelisted method that's accesible to guest should be manually reviewed by security team." diff --git a/.semgrep/translate.yml b/.semgrep/translate.yml new file mode 100644 index 0000000..24dbb7b --- /dev/null +++ b/.semgrep/translate.yml @@ -0,0 +1,98 @@ +rules: +- id: frappe-translation-empty-string + pattern-either: + - pattern: _("") + - pattern: __("") + message: | + Empty string is useless for translation. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: [python, javascript, json] + severity: WARNING + +- id: frappe-translation-variable-only + pattern-either: + - pattern: _("{}", ...) + - pattern: _("{0}", ...) + - pattern: _("{0} {1}", ...) + - pattern: _("{0} {1} {2}", ...) + - pattern: __("{}", ...) + - pattern: __("{0}", ...) + - pattern: __("{0} {1}", ...) + - pattern: __("{0} {1} {2}", ...) + message: | + Tries to translate variables that are not known to the translators. It has the same effect as using the variables directly. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: [python, javascript, json] + severity: WARNING + +- id: frappe-translation-trailing-spaces + pattern-either: + - pattern: _("=~/(^[ \t]+|[ \t]+$)/") + - pattern: __("=~/(^[ \t]+|[ \t]+$)/") + message: | + Trailing or leading whitespace not allowed in translate strings. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: [python, javascript, json] + severity: WARNING + +- id: frappe-translation-python-formatting + pattern-either: + - pattern: _("..." % ...) + - pattern: _("...".format(...)) + - pattern: | + $VAR = "..." % ... + ... + _($VAR) + - pattern: | + $VAR = "...".format(...) + ... + _($VAR) + message: | + Only positional formatters are allowed and formatting should not be done before translating. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: [python] + severity: WARNING + +- id: frappe-translation-js-formatting + patterns: + - pattern: __(`...`) + - pattern-not: __("...") + message: | + Template strings are not allowed for text formatting. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: [javascript, json] + severity: WARNING + +- id: frappe-translation-python-splitting + pattern-either: + - pattern: _(...) + _(...) + - pattern: _("..." + "...") + - pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\` + message: | + Do not split strings inside translate function. Do not concatenate using translate functions. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: [python] + severity: WARNING + +- id: frappe-translation-js-splitting + pattern-either: + - pattern-regex: '__\([^\)]*[\\]\s+' + - pattern: __('...' + '...', ...) + - pattern: __('...') + __('...') + message: | + Do not split strings inside translate function. Do not concatenate using translate functions. + Please refer: https://frappeframework.com/docs/user/en/translations + languages: [javascript, json] + severity: WARNING + +- id: frappe-untranslated-label-in-translated-message + pattern: | + _("...", ...).format( + ..., + self.meta.get_label(...), + ... + ) + message: | + The label returned by `self.meta.get_label(...)` needs to be translated by wrapping it in `_()`. + languages: [python] + severity: WARNING diff --git a/.semgrep/ux.yml b/.semgrep/ux.yml new file mode 100644 index 0000000..d5a1ea7 --- /dev/null +++ b/.semgrep/ux.yml @@ -0,0 +1,57 @@ +rules: +- id: frappe-missing-translate-function-python + pattern-either: + - patterns: + - pattern: frappe.msgprint("...", ...) + - pattern-not: frappe.msgprint(_("..."), ...) + - patterns: + - pattern: frappe.throw("...", ...) + - pattern-not: frappe.throw(_("..."), ...) + - patterns: + - pattern: frappe.throw("..." + ..., ...) + - pattern-not: frappe.throw(_("...") + ..., ...) + - patterns: + - pattern: frappe.msgprint("..." + ..., ...) + - pattern-not: frappe.msgprint(_("...") + ..., ...) + - patterns: + - pattern: frappe.throw(..., title="...") + - pattern-not: frappe.throw(..., title=_("...")) + - patterns: + - pattern: frappe.msgprint(..., title="...") + - pattern-not: frappe.msgprint(..., title=_("...")) + message: | + All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations + languages: [python] + severity: WARNING + +- id: frappe-missing-translate-function-js + pattern-either: + - patterns: + - pattern: frappe.msgprint("...", ...) + - pattern-not: frappe.msgprint(__("..."), ...) + # ignore microtemplating e.g. msgprint("{{ _("server side translation") }}") + - pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...) + - patterns: + - pattern: frappe.throw("...", ...) + - pattern-not: frappe.throw(__("..."), ...) + # ignore microtemplating + - pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...) + - patterns: + - pattern: frappe.show_alert("...", ...) + - pattern-not: frappe.show_alert(__("..."), ...) + message: | + All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations + languages: [javascript] + severity: WARNING + +- id: frappe-missing-translation-button-text + pattern-either: + - patterns: + - pattern: frm.add_custom_button("...", ...) + - pattern-not: frm.add_custom_button(__("..."), ...) + # ignore microtemplating e.g. msgprint("{{ _("server side translation") }}") + - pattern-not: frm.add_custom_button("=~/\{\{.*\_.*\}\}/i", ...) + message: | + All user facing button text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations + languages: [javascript] + severity: WARNING diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100755 index 0000000..0f70a3b --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,128 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official email address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the project maintainer at **hello@kodlyft.com**. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series +of actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or +permanent ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within +the community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. + +Community Impact Guidelines were inspired by [Mozilla's code of conduct +enforcement ladder](https://github.com/mozilla/diversity). + +[homepage]: https://www.contributor-covenant.org + +For answers to common questions about this code of conduct, see the FAQ at +https://www.contributor-covenant.org/faq. Translations are available at +https://www.contributor-covenant.org/translations. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..76627ff --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,165 @@ +# Contributing to ERP Core + +Thank you for your interest in contributing to ERP Core! This guide will help you get started. + +## Table of Contents + +- [Code of Conduct](#code-of-conduct) +- [Getting Started](#getting-started) +- [Development Setup](#development-setup) +- [Making Changes](#making-changes) +- [Commit Guidelines](#commit-guidelines) +- [Pull Request Process](#pull-request-process) +- [Reporting Bugs](#reporting-bugs) +- [Requesting Features](#requesting-features) + +## Code of Conduct + +This project follows a [Code of Conduct](CODE_OF_CONDUCT.md). By participating, you are expected to uphold this code. + +## Getting Started + +1. **Fork** the repository on GitHub. +2. **Clone** your fork into your bench: + + ```bash + cd $PATH_TO_YOUR_BENCH + bench get-app https://github.com//erpcore --branch develop + bench --site your-site.com install-app erpcore + ``` + +3. **Add the upstream remote:** + + ```bash + cd apps/erpcore + git remote add upstream https://github.com/kodlyft/erpcore.git + ``` + +4. **Create a new branch** from `develop`: + + ```bash + git checkout -b feat/your-feature-name develop + ``` + +## Development Setup + +### Prerequisites + +| Dependency | Version | +| ---------------- | -------- | +| Python | >= 3.14 | +| Node.js | >= 24 | +| Frappe Framework | v16 | +| ERPNext | v16 (required) | + +### Pre-commit Hooks + +Install pre-commit hooks to ensure code quality before every commit: + +```bash +cd apps/erpcore +pip install pre-commit +pre-commit install +pre-commit install --hook-type commit-msg +``` + +### Backend Development + +```bash +cd $PATH_TO_YOUR_BENCH +bench start # Start Frappe development server +``` + +## Making Changes + +- Keep changes focused. A single PR should address a single concern. +- Write or update tests for any new functionality. +- Ensure all linters and tests pass before submitting: + + ```bash + # Python linting + ruff check erpcore/ + + # Run the app's unit tests + bench --site your-site.com run-tests --app erpcore + ``` + +## Commit Guidelines + +All commits **must** follow the [Conventional Commits](https://www.conventionalcommits.org/) specification. This enables automated versioning and changelog generation. + +### Format + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +### Allowed Types + +| Type | Description | +| ---------- | ---------------------------------------- | +| `feat` | A new feature | +| `fix` | A bug fix | +| `docs` | Documentation only changes | +| `style` | Formatting, missing semicolons, etc. | +| `refactor` | Code change that neither fixes nor adds | +| `perf` | Performance improvement | +| `test` | Adding or updating tests | +| `build` | Build system or dependency changes | +| `ci` | CI configuration changes | +| `chore` | Other changes that don't modify src/test | +| `revert` | Reverts a previous commit | + +### Examples + +``` +feat: add ZATCA phase 2 e-invoice QR generation +fix: correct VAT rounding on credit notes +docs: document tax category setup +refactor(vat): simplify tax template resolution +test: add unit tests for VAT return report +``` + +## Pull Request Process + +1. **Sync your branch** with the latest `develop`: + + ```bash + git fetch upstream + git rebase upstream/develop + ``` + +2. **Push** your branch and open a Pull Request against the `develop` branch. + +3. Fill out the PR template completely describe what changed and why. + +4. Ensure all CI checks pass (linting, semgrep, tests). + +5. A maintainer will review your PR. Address any feedback and push updates to the same branch. + +6. Once approved, a maintainer will merge your PR. + +## Reporting Bugs + +Use the [Bug Report](https://github.com/kodlyft/erpcore/issues/new?template=bug_report.md) issue template. Please include: + +- Steps to reproduce +- Expected vs. actual behavior +- Error logs / traceback if applicable +- Your environment (Frappe version, Python version, OS) + +## Requesting Features + +Use the [Feature Request](https://github.com/kodlyft/erpcore/issues/new?template=feature_request.md) issue template. Describe: + +- The problem you're trying to solve +- Your proposed solution +- Any alternatives you've considered + +--- + +Thank you for contributing to ERP Core! diff --git a/license.txt b/LICENSE similarity index 97% rename from license.txt rename to LICENSE index 8aa2645..66e5bd6 100644 --- a/license.txt +++ b/LICENSE @@ -1,6 +1,6 @@ MIT License -Copyright (c) [year] [fullname] +Copyright (c) 2026 Kodlyft Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index b3b17ec..c817a79 100644 --- a/README.md +++ b/README.md @@ -1,40 +1,98 @@ -### ERP Core +
-Core requirements for ERPNext +# ERP Core -### Installation +**Core requirements for [ERPNext](https://erpnext.com), built on the [Frappe Framework](https://frappeframework.com).** -You can install this app using the [bench](https://github.com/frappe/bench) CLI: +[![CI](https://github.com/kodlyft/erpcore/actions/workflows/ci.yml/badge.svg)](https://github.com/kodlyft/erpcore/actions/workflows/ci.yml) +[![Linters](https://github.com/kodlyft/erpcore/actions/workflows/linter.yml/badge.svg)](https://github.com/kodlyft/erpcore/actions/workflows/linter.yml) +[![Release](https://github.com/kodlyft/erpcore/actions/workflows/release.yml/badge.svg)](https://github.com/kodlyft/erpcore/actions/workflows/release.yml) +[![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE) +[![Conventional Commits](https://img.shields.io/badge/Conventional%20Commits-1.0.0-yellow.svg)](https://conventionalcommits.org) + +
+ +--- + +## Overview + +**ERP Core** is a Frappe app that packages shared, cross-cutting requirements used +across Kodlyft's ERPNext deployments, so they live in one versioned place instead of +being duplicated per site. + +> **Status: early development.** The app scaffolding, CI, and release automation are in +> place, but no functionality has shipped yet. Expect the surface area to change without +> notice until a `1.0` release. Watch the [releases](https://github.com/kodlyft/erpcore/releases) +> page for progress. + +## Requirements + +| Dependency | Version | +| ---------------- | --------------- | +| Frappe Framework | v16 | +| ERPNext | v16 (required) | +| Python | >= 3.14 | + +ERPNext is a hard dependency — it is declared in `required_apps`, so installing +`erpcore` on a site without ERPNext will fail. + +## Installation + +Install with the [bench](https://github.com/frappe/bench) CLI: ```bash cd $PATH_TO_YOUR_BENCH -bench get-app $URL_OF_THIS_REPO --branch develop -bench install-app erpcore +bench get-app https://github.com/kodlyft/erpcore --branch develop +bench --site your-site.com install-app erpcore ``` -### Contributing +## Development -This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository: +This app uses `pre-commit` for formatting and linting. After cloning: ```bash cd apps/erpcore +pip install pre-commit pre-commit install +pre-commit install --hook-type commit-msg ``` -Pre-commit is configured to use the following tools for checking and formatting your code: +Configured hooks: + +- **ruff**: Python linting, import sorting, and formatting +- **prettier**: JS / Vue / SCSS formatting +- **eslint**: JavaScript linting +- **Frappe semgrep rules**: vendored static-analysis rules (see [`.semgrep`](.semgrep)) +- **commitlint**: enforces [Conventional Commits](https://www.conventionalcommits.org/) + +Run the test suite: + +```bash +bench --site your-site.com run-tests --app erpcore +``` + +## Continuous Integration + +This repository ships a full CI/CD suite via GitHub Actions: + +| Workflow | Trigger | Purpose | +| -------- | ------- | ------- | +| [`ci.yml`](.github/workflows/ci.yml) | push to `develop`, PRs | Installs ERPNext + this app on a fresh bench and runs unit tests | +| [`linter.yml`](.github/workflows/linter.yml) | PRs | Pre-commit, Frappe semgrep rules, and `pip-audit` dependency scan | +| [`semantic-commits.yml`](.github/workflows/semantic-commits.yml) | PRs | Validates commit titles against Conventional Commits | +| [`release.yml`](.github/workflows/release.yml) | push to `develop` | Automated semantic versioning + GitHub releases | -- ruff -- eslint -- prettier -- pyupgrade -### CI +[Dependabot](.github/dependabot.yml) keeps GitHub Actions and Python dependencies up to date weekly. -This app can use GitHub Actions for CI. The following workflows are configured: +## Contributing -- CI: Installs this app and runs unit tests on every push to `develop` branch. -- Linters: Runs [Frappe Semgrep Rules](https://github.com/frappe/semgrep-rules) and [pip-audit](https://pypi.org/project/pip-audit/) on every pull request. +Contributions are welcome! Please read the [Contributing Guide](CONTRIBUTING.md) +and our [Code of Conduct](CODE_OF_CONDUCT.md) before opening a pull request. +- Report bugs with the [Bug Report](https://github.com/kodlyft/erpcore/issues/new?template=bug_report.md) template. +- Suggest features with the [Feature Request](https://github.com/kodlyft/erpcore/issues/new?template=feature_request.md) template. +- Found a security issue? See the [Security Policy](SECURITY.md). -### License +## License -mit +[MIT](LICENSE) © [Kodlyft](https://kodlyft.com) diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..d123d31 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,50 @@ +# Security Policy + +## Supported Versions + +| Version | Supported | +| ------- | ------------------ | +| 0.x | :white_check_mark: | + +## Reporting a Vulnerability + +We take the security of ERP Core seriously. If you discover a security vulnerability, please report it responsibly. + +**Do NOT open a public GitHub issue for security vulnerabilities.** + +Instead, please email **hello@kodlyft.com** with: + +- A description of the vulnerability +- Steps to reproduce the issue +- The potential impact +- Any suggested fix (if you have one) + +### What to Expect + +- **Acknowledgment**: We will acknowledge receipt of your report within **48 hours**. +- **Assessment**: We will assess the severity and impact within **5 business days**. +- **Resolution**: We aim to release a fix within **30 days** of confirmation, depending on complexity. +- **Credit**: We will credit you in the release notes (unless you prefer to remain anonymous). + +### Scope + +The following are in scope: + +- The ERP Core application code (Python backend) +- Authentication and authorization logic +- Data handling and storage +- API endpoints exposed by the app + +The following are out of scope: + +- Vulnerabilities in Frappe Framework or ERPNext (report these to their respective projects) +- Issues in third-party dependencies (report upstream, but let us know so we can update) +- Social engineering attacks +- Denial of service attacks + +## Security Best Practices for Deployers + +- Always run ERP Core behind HTTPS +- Keep Frappe, ERPNext, and ERP Core updated to the latest versions +- Use strong passwords and enable two-factor authentication +- Review user permissions and roles regularly diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100755 index 0000000..300da21 --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,13 @@ +module.exports = { + extends: ["@commitlint/config-conventional"], + rules: { + "subject-empty": [2, "never"], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"], + ], + }, +}; diff --git a/erpcore/__init__.py b/erpcore/__init__.py index f102a9c..5becc17 100644 --- a/erpcore/__init__.py +++ b/erpcore/__init__.py @@ -1 +1 @@ -__version__ = "0.0.1" +__version__ = "1.0.0" diff --git a/erpcore/hooks.py b/erpcore/hooks.py index 080ac09..c54444c 100644 --- a/erpcore/hooks.py +++ b/erpcore/hooks.py @@ -8,7 +8,7 @@ # Apps # ------------------ -# required_apps = [] +required_apps = ["erpnext"] # Each item in the list will be shown as an app in the apps page # add_to_apps_screen = [ @@ -255,4 +255,3 @@ # ------------ # List of apps whose translatable strings should be excluded from this app's translations. # ignore_translatable_strings_from = [] - diff --git a/erpcore/tests/__init__.py b/erpcore/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/erpcore/tests/test_erpcore.py b/erpcore/tests/test_erpcore.py new file mode 100644 index 0000000..8f29944 --- /dev/null +++ b/erpcore/tests/test_erpcore.py @@ -0,0 +1,15 @@ +# Copyright (c) 2026, Kodlyft and Contributors +# See LICENSE + +import unittest + +import frappe + + +class TestERPCore(unittest.TestCase): + def test_app_is_installed(self): + self.assertIn("erpcore", frappe.get_installed_apps()) + + def test_erpnext_is_available(self): + """erpcore declares erpnext in required_apps, so it must be installed alongside.""" + self.assertIn("erpnext", frappe.get_installed_apps()) diff --git a/package.json b/package.json new file mode 100644 index 0000000..4f4ced1 --- /dev/null +++ b/package.json @@ -0,0 +1,25 @@ +{ + "name": "erpcore", + "version": "0.0.1", + "description": "Core requirements for ERPNext, built on Frappe", + "author": "Kodlyft ", + "license": "MIT", + "private": true, + "repository": { + "type": "git", + "url": "https://github.com/kodlyft/erpcore.git" + }, + "bugs": { + "url": "https://github.com/kodlyft/erpcore/issues" + }, + "homepage": "https://github.com/kodlyft/erpcore#readme", + "keywords": [ + "erpcore", + "frappe", + "erpnext", + "core" + ], + "scripts": { + "prepare": "command -v pre-commit > /dev/null 2>&1 && pre-commit install && pre-commit install --hook-type commit-msg || true" + } +} diff --git a/pyproject.toml b/pyproject.toml index eaa4a96..e3cdae9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -6,11 +6,19 @@ authors = [ description = "Core requirements for ERPNext" requires-python = ">=3.14" readme = "README.md" +license = { text = "MIT" } dynamic = ["version"] +keywords = ["frappe", "erpnext", "erpcore", "core"] dependencies = [ # "frappe~=16.0.0" # Installed and managed by bench. ] +[project.urls] +Homepage = "https://github.com/kodlyft/erpcore" +Repository = "https://github.com/kodlyft/erpcore" +Issues = "https://github.com/kodlyft/erpcore/issues" +Changelog = "https://github.com/kodlyft/erpcore/releases" + [build-system] requires = ["flit_core >=3.4,<4"] build-backend = "flit_core.buildapi" @@ -19,6 +27,10 @@ build-backend = "flit_core.buildapi" [tool.bench.dev-dependencies] # package_name = "~=1.1.0" +[tool.bench.frappe-dependencies] +frappe = ">=16.0.0,<17.0.0" +erpnext = ">=16.0.0,<17.0.0" + # These apt dependencies will be installed from Ubuntu repositories when you host your app on Frappe Cloud [deploy.dependencies.apt] packages = []