diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml deleted file mode 100644 index 3d87d75b..00000000 --- a/.github/workflows/ci.yml +++ /dev/null @@ -1,113 +0,0 @@ - -name: CI - -on: - push: - branches: - - develop - pull_request: - -concurrency: - group: develop-csf_tz-${{ github.event.number }} - cancel-in-progress: true - -jobs: - tests: - runs-on: ubuntu-latest - strategy: - fail-fast: false - name: Server - - services: - redis-cache: - image: redis:alpine - ports: - - 13000:6379 - redis-queue: - image: redis:alpine - ports: - - 11000:6379 - mariadb: - image: mariadb:10.6 - env: - MYSQL_ROOT_PASSWORD: root - ports: - - 3306:3306 - options: --health-cmd="mariadb-admin ping" --health-interval=5s --health-timeout=2s --health-retries=3 - - steps: - - name: Clone - uses: actions/checkout@v4 - - - name: Find tests - run: | - echo "Finding tests" - grep -rn "def test" > /dev/null - - - name: Setup Python - uses: actions/setup-python@v5 - with: - python-version: '3.10' - - - name: Setup Node - uses: actions/setup-node@v4 - with: - node-version: 18 - check-latest: true - - - name: Cache pip - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - - name: Get yarn cache directory path - id: yarn-cache-dir-path - run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' - - - uses: actions/cache@v4 - id: yarn-cache - with: - path: ${{ steps.yarn-cache-dir-path.outputs.dir }} - key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }} - restore-keys: | - ${{ runner.os }}-yarn- - - - name: Install MariaDB Client - run: sudo apt-get install -y mariadb-client - - - name: Setup - run: | - pip install frappe-bench - bench init --skip-redis-config-generation --skip-assets --frappe-branch version-15 --python "$(which python)" ~/frappe-bench - mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" - mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" - - - name: Install - working-directory: /home/runner/frappe-bench - run: | - bench get-app --skip-assets payments --branch version-15 - bench get-app --skip-assets erpnext --branch version-15 --resolve-deps - bench get-app --skip-assets hrms --branch version-15 - bench get-app --skip-assets csf_tz $GITHUB_WORKSPACE --resolve-deps - bench setup requirements --dev - bench new-site --db-root-password root --admin-password admin test_site - bench --site test_site install-app payments - bench --site test_site install-app erpnext - bench --site test_site install-app hrms - bench --site test_site install-app csf_tz - env: - CI: 'Yes' - - - name: Smoke Test - working-directory: /home/runner/frappe-bench - run: | - bench --site test_site set-config allow_tests true - bench --site test_site execute erpnext.setup.utils.before_tests - bench --site test_site migrate - bench --site test_site list-apps - env: - TYPE: server diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 357aba35..50b5ccd7 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -1,4 +1,3 @@ - name: Linters on: @@ -9,51 +8,55 @@ permissions: contents: read concurrency: - group: ${{ github.workflow }}-${{ github.ref }} + group: linters-csf_tz-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} cancel-in-progress: true jobs: - linter: - name: 'Frappe Linter' + semgrep: + name: Frappe Linter runs-on: ubuntu-latest - if: github.event_name == 'pull_request' steps: - uses: actions/checkout@v4 with: fetch-depth: 0 + - uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: "3.11" cache: pip - - name: Install pre-commit - run: pip install pre-commit - - name: Run pre-commit on changed files - run: | - pre-commit run \ - --show-diff-on-failure \ - --color=always \ - --from-ref origin/${{ github.base_ref }} \ - --to-ref HEAD - name: Download Semgrep rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + - name: Install Semgrep + run: pip install semgrep + + # Blocking: real bugs and security issues only - name: Run Semgrep rules run: | - pip install semgrep - semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + semgrep scan --config ./frappe-semgrep-rules/rules \ + --config r/python.lang.security \ + --severity=ERROR --error csf_tz + + # Informational: style and i18n warnings, never fails the build + - name: Semgrep warnings (non-blocking) + if: always() + run: | + semgrep scan --config ./frappe-semgrep-rules/rules \ + --config r/python.lang.security \ + --severity=WARNING csf_tz || true deps-vulnerable-check: - name: 'Vulnerable Dependency Check' + name: Vulnerable Dependency Check runs-on: ubuntu-latest steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 with: - python-version: '3.10' - - - uses: actions/checkout@v4 + python-version: "3.11" - name: Cache pip uses: actions/cache@v4 @@ -67,5 +70,4 @@ jobs: - name: Install and run pip-audit run: | pip install pip-audit - cd ${GITHUB_WORKSPACE} pip-audit --desc on . diff --git a/.github/workflows/modernize-frappe.yml b/.github/workflows/modernize-frappe.yml new file mode 100644 index 00000000..e6e37596 --- /dev/null +++ b/.github/workflows/modernize-frappe.yml @@ -0,0 +1,12 @@ +name: Modernize Frappe Packaging + +on: + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + modernize: + uses: Aakvatech-Limited/frappe-maintenance/.github/workflows/modernize-frappe-reusable.yml@main diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 00000000..4cc2ffa1 --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,31 @@ +name: Pre-commit + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: precommit-csf_tz-${{ github.event_name }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + cache: pip + + - uses: actions/setup-node@v4 + with: + node-version: 20 + + - uses: pre-commit/action@v3.0.1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml deleted file mode 100644 index 3bc54c76..00000000 --- a/.github/workflows/release.yml +++ /dev/null @@ -1,34 +0,0 @@ -name: Release - -on: - workflow_dispatch: - -permissions: - contents: write - issues: write - pull-requests: write - -concurrency: - group: release-${{ github.ref }} - cancel-in-progress: true - -jobs: - release: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - with: - fetch-depth: 0 - persist-credentials: false - - - uses: actions/setup-node@v4 - with: - node-version: 20 - - - name: Install semantic-release - run: npm install --no-save semantic-release @semantic-release/changelog @semantic-release/exec @semantic-release/git @semantic-release/github conventional-changelog-conventionalcommits - - - name: Run semantic-release - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: npx semantic-release diff --git a/.github/workflows/semantic-commits.yml b/.github/workflows/semantic-commits.yml index 1370dfe8..7ec0d66a 100644 --- a/.github/workflows/semantic-commits.yml +++ b/.github/workflows/semantic-commits.yml @@ -14,6 +14,7 @@ jobs: commitlint: name: Check Commit Messages runs-on: ubuntu-latest + steps: - uses: actions/checkout@v4 with: diff --git a/.gitignore b/.gitignore index 119ee615..52faed98 100755 --- a/.gitignore +++ b/.gitignore @@ -59,3 +59,6 @@ build/ coverage/ *.lcov .nyc_output + +# Semgrep rules cloned by pre-commit / CI +frappe-semgrep-rules/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7e4f33eb..f5f05285 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,6 @@ -exclude: 'node_modules|.git' +exclude: "^(node_modules/|frappe-semgrep-rules/|[.]vscode/|.*/node_modules/|csf_tz/public/dist/|csf_tz/public/css/)" default_stages: [pre-commit] -default_install_hook_types: [pre-commit, commit-msg] +default_install_hook_types: [pre-commit, commit-msg, pre-push] fail_fast: false repos: @@ -8,37 +8,94 @@ repos: rev: v5.0.0 hooks: - id: trailing-whitespace - files: "csf_tz.*" - exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + exclude: '\.(json|txt|csv|md|svg)$' - id: end-of-file-fixer + exclude: '\.(json|csv|svg)$' - id: check-merge-conflict - id: check-ast - id: check-json - id: check-toml - id: check-yaml - id: debug-statements + - id: no-commit-to-branch + args: + - --branch + - main + - --branch + - master + - --branch + - production + - --branch + - version-14 + - --branch + - version-15 + - --branch + - version-16 - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.13.2 hooks: - id: ruff name: "Run ruff import sorter" args: ["--select=I", "--fix"] - + files: '^csf_tz/.*\.py$' - id: ruff name: "Run ruff linter" - + args: ["--fix"] + files: '^csf_tz/.*\.py$' - id: ruff-format name: "Run ruff formatter" + files: '^csf_tz/.*\.py$' + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v2.7.1 + hooks: + - id: prettier + name: "Run prettier on frontend sources" + types_or: [javascript, vue, css, scss] + files: '^(csf_tz|frontend)/.*\.(js|vue|css|scss)$' + exclude: | + (?x)^( + .*/public/dist/.*| + .*/public/frontend/.*| + .*/public/node_modules/.*| + .*\.bundle\.js| + .*\.min\.js| + frontend/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.* + )$ + + - repo: local + hooks: + - id: frappe-semgrep-rules + name: "Frappe Semgrep Security Rules" + entry: bash -c 'if [ ! -d frappe-semgrep-rules/.git ]; then rm -rf frappe-semgrep-rules && GIT_TEMPLATE_DIR="" git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules; fi && semgrep scan --config ./frappe-semgrep-rules/rules --config r/python.lang.security --severity=ERROR --error --quiet "$@"' -- + language: python + additional_dependencies: ["semgrep"] + types: [python] + files: '^csf_tz/.*\.py$' + pass_filenames: true + require_serial: true + + - id: full-repository-check + name: "Full repository check before push" + entry: bash -c 'if command -v pre-commit >/dev/null 2>&1; then exec pre-commit run --all-files --hook-stage pre-commit --show-diff-on-failure --color=always; else exec python3 -m pre_commit run --all-files --hook-stage pre-commit --show-diff-on-failure --color=always; fi' + language: system + stages: [pre-push] + pass_filenames: false + always_run: true + verbose: true - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.22.0 hooks: - id: commitlint stages: [commit-msg] - additional_dependencies: ['conventional-changelog-conventionalcommits'] + additional_dependencies: ["@commitlint/config-conventional"] ci: - autoupdate_schedule: weekly - skip: [] - submodules: false + autoupdate_schedule: weekly + skip: [frappe-semgrep-rules, full-repository-check] + submodules: false diff --git a/.releaserc.json b/.releaserc.json deleted file mode 100644 index e40d8fe7..00000000 --- a/.releaserc.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "branches": ["version-15"], - "plugins": [ - ["@semantic-release/commit-analyzer", { - "preset": "conventionalcommits" - }], - ["@semantic-release/release-notes-generator", { - "preset": "conventionalcommits" - }], - ["@semantic-release/changelog", { - "changelogFile": "CHANGELOG.md" - }], - ["@semantic-release/exec", { - "prepareCmd": "sed -i 's/^__version__ = .*/__version__ = \"${nextRelease.version}\"/' csf_tz/__init__.py" - }], - ["@semantic-release/git", { - "assets": ["CHANGELOG.md", "csf_tz/__init__.py"], - "message": "chore(release): ${nextRelease.version} [skip ci]\n\n${nextRelease.notes}" - }], - "@semantic-release/github" - ] -} diff --git a/.semgrepignore b/.semgrepignore new file mode 100644 index 00000000..1554660e --- /dev/null +++ b/.semgrepignore @@ -0,0 +1,13 @@ +# Semgrep ignore file. Creating it replaces semgrep's built-in defaults, so they are re-listed. +.git/ +node_modules/ +__pycache__/ +*.pyc +*.egg-info/ +dist/ +build/ +frappe-semgrep-rules/ + +# Test files +**/test_*.py +**/tests/ diff --git a/SPEC.md b/SPEC.md new file mode 100644 index 00000000..97763d4b --- /dev/null +++ b/SPEC.md @@ -0,0 +1,681 @@ +# CSF TZ Spec + +CSF TZ provides country-specific functionality for Tanzania on top of Frappe and ERPNext. + +The application extends standard ERPNext behaviour rather than replacing ERPNext. Tanzanian statutory requirements, integrations, local business rules, accounting extensions, payroll extensions, banking functionality, and other reusable Tanzania-specific functionality belong in CSF TZ when they cannot reasonably be implemented through standard ERPNext configuration. + +The application should preserve ERPNext conventions and upgradeability wherever possible. + +## Goals + +* Provide reusable Tanzania-specific functionality for ERPNext. +* Support Tanzanian statutory, taxation, fiscal, payroll, banking, regulatory, and business requirements. +* Extend standard ERPNext transactions without unnecessarily duplicating ERPNext functionality. +* Keep integrations with Tanzanian authorities, fiscal systems, banks, and payment providers isolated behind clear interfaces. +* Keep custom business logic deterministic, auditable, and maintainable. +* Make upgrades between supported Frappe and ERPNext versions predictable. +* Keep site-specific or customer-specific functionality outside the common CSF TZ application unless it is genuinely reusable. +* Prefer standard Frappe extension mechanisms over modifications to Frappe or ERPNext source code. + +## Application Model + +CSF TZ is an extension application running inside a Frappe/ERPNext site. + +The important architectural areas are: + +* **DocTypes** own persistent CSF TZ business entities and configuration. +* **Hooks** connect CSF TZ behaviour to Frappe and ERPNext lifecycle events. +* **Overrides** replace standard document controller behaviour only when extension through hooks is insufficient. +* **Client scripts and bundled JavaScript** extend standard Desk behaviour. +* **APIs** expose explicitly supported server-side operations and integrations. +* **Integrations** communicate with banks, payment providers, fiscal systems, government authorities, and other external services. +* **Scheduled jobs** perform recurring reconciliation, synchronization, notification, regulatory, and maintenance work. +* **Patches** perform controlled schema, metadata, configuration, and data migrations. +* **Reports** expose business, accounting, operational, and statutory information. +* **Workspaces** provide user-facing entry points into Tanzania-specific functionality. + +Business rules should live as close as possible to the domain that owns them. + +Do not place substantial business logic in `hooks.py`. Hooks should primarily map framework events to appropriately grouped implementation functions. + +## Functional Domains + +The application may contain functionality covering areas including: + +* Tanzania tax and fiscal compliance +* VFD/EFD integrations +* Sales and receivables extensions +* Purchasing and payables extensions +* Withholding taxes +* Banking and reconciliation +* Payroll and employee-related localization +* Inventory and stock controls +* Importation and landed-cost processes +* Payment provider integrations +* Tanzanian geographic and regulatory data +* Vehicle and authority integrations +* Education-related extensions where required by supported deployments +* Operational utilities and reusable ERPNext enhancements + +A feature does not belong in CSF TZ merely because it was developed for a Tanzanian customer. + +New functionality should normally satisfy at least one of these conditions: + +1. It implements a Tanzanian statutory or regulatory requirement. +2. It integrates with a Tanzania-specific service or institution. +3. It represents a business requirement broadly reusable by CSF TZ installations. +4. It provides infrastructure required by another legitimate CSF TZ feature. + +Customer-specific workflows, reports, integrations, fields, naming conventions, or business rules should normally live in a customer-specific application. + +## Extension Model + +Use Frappe's standard extension mechanisms in this order of preference: + +1. Configuration and standard ERPNext functionality +2. Custom fields and property setters managed by the application +3. Document events +4. Client-side DocType extensions +5. Whitelisted methods and APIs +6. Scheduler events +7. Controller extension or override where required + +Direct modification of Frappe or ERPNext source code is not part of the CSF TZ architecture. + +### Document Events + +Use `doc_events` when logic belongs to a standard Frappe or ERPNext document lifecycle. + +Event handlers should: + +* receive the document and event using standard Frappe conventions; +* perform one clearly identifiable business responsibility; +* avoid duplicating ERPNext controller logic; +* avoid committing or rolling back database transactions independently unless specifically required; +* raise meaningful validation errors when a transaction cannot proceed; +* remain safe when called during normal framework lifecycle processing. + +Large handlers should delegate to domain-specific modules. + +### Controller Overrides + +Controller overrides are a high-impact extension mechanism. + +Use `override_doctype_class` only when the required behaviour cannot safely be implemented through events or supported extension points. + +An override should inherit from the corresponding upstream controller wherever practical. + +When overriding a standard controller: + +* preserve upstream behaviour unless the specification explicitly changes it; +* call the superclass implementation where appropriate; +* document why an override is required; +* consider upstream changes during every major ERPNext upgrade; +* keep the override narrowly scoped. + +Controller overrides should not become independent copies of ERPNext controllers. + +## Client-Side Extensions + +JavaScript attached through `doctype_js`, `doctype_list_js`, application bundles, or other Frappe hooks should enhance the standard UI rather than reproduce server-side business logic. + +Client-side code may: + +* improve data entry; +* provide validations for user convenience; +* calculate previews; +* add buttons and actions; +* call approved server methods; +* adapt standard forms to CSF TZ workflows. + +Business-critical validation must also exist server-side. + +Never rely only on browser-side validation for accounting, compliance, authorization, statutory, or data-integrity controls. + +## API Model + +Server APIs should be grouped by domain rather than accumulating unrelated behaviour in large generic modules. + +New APIs should preferably live in a dedicated package or domain module. + +Whitelisted methods must explicitly consider: + +* authentication; +* authorization; +* input validation; +* document permissions; +* idempotency; +* transaction boundaries; +* external-service failures; +* logging; +* exposure of confidential data. + +Do not make a method guest-accessible unless anonymous access is a genuine integration requirement. + +Public or integration-facing APIs should have stable request and response contracts. + +Breaking API changes should be treated as compatibility changes. + +## Integration Model + +External systems should be treated as unreliable network dependencies. + +Integrations may include: + +* VFD/EFD providers; +* TRA-related services; +* banks; +* payment gateways; +* SFTP endpoints; +* vehicle and licensing authorities; +* regulatory services; +* other approved third-party systems. + +Integration code should separate: + +1. configuration; +2. authentication; +3. request construction; +4. transport; +5. response parsing; +6. business processing; +7. retry/reconciliation behaviour; +8. logging. + +Provider-specific behaviour should remain inside provider-specific modules wherever possible. + +Do not spread provider-specific conditionals throughout Sales Invoice, Payment Entry, Payroll Entry, or other unrelated domains. + +### Credentials + +Credentials, tokens, private keys, API secrets, passwords, and similar material must never be hard-coded in source files. + +Use Frappe configuration or password fields appropriate to the sensitivity of the credential. + +Logs must not expose credentials or sensitive authentication material. + +### External Calls + +External calls performed during document submission should be used carefully. + +Where an external operation can safely occur asynchronously, prefer a background or reconciliation process rather than making the external provider's availability a prerequisite for completing an ERPNext transaction. + +Where synchronous communication is legally or operationally required, failure behaviour must be explicit. + +## VFD and Fiscal Processing + +Fiscal processing is compliance-sensitive functionality. + +VFD functionality should maintain a clear distinction between: + +* ERPNext transaction state; +* fiscal submission state; +* provider request state; +* provider response state; +* retries; +* successful fiscalization; +* failure; +* cancellation or reversal. + +A Sales Invoice being submitted in ERPNext does not by itself prove successful fiscal submission. + +Fiscal operations should preserve enough information to determine: + +* what was submitted; +* when it was submitted; +* which provider was used; +* what response was received; +* whether the operation succeeded; +* whether retry is required; +* whether subsequent cancellation or adjustment occurred. + +Provider communication and fiscal business rules should be kept separate wherever practical. + +## Accounting Integrity + +Any CSF TZ functionality that creates or alters accounting consequences must respect ERPNext's accounting model. + +Examples include: + +* withholding tax; +* bank charges; +* exchange differences; +* landed costs; +* import tracking; +* additional salary accounting; +* payment integrations. + +Accounting logic must: + +* use submitted documents where ERPNext requires submission; +* preserve company and currency context; +* preserve debit/credit integrity; +* respect cancellation; +* avoid orphan accounting references; +* avoid duplicate GL consequences; +* remain reproducible from the underlying business transaction. + +Do not update accounting tables directly when an ERPNext document or accounting API should own the transaction. + +## Scheduled Jobs + +Recurring processing is registered through Frappe scheduler hooks. + +Scheduled jobs are appropriate for work including: + +* synchronization; +* reconciliation; +* retries; +* token renewal; +* regulatory data refreshes; +* notifications; +* queue seeding; +* periodic cleanup; +* maintenance; +* delayed transaction processing. + +Scheduler methods must be safe to execute repeatedly. + +Where possible they should be idempotent: running the same job again should not create duplicate financial, regulatory, or operational consequences. + +A scheduled job should not assume that the previous invocation completed successfully. + +Jobs processing potentially large datasets should operate in bounded batches. + +Do not load an unbounded number of documents into memory. + +Failures affecting one record should not unnecessarily prevent all other independent records from processing. + +## Background Work + +Operations involving significant network communication, file processing, large datasets, or long-running calculations should normally use Frappe background jobs. + +Queue work when synchronous execution would: + +* make a user transaction unnecessarily slow; +* risk HTTP timeouts; +* depend on unreliable third-party services; +* process large numbers of records; +* perform retryable work. + +Background jobs must receive enough identifiers to reload authoritative state rather than depending on stale in-memory documents. + +## Configuration + +CSF TZ configuration should use Frappe DocTypes or supported site configuration. + +Configuration belongs at the narrowest appropriate scope: + +* system-wide; +* company; +* provider; +* bank; +* fiscal device; +* user; +* transaction. + +Do not introduce global settings for configuration that legitimately varies by Company. + +Configuration fields should have clear defaults and should fail explicitly when mandatory configuration is missing. + +Settings DocTypes should be preferred over scattered custom fields when a feature has substantial configuration of its own. + +## Custom Fields and Property Setters + +CSF TZ may extend standard DocTypes using Custom Fields and Property Setters. + +Application-owned metadata must be reproducible from source. + +Do not rely on production sites containing manually created Custom Fields that are absent from application setup or migration logic. + +Field creation must be idempotent. + +Before changing or deleting existing fields, account for installations that may already contain data. + +Fieldnames should be stable after release wherever possible. + +## Data Model + +A CSF TZ DocType should exist when a concept has an independent lifecycle, configuration role, transactional role, integration role, or audit requirement. + +Do not create a new DocType merely to avoid using an appropriate ERPNext model. + +Links to ERPNext documents should use proper Link or Dynamic Link fields wherever possible. + +Child tables should be used for records that exist only as part of their parent document. + +Integration logs should retain identifiers required to trace the corresponding ERPNext transaction and external transaction. + +## Migrations and Patches + +Database and metadata migrations are part of the application contract. + +Use `patches.txt` for one-time migration work. + +Use install or migrate hooks for operations that genuinely need to remain repeatable. + +A patch should: + +* be safe for existing production data; +* be deterministic; +* preferably be idempotent; +* avoid assumptions about optional modules or data; +* handle already-migrated records safely; +* avoid silently destroying business data; +* complete in reasonable bounded operations. + +Do not rewrite the behaviour of a previously released patch after installations may already have executed it. + +Create a new patch for subsequent corrections. + +Destructive migrations require particular care and should be explicitly documented. + +## Installation and Migration Hooks + +`after_install` prepares newly installed sites. + +`after_migrate` may enforce application-owned metadata or configuration that must remain synchronized. + +Do not put expensive recurring business processing in migration hooks. + +Migration hooks must not rely on external services being available. + +A failed external provider must not prevent a normal `bench migrate` unless that provider is fundamentally required to make the schema valid. + +## Version Compatibility + +Each maintained branch must explicitly declare the supported Frappe and ERPNext major versions in `pyproject.toml`. + +A branch should target a defined framework generation. + +Do not make one branch silently support incompatible framework majors through extensive version-condition logic. + +Compatibility changes involving: + +* controller APIs; +* DocType fields; +* hooks; +* accounting behaviour; +* scheduler behaviour; +* framework APIs; +* JavaScript APIs + +must be checked against the targeted Frappe and ERPNext versions. + +Upstream APIs should not be assumed stable across major releases. + +## Modules + +Functional modules should group related business behaviour. + +Current module boundaries may include areas such as: + +* CSF TZ +* Purchase and Stock Management +* Sales and Marketing +* Meal Count +* Stanbic +* KCB +* VFD Providers +* VFD Settings + +New modules should only be introduced when they represent a coherent functional domain. + +Do not create a module for every small feature. + +## Public Surfaces + +The important public surfaces of CSF TZ include: + +* DocTypes +* reports +* workspaces +* whitelisted methods +* hooks into ERPNext documents +* scheduled jobs +* integrations consumed by external systems +* configuration DocTypes +* print and Jinja helpers where explicitly exposed + +Changes to these surfaces may affect installed sites even when no Python import API changes. + +Treat fieldnames, DocType names, integration contracts, and externally consumed endpoints as compatibility-sensitive. + +## Permissions and Authorization + +Server-side permission checks remain authoritative. + +Creating a custom form button does not grant permission to perform the corresponding operation. + +APIs that read or modify ERPNext documents must respect Frappe permissions unless the integration explicitly requires privileged system processing. + +Any deliberate permission bypass must: + +* have a documented reason; +* be scoped narrowly; +* validate the caller or integration; +* avoid accepting arbitrary document access from untrusted input. + +## Security Model + +CSF TZ runs with the privileges of the Frappe application process and has access to site data. + +Application code therefore belongs inside the site's trusted computing boundary. + +Assume that server-side CSF TZ code can potentially access: + +* accounting information; +* customer and supplier records; +* employee information; +* payroll information; +* integration credentials; +* regulatory records; +* uploaded files. + +From this: + +* validate untrusted input; +* avoid arbitrary SQL construction; +* avoid arbitrary filesystem access; +* do not execute user-supplied code; +* protect integration credentials; +* restrict guest endpoints; +* validate uploaded files; +* avoid logging unnecessary personal or financial information. + +External responses must be treated as untrusted input. + +## SQL and Database Access + +Prefer Frappe ORM, Query Builder, and standard document APIs. + +Direct SQL is acceptable when there is a clear technical reason such as reporting, performance, migration, or functionality not reasonably expressible through supported APIs. + +Direct SQL must: + +* parameterize dynamic values; +* respect `docstatus` where relevant; +* consider Company boundaries; +* consider permissions when used in user-facing operations; +* avoid direct writes to framework-owned accounting or stock ledgers unless explicitly required by framework architecture. + +Database writes should normally occur through document APIs. + +## Error Handling + +Errors shown to users should explain the business problem and, where possible, the corrective action. + +Do not expose raw provider credentials, tokens, SQL, or internal stack details through user-facing errors. + +Integration errors should preserve enough technical information in appropriate logs for diagnosis. + +Retryable errors should be distinguishable from permanent validation failures. + +## Logging and Auditability + +Compliance-sensitive and integration-sensitive operations should be traceable. + +Where appropriate, preserve: + +* source document; +* external reference; +* timestamp; +* provider; +* operation; +* result; +* error; +* retry information. + +Do not use unrestricted console output as the primary production logging mechanism. + +Use Frappe logging, integration log DocTypes, or purpose-built audit records. + +## Cancellation and Reversal + +Any feature that creates downstream records must explicitly consider cancellation. + +When a source ERPNext document is cancelled, CSF TZ must determine whether downstream records should: + +* be cancelled; +* be reversed; +* be unlinked; +* remain as immutable audit evidence; +* trigger an external cancellation; +* require manual intervention. + +Cancellation logic must not silently leave active financial or compliance consequences behind. + +## Idempotency + +Operations that may be retried must protect against duplicate execution. + +This particularly applies to: + +* scheduled jobs; +* webhook/API callbacks; +* fiscal submissions; +* payment processing; +* bank reconciliation; +* journal creation; +* background jobs; +* authority synchronization. + +Where an external system provides a transaction identifier, persist and use it for duplicate detection when practical. + +## Performance + +Code running in transaction hooks must remain bounded. + +Avoid: + +* queries inside large loops; +* loading complete tables unnecessarily; +* performing expensive external calls repeatedly; +* processing entire transaction histories during ordinary document validation; +* synchronous bulk processing where a background job is appropriate. + +Use batching for high-volume scheduled operations. + +Performance optimizations must not compromise accounting or compliance correctness. + +## Testing + +Business-critical features should have automated tests. + +Priority areas include: + +* accounting consequences; +* taxation; +* VFD/fiscalization; +* payroll calculations; +* document submission and cancellation; +* integration request/response handling; +* migration patches; +* scheduled job idempotency; +* duplicate prevention. + +Tests should exercise business outcomes rather than merely whether a function executes. + +Where an external provider is involved, provider calls should normally be mocked in automated tests. + +Tests must not depend on live banking, fiscal, payment, or authority services. + +## Development Rules + +When changing existing functionality: + +1. Identify the owning domain. +2. Check existing hooks and overrides before adding another extension point. +3. Reuse ERPNext behaviour where possible. +4. Preserve submission and cancellation semantics. +5. Consider multi-company behaviour. +6. Consider permissions. +7. Consider migration requirements. +8. Consider scheduled or asynchronous execution. +9. Consider integration retry and duplicate behaviour. +10. Add or update tests for material business logic. + +Avoid adding unrelated convenience functions to `custom_api.py` or other already broad modules. + +New substantial features should use dedicated domain modules. + +## Naming + +Use names that describe the business concept rather than a customer or temporary implementation. + +Provider-specific functionality may use the provider name where the provider itself defines the integration. + +Avoid abbreviations unless they are established domain terminology such as VAT, VFD, TRA, PAYE, or NSSF. + +Do not encode one customer's name into reusable CSF TZ business logic. + +## Source of Truth + +For application behaviour: + +* Python source is the source of truth for server-side logic. +* JavaScript source is the source of truth for client-side behaviour. +* DocType JSON is the source of truth for application-owned DocType metadata. +* patch modules and migration hooks are the source of truth for migrations. +* `hooks.py` is the source of truth for registered framework extensions and schedules. +* `pyproject.toml` is the source of truth for Python and Frappe/ERPNext compatibility declarations. + +Production-site manual customizations are not substitutes for source-controlled application behaviour. + +## Contribution Boundary + +Before adding functionality to CSF TZ, ask: + +**Is this Tanzania-specific or reusable across a substantial number of CSF TZ installations?** + +If no, it probably belongs in: + +* standard ERPNext configuration; +* another reusable application; +* an industry-specific application; or +* a customer-specific application. + +CSF TZ should not become a collection of unrelated customer customizations. + +## Documentation Map + +Documentation should progressively cover: + +* Architecture +* Installation and upgrade +* Tanzanian statutory configuration +* VFD configuration and providers +* Tax and withholding configuration +* Banking integrations +* Payroll localization +* Purchase and import processes +* Scheduled jobs +* API and integration contracts +* Migration and compatibility guidance +* Troubleshooting + +`SPEC.md` defines architectural and development rules. + +`README.md` should remain the high-level introduction and installation entry point. + +Detailed operational and developer documentation should live under `docs/` as the repository grows. diff --git a/commitlint.config.js b/commitlint.config.js index 56702092..300da21e 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,5 +1,5 @@ module.exports = { - parserPreset: "conventional-changelog-conventionalcommits", + extends: ["@commitlint/config-conventional"], rules: { "subject-empty": [2, "never"], "type-case": [2, "always", "lower-case"], diff --git a/csf_tz/__init__.py b/csf_tz/__init__.py index 95acbb0d..3f1c0388 100755 --- a/csf_tz/__init__.py +++ b/csf_tz/__init__.py @@ -19,11 +19,17 @@ def load_monkey_patches(): if patches_loaded: return - patches_loaded = True + # Bench-level commands such as asset builds can run without a site context. + # Avoid querying installed apps in that case, because it attempts a database + # connection and fails with "site must be fully initialized, db_name missing". + if not getattr(frappe.local, "site", None): + return if app_name not in frappe.get_installed_apps(): return + patches_loaded = True + for module_name in os.listdir(frappe.get_app_path(app_name, "monkey_patches")): if not module_name.endswith(".py") or module_name == "__init__.py": continue diff --git a/csf_tz/check-all-git.sh b/csf_tz/check-all-git.sh index 5f9aaf50..21342bbf 100755 --- a/csf_tz/check-all-git.sh +++ b/csf_tz/check-all-git.sh @@ -8,4 +8,3 @@ for dir in apps/* ; do cd ../.. fi done - diff --git a/csf_tz/csf_tz/additional_salary.js b/csf_tz/csf_tz/additional_salary.js index 1b6318f5..26dacbe6 100644 --- a/csf_tz/csf_tz/additional_salary.js +++ b/csf_tz/csf_tz/additional_salary.js @@ -1,47 +1,50 @@ -frappe.ui.form.on('Additional Salary', { - refresh: function(frm) { - cur_frm.add_custom_button(__("Generate Additional Salary Records"), function() { - frappe.call({ - method: "csf_tz.csftz_hooks.additional_salary.generate_additional_salary_records", - args: {}, - callback: function () { - cur_frm.reload_doc(); - } - }); - }); - }, - payroll_date: function(frm) { - if (!frm.doc.payroll_date) { - frm.set_value("no_of_hours", null); - } - }, - employee: function(frm) { - if (!frm.doc.employee) { - frm.set_value("no_of_hours", null); - } - }, - salary_component: function(frm) { - if (!frm.doc.salary_component) { - frm.set_value("based_on_hourly_rate", null); - frm.set_value("hourly_rate", null); - } - }, - no_of_hours: function(frm) { - if (frm.doc.employee && frm.doc.payroll_date) { - frappe.call({ - method: "csf_tz.csftz_hooks.additional_salary.get_employee_base_salary_in_hours", - args: { - employee: frm.doc.employee, - payroll_date: frm.doc.payroll_date - }, - async: false, - callback: function(r) { - console.log(r.message) - if(r.message) { - frm.set_value("amount", frm.doc.hourly_rate / 100 * frm.doc.no_of_hours * r.message.base_salary_in_hours); - } - } - }); - } - }, -}); +frappe.ui.form.on("Additional Salary", { + refresh: function (frm) { + cur_frm.add_custom_button(__("Generate Additional Salary Records"), function () { + frappe.call({ + method: "csf_tz.csftz_hooks.additional_salary.generate_additional_salary_records", + args: {}, + callback: function () { + cur_frm.reload_doc(); + }, + }); + }); + }, + payroll_date: function (frm) { + if (!frm.doc.payroll_date) { + frm.set_value("no_of_hours", null); + } + }, + employee: function (frm) { + if (!frm.doc.employee) { + frm.set_value("no_of_hours", null); + } + }, + salary_component: function (frm) { + if (!frm.doc.salary_component) { + frm.set_value("based_on_hourly_rate", null); + frm.set_value("hourly_rate", null); + } + }, + no_of_hours: function (frm) { + if (frm.doc.employee && frm.doc.payroll_date) { + frappe.call({ + method: "csf_tz.csftz_hooks.additional_salary.get_employee_base_salary_in_hours", + args: { + employee: frm.doc.employee, + payroll_date: frm.doc.payroll_date, + }, + async: false, + callback: function (r) { + console.log(r.message); + if (r.message) { + frm.set_value( + "amount", + (frm.doc.hourly_rate / 100) * frm.doc.no_of_hours * r.message.base_salary_in_hours + ); + } + }, + }); + } + }, +}); diff --git a/csf_tz/csf_tz/bank_reconciliation.js b/csf_tz/csf_tz/bank_reconciliation.js index fec5229c..926b8ed5 100644 --- a/csf_tz/csf_tz/bank_reconciliation.js +++ b/csf_tz/csf_tz/bank_reconciliation.js @@ -1,21 +1,20 @@ -frappe.ui.form.on('Bank Reconciliation', { - get_payment_entries: function (frm) { - frappe.call({ - method: 'erpnext.accounts.utils.get_balance_on', - args: { - account: frm.doc.account, - date: frappe.datetime.add_days(frm.doc.from_date, -1), - }, - async: false, - callback: function (r) { - if (r.message) { - frm.set_value("opening_balance", r.message || 0); - } - else { - frm.set_value("opening_balance", 0); - } - } - }); - frm.set_value("closing_balance", frm.doc.total_amount + frm.doc.opening_balance); - }, +frappe.ui.form.on("Bank Reconciliation", { + get_payment_entries: function (frm) { + frappe.call({ + method: "erpnext.accounts.utils.get_balance_on", + args: { + account: frm.doc.account, + date: frappe.datetime.add_days(frm.doc.from_date, -1), + }, + async: false, + callback: function (r) { + if (r.message) { + frm.set_value("opening_balance", r.message || 0); + } else { + frm.set_value("opening_balance", 0); + } + }, + }); + frm.set_value("closing_balance", frm.doc.total_amount + frm.doc.opening_balance); + }, }); diff --git a/csf_tz/csf_tz/bom_addittional_costs.js b/csf_tz/csf_tz/bom_addittional_costs.js index 60b1007e..d94041bb 100644 --- a/csf_tz/csf_tz/bom_addittional_costs.js +++ b/csf_tz/csf_tz/bom_addittional_costs.js @@ -1,22 +1,21 @@ frappe.ui.form.on("BOM", { - refresh: function (frm) { - frm.set_query("expense_account", "additional_costs", function () { - return { - filters: { - account_type: [ - "in", - [ - "Tax", - "Chargeable", - "Income Account", - "Expenses Included In Valuation", - "Expenses Included In Asset Valuation", - ], - ], - company: frm.doc.company, - }, - }; - }); - }, - + refresh: function (frm) { + frm.set_query("expense_account", "additional_costs", function () { + return { + filters: { + account_type: [ + "in", + [ + "Tax", + "Chargeable", + "Income Account", + "Expenses Included In Valuation", + "Expenses Included In Asset Valuation", + ], + ], + company: frm.doc.company, + }, + }; + }); + }, }); diff --git a/csf_tz/csf_tz/company.js b/csf_tz/csf_tz/company.js index 3de366b9..580d746e 100644 --- a/csf_tz/csf_tz/company.js +++ b/csf_tz/csf_tz/company.js @@ -1,153 +1,165 @@ frappe.ui.form.on("Company", { - - setup: function(frm) { - frm.set_query("default_withholding_payable_account", function() { + setup: function (frm) { + frm.set_query("default_withholding_payable_account", function () { return { - "filters": { - "company": frm.doc.name, - "account_type": "Payable", - } + filters: { + company: frm.doc.name, + account_type: "Payable", + }, }; }); - frm.set_query("default_withholding_receivable_account", function() { + frm.set_query("default_withholding_receivable_account", function () { return { - "filters": { - "company": frm.doc.name, - "account_type": "Receivable", - } + filters: { + company: frm.doc.name, + account_type: "Receivable", + }, }; }); - frm.set_query("fee_bank_account", function() { + frm.set_query("fee_bank_account", function () { return { - "filters": { - "company": frm.doc.name, - "account_type": ["in",["Cash","Bank"]], - "account_currency": frm.doc.default_currency, - } + filters: { + company: frm.doc.name, + account_type: ["in", ["Cash", "Bank"]], + account_currency: frm.doc.default_currency, + }, }; }); - frm.set_query("student_applicant_fees_revenue_account", function() { + frm.set_query("student_applicant_fees_revenue_account", function () { return { - "filters": { - "company": frm.doc.name, - "account_type": "Income Account", - "account_currency": frm.doc.default_currency, - } + filters: { + company: frm.doc.name, + account_type: "Income Account", + account_currency: frm.doc.default_currency, + }, }; - }); + }); }, - - refresh: function(frm) { - frm.add_custom_button(__('Auto create accounts'), function() { - frm.trigger("auto_create_account"); - }, __("Setup")); - frm.add_custom_button(__('create Item Tax Template'), function() { - frm.trigger("create_tax_template"); - }, __("Setup")); - frm.add_custom_button(__('Create Tax Category'), function() { - frm.trigger("make_tax_category"); - }, __("Setup")); - frm.add_custom_button(__('Create Salary Component'), function() { - frm.trigger("make_salary_components_and_structure"); - }, __("Setup")); - frm.add_custom_button(__('Link Item Tax Template'), function() { - let d = new frappe.ui.Dialog({ - title: 'Enter details', - fields: [ - { - fieldtype: 'Link', - options: 'Item Tax Template', - label: __('Item Tax Category'), - fieldname: 'default_tax_template', - reqd: 1 - } - ], - primary_action_label: 'Submit', - primary_action(values) { - console.log(values); - - frappe.call({ - method: 'csf_tz.custom_api.linking_tax_template', - args: { - abbr: frm.doc.abbr, - doctype: 'Item', - default_tax_template: { - default_tax_template: values.default_tax_template - } + refresh: function (frm) { + frm.add_custom_button( + __("Auto create accounts"), + function () { + frm.trigger("auto_create_account"); + }, + __("Setup") + ); + frm.add_custom_button( + __("create Item Tax Template"), + function () { + frm.trigger("create_tax_template"); + }, + __("Setup") + ); + frm.add_custom_button( + __("Create Tax Category"), + function () { + frm.trigger("make_tax_category"); + }, + __("Setup") + ); + frm.add_custom_button( + __("Create Salary Component"), + function () { + frm.trigger("make_salary_components_and_structure"); + }, + __("Setup") + ); + frm.add_custom_button( + __("Link Item Tax Template"), + function () { + let d = new frappe.ui.Dialog({ + title: "Enter details", + fields: [ + { + fieldtype: "Link", + options: "Item Tax Template", + label: __("Item Tax Category"), + fieldname: "default_tax_template", + reqd: 1, }, - callback: function(response) { - if (response.message) { - frappe.msgprint(__('Item Tax Template Linked successfully.')); - } - } - }); - - d.hide(); - } - }); + ], + primary_action_label: "Submit", + primary_action(values) { + console.log(values); - d.show(); - }, __("Setup")); + frappe.call({ + method: "csf_tz.custom_api.linking_tax_template", + args: { + abbr: frm.doc.abbr, + doctype: "Item", + default_tax_template: { + default_tax_template: values.default_tax_template, + }, + }, + callback: function (response) { + if (response.message) { + frappe.msgprint(__("Item Tax Template Linked successfully.")); + } + }, + }); + d.hide(); + }, + }); + d.show(); + }, + __("Setup") + ); }, - auto_create_account: function(frm) { + auto_create_account: function (frm) { frappe.call({ - method: 'csf_tz.custom_api.auto_create_account', - args:{ - abbr: frm.doc.abbr - + method: "csf_tz.custom_api.auto_create_account", + args: { + abbr: frm.doc.abbr, }, - callback: function(response) { + callback: function (response) { if (response.message) { - frappe.msgprint(__('Accounts created successfully.')); + frappe.msgprint(__("Accounts created successfully.")); } - } - }) + }, + }); }, - create_tax_template: function(frm) { + create_tax_template: function (frm) { frappe.call({ - method: 'csf_tz.custom_api.create_item_tax_template', - args:{ - abbr: frm.doc.abbr - + method: "csf_tz.custom_api.create_item_tax_template", + args: { + abbr: frm.doc.abbr, }, - callback: function(response) { + callback: function (response) { if (response.message) { - frappe.msgprint(__('Item Tax Templates created successfully.')); + frappe.msgprint(__("Item Tax Templates created successfully.")); } - } - }) + }, + }); }, - make_tax_category: function(frm) { + make_tax_category: function (frm) { frappe.call({ - method: 'csf_tz.custom_api.create_tax_category', - args:{ - abbr: frm.doc.abbr - + method: "csf_tz.custom_api.create_tax_category", + args: { + abbr: frm.doc.abbr, }, - callback: function(response) { + callback: function (response) { if (response.message) { - frappe.msgprint(__('Tax Category created successfully.')); + frappe.msgprint(__("Tax Category created successfully.")); } - } - }) + }, + }); }, - make_salary_components_and_structure: function(frm) { + make_salary_components_and_structure: function (frm) { frappe.call({ - method: 'csf_tz.custom_api.make_salary_components_and_structure', - args:{ - abbr: frm.doc.abbr - + method: "csf_tz.custom_api.make_salary_components_and_structure", + args: { + abbr: frm.doc.abbr, }, - callback: function(response) { + callback: function (response) { if (response.message) { - frappe.msgprint(__('Salary Components and Structure are created successfully.')); + frappe.msgprint(__("Salary Components and Structure are created successfully.")); } - } - }) + }, + }); }, }); diff --git a/csf_tz/csf_tz/custom_field.js b/csf_tz/csf_tz/custom_field.js index ce24a929..0d50f289 100644 --- a/csf_tz/csf_tz/custom_field.js +++ b/csf_tz/csf_tz/custom_field.js @@ -1,82 +1,84 @@ -frappe.listview_settings['Custom Field'] = { - onload: function (listview) { - listview.page.add_menu_item(__('Export Selected'), async function () { - const selected_docs = listview.get_checked_items(); - if (selected_docs.length === 0) { - frappe.msgprint(__('Please select at least one document.')); - return; - } +frappe.listview_settings["Custom Field"] = { + onload: function (listview) { + listview.page.add_menu_item(__("Export Selected"), async function () { + const selected_docs = listview.get_checked_items(); + if (selected_docs.length === 0) { + frappe.msgprint(__("Please select at least one document.")); + return; + } - const detailed_docs = await Promise.all(selected_docs.map(doc => - fetch(`/api/resource/Custom Field/${doc.name}`) - .then(response => response.json()) - .then(data => data.data) - )); + const detailed_docs = await Promise.all( + selected_docs.map((doc) => + fetch(`/api/resource/Custom Field/${doc.name}`) + .then((response) => response.json()) + .then((data) => data.data) + ) + ); - const data_to_export = detailed_docs.map(doc => { - return { - name: doc.name, - owner: doc.owner, - creation: doc.creation, - modified: doc.modified, - modified_by: doc.modified_by, - docstatus: doc.docstatus, - idx: doc.idx, - is_system_generated: doc.is_system_generated, - dt: doc.dt, - label: doc.label, - fieldname: doc.fieldname, - insert_after: doc.insert_after, - length: doc.length, - fieldtype: doc.fieldtype, - precision: doc.precision, - hide_seconds: doc.hide_seconds, - hide_days: doc.hide_days, - options: doc.options, - sort_options: doc.sort_options, - fetch_if_empty: doc.fetch_if_empty, - fetch_from: doc.fetch_from, - collapsible: doc.collapsible, - non_negative: doc.non_negative, - mandatory_depends_on: doc.mandatory_depends_on, - depends_on: doc.depends_on, - reqd: doc.reqd, - unique: doc.unique, - is_virtual: doc.is_virtual, - read_only: doc.read_only, - ignore_user_permissions: doc.ignore_user_permissions, - hidden: doc.hidden, - print_hide: doc.print_hide, - print_hide_if_no_value: doc.print_hide_if_no_value, - no_copy: doc.no_copy, - allow_on_submit: doc.allow_on_submit, - in_list_view: doc.in_list_view, - in_standard_filter: doc.in_standard_filter, - in_global_search: doc.in_global_search, - in_preview: doc.in_preview, - bold: doc.bold, - report_hide: doc.report_hide, - search_index: doc.search_index, - allow_in_quick_entry: doc.allow_in_quick_entry, - ignore_xss_filter: doc.ignore_xss_filter, - translatable: doc.translatable, - hide_border: doc.hide_border, - show_dashboard: doc.show_dashboard, - permlevel: doc.permlevel, - columns: doc.columns, - doctype: doc.doctype, - __last_sync_on: doc.__last_sync_on - }; - }); + const data_to_export = detailed_docs.map((doc) => { + return { + name: doc.name, + owner: doc.owner, + creation: doc.creation, + modified: doc.modified, + modified_by: doc.modified_by, + docstatus: doc.docstatus, + idx: doc.idx, + is_system_generated: doc.is_system_generated, + dt: doc.dt, + label: doc.label, + fieldname: doc.fieldname, + insert_after: doc.insert_after, + length: doc.length, + fieldtype: doc.fieldtype, + precision: doc.precision, + hide_seconds: doc.hide_seconds, + hide_days: doc.hide_days, + options: doc.options, + sort_options: doc.sort_options, + fetch_if_empty: doc.fetch_if_empty, + fetch_from: doc.fetch_from, + collapsible: doc.collapsible, + non_negative: doc.non_negative, + mandatory_depends_on: doc.mandatory_depends_on, + depends_on: doc.depends_on, + reqd: doc.reqd, + unique: doc.unique, + is_virtual: doc.is_virtual, + read_only: doc.read_only, + ignore_user_permissions: doc.ignore_user_permissions, + hidden: doc.hidden, + print_hide: doc.print_hide, + print_hide_if_no_value: doc.print_hide_if_no_value, + no_copy: doc.no_copy, + allow_on_submit: doc.allow_on_submit, + in_list_view: doc.in_list_view, + in_standard_filter: doc.in_standard_filter, + in_global_search: doc.in_global_search, + in_preview: doc.in_preview, + bold: doc.bold, + report_hide: doc.report_hide, + search_index: doc.search_index, + allow_in_quick_entry: doc.allow_in_quick_entry, + ignore_xss_filter: doc.ignore_xss_filter, + translatable: doc.translatable, + hide_border: doc.hide_border, + show_dashboard: doc.show_dashboard, + permlevel: doc.permlevel, + columns: doc.columns, + doctype: doc.doctype, + __last_sync_on: doc.__last_sync_on, + }; + }); - const jsonStr = JSON.stringify(data_to_export); - let blob = new Blob([jsonStr], { type: "application/json" }); - let a = document.createElement("a"); - a.href = URL.createObjectURL(blob); - a.download = "exported_custom_fields.json"; - a.click(); - URL.revokeObjectURL(a.href); - a.remove(); - }); - } + const jsonStr = JSON.stringify(data_to_export); + let blob = new Blob([jsonStr], { type: "application/json" }); + let a = document.createElement("a"); + a.href = URL.createObjectURL(blob); + a.download = "exported_custom_fields.json"; + a.click(); + URL.revokeObjectURL(a.href); + a.remove(); + }); + }, }; diff --git a/csf_tz/csf_tz/customer.js b/csf_tz/csf_tz/customer.js index 3751e049..5404b64d 100644 --- a/csf_tz/csf_tz/customer.js +++ b/csf_tz/csf_tz/customer.js @@ -2,20 +2,16 @@ // For license information, please see license.txt /* eslint-disable */ - frappe.ui.form.on("Customer", { - - - refresh: function(frm) { - - if(!frm.doc.__islocal) { + refresh: function (frm) { + if (!frm.doc.__islocal) { // custom buttons - frm.add_custom_button(__('Multi-Currency Ledger'), function() { - frappe.set_route('query-report', 'Multi-Currency Ledger', - {party_type:'Customer', party:frm.doc.name}); + frm.add_custom_button(__("Multi-Currency Ledger"), function () { + frappe.set_route("query-report", "Multi-Currency Ledger", { + party_type: "Customer", + party: frm.doc.name, + }); }); - } }, - }); diff --git a/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.js b/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.js index dd8cc988..e154230d 100644 --- a/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.js +++ b/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.js @@ -29,6 +29,6 @@ frappe.dashboards.chart_sources["Multi_Account Balance Timeline"] = { label: __("Include Inactive Accounts"), fieldtype: "Check", default: 0, - } + }, ], }; diff --git a/csf_tz/csf_tz/delivery_note.js b/csf_tz/csf_tz/delivery_note.js index 4abb786c..41c4dc38 100644 --- a/csf_tz/csf_tz/delivery_note.js +++ b/csf_tz/csf_tz/delivery_note.js @@ -1,18 +1,18 @@ frappe.ui.keys.add_shortcut({ - shortcut: 'ctrl+q', - action: () => { - const current_doc = $('.data-row.editable-row').parent().attr("data-name"); - const item_row = locals["Delivery Note Item"][current_doc]; - frappe.call({ - method: 'csf_tz.custom_api.get_item_info', - args: {item_code: item_row.item_code}, - callback: function(r) { - if (r.message.length > 0){ - const d = new frappe.ui.Dialog({ - title: __('Item Balance'), - width: 600 - }); - $(`