From f229a3809f5a02dd7eaf10909ca8ce5dd1e7dfed Mon Sep 17 00:00:00 2001 From: Emanuel Kagombora Date: Tue, 4 Aug 2026 14:50:03 +0300 Subject: [PATCH 01/36] ci: disable automatic semantic release on push --- .github/workflows/release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3563d723..3bc54c76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,6 @@ name: Release on: - push: - branches: [version-15] workflow_dispatch: permissions: From 9bb39b11473f968ab5b2e1962df3ab871cfabddc Mon Sep 17 00:00:00 2001 From: Emanuel Kagombora Date: Tue, 4 Aug 2026 14:50:03 +0300 Subject: [PATCH 02/36] ci: disable automatic semantic release on push (cherry picked from commit f229a3809f5a02dd7eaf10909ca8ce5dd1e7dfed) --- .github/workflows/release.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3563d723..3bc54c76 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -1,8 +1,6 @@ name: Release on: - push: - branches: [version-15] workflow_dispatch: permissions: From d5dbddc0bdc863a97a430669443f840522b28f29 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 16:40:24 +0300 Subject: [PATCH 03/36] chore: enforce app-owned ruff, pre-commit and frappe semgrep hooks --- .gitignore | 3 +++ .pre-commit-config.yaml | 20 +++++++++++++++++--- .semgrepignore | 13 +++++++++++++ commitlint.config.js | 1 - pyproject.toml | 7 +++++++ 5 files changed, 40 insertions(+), 4 deletions(-) create mode 100644 .semgrepignore 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..12126078 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,4 +1,4 @@ -exclude: 'node_modules|.git' +exclude: 'node_modules|.git|frappe-semgrep-rules' default_stages: [pre-commit] default_install_hook_types: [pre-commit, commit-msg] fail_fast: false @@ -11,6 +11,7 @@ repos: files: "csf_tz.*" exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" - id: end-of-file-fixer + exclude: ".*json$" - id: check-merge-conflict - id: check-ast - id: check-json @@ -19,7 +20,7 @@ repos: - id: debug-statements - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.8.1 + rev: v0.11.0 hooks: - id: ruff name: "Run ruff import sorter" @@ -27,10 +28,23 @@ repos: - id: ruff name: "Run ruff linter" + args: ["--fix"] - id: ruff-format name: "Run ruff formatter" + - 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 + - repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook rev: v9.22.0 hooks: @@ -40,5 +54,5 @@ repos: ci: autoupdate_schedule: weekly - skip: [] + skip: [frappe-semgrep-rules] submodules: false 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/commitlint.config.js b/commitlint.config.js index 56702092..3acd5a31 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,5 +1,4 @@ module.exports = { - parserPreset: "conventional-changelog-conventionalcommits", rules: { "subject-empty": [2, "never"], "type-case": [2, "always", "lower-case"], diff --git a/pyproject.toml b/pyproject.toml index a849daf2..951b14a0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -29,9 +29,16 @@ build-backend = "flit_core.buildapi" [tool.ruff] line-length = 110 +target-version = "py310" [tool.ruff.format] +quote-style = "double" indent-style = "tab" +docstring-code-format = true + +[tool.ruff.lint] +select = ["F", "E", "W", "I", "UP", "B"] +ignore = ["E101", "E402", "E501", "E741", "W191"] [tool.bench.frappe-dependencies] frappe = ">=15.0.0,<16.0.0" From 23e61ad473fedd12168f41dee63593efea997112 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 16:40:26 +0300 Subject: [PATCH 04/36] ci: drop bench install job, tests run manually on the bench --- .github/workflows/ci.yml | 113 --------------------------------------- 1 file changed, 113 deletions(-) delete mode 100644 .github/workflows/ci.yml 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 From 269f02f2aacac40eee232bd77cca0cf5ffb10e6c Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 16:40:25 +0300 Subject: [PATCH 05/36] ci: run frappe semgrep as a full-repo scan instead of inside pre-commit --- .github/workflows/linter.yml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linter.yml b/.github/workflows/linter.yml index 357aba35..4282edc8 100644 --- a/.github/workflows/linter.yml +++ b/.github/workflows/linter.yml @@ -28,7 +28,10 @@ jobs: cache: pip - name: Install pre-commit run: pip install pre-commit + # Semgrep is skipped here and run as a full-repo scan in the next steps - name: Run pre-commit on changed files + env: + SKIP: frappe-semgrep-rules run: | pre-commit run \ --show-diff-on-failure \ @@ -39,10 +42,23 @@ jobs: - 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 / 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' From adf45f29bd33624eadd0929d49e207e29938c396 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 17:03:34 +0300 Subject: [PATCH 06/36] fix: drop unused PyPDF2 dependency flagged by pip-audit --- csf_tz/csftz_hooks/landed_cost_voucher.py | 1 - pyproject.toml | 1 - 2 files changed, 2 deletions(-) diff --git a/csf_tz/csftz_hooks/landed_cost_voucher.py b/csf_tz/csftz_hooks/landed_cost_voucher.py index 508c358f..2b72628c 100644 --- a/csf_tz/csftz_hooks/landed_cost_voucher.py +++ b/csf_tz/csftz_hooks/landed_cost_voucher.py @@ -5,7 +5,6 @@ import os from frappe.utils.background_jobs import enqueue from frappe.utils.pdf import get_pdf, cleanup -from PyPDF2 import PdfFileWriter from csf_tz import console diff --git a/pyproject.toml b/pyproject.toml index a849daf2..1cc098bd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,7 +16,6 @@ dependencies = [ "PGPy", "standard-imghdr", "PyPDF3", - "PyPDF2", "pyotp", "qrcode", "xmltodict", From 87d463853b08fa1cd57d63e1320baa64872d0611 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 16:40:27 +0300 Subject: [PATCH 07/36] style: apply ruff format and fix all pre-commit findings --- .deepsource.toml | 1 - csf_tz/__init__.py | 50 +- csf_tz/api/selcom.py | 141 +- csf_tz/api/utils.py | 19 +- csf_tz/bank_api.py | 853 +++--- csf_tz/budget_check.py | 27 +- csf_tz/config/accounts.py | 9 +- csf_tz/config/csf_tz.py | 33 +- csf_tz/config/desktop.py | 3 +- csf_tz/config/docs.py | 1 + .../config/purchase_and_stock_management.py | 100 +- csf_tz/config/sales_and_marketing.py | 115 +- csf_tz/config/stock.py | 1 - csf_tz/csf_tz/additional_salary.js | 2 +- csf_tz/csf_tz/bom_addittional_costs.js | 2 +- csf_tz/csf_tz/company.js | 16 +- csf_tz/csf_tz/custom_field.js | 4 +- csf_tz/csf_tz/customer.js | 8 +- .../csf_tz/dashboard_chart_source/__init__.py | 4 +- .../multi_account_balance_timeline.py | 1857 ++++++------- csf_tz/csf_tz/delivery_note.js | 12 +- .../authority_notification_role/__init__.py | 1 - .../authority_notification_role.py | 2 +- .../bank_charges_pattern.py | 1 + .../test_bank_charges_pattern.py | 1 + .../csf_api_response_log.py | 38 +- .../test_csf_api_response_log.py | 3 +- .../csf_tz_bank_charges.py | 231 +- .../csf_tz_bank_charges_detail.py | 1 + .../csf_tz_settings/csf_tz_settings.py | 2 - .../csf_tz_settings/test_csf_tz_settings.py | 3 +- .../doctype/efd_z_report/efd_z_report.js | 4 +- .../doctype/efd_z_report/efd_z_report.py | 247 +- .../doctype/efd_z_report/test_efd_z_report.py | 3 +- .../efd_z_report_invoice.py | 3 +- .../test_efd_z_report_invoice.py | 3 +- .../electronic_fiscal_device.py | 3 +- .../test_electronic_fiscal_device.py | 3 +- .../foreign_import_transaction.js | 14 +- .../foreign_import_transaction.py | 355 ++- .../test_foreign_import_transaction.py | 874 +++--- .../doctype/latra_licenses/latra_licenses.py | 54 +- .../doctype/nmb_callback/nmb_callback.py | 3 +- .../doctype/nmb_callback/test_nmb_callback.py | 3 +- .../student_applicant_fees.py | 30 +- .../test_student_applicant_fees.py | 3 +- .../doctype/tra_tax_inv/test_tra_tax_inv.py | 30 +- .../csf_tz/doctype/tra_tax_inv/tra_tax_inv.py | 2345 ++++++++--------- .../tra_tax_inv_item/tra_tax_inv_item.py | 3 +- .../doctype/tz_district/test_tz_district.py | 1 + .../csf_tz/doctype/tz_district/tz_district.py | 1 + .../tz_insurance_company_detail.py | 1 + .../test_tz_insurance_cover_note.py | 1 + .../tz_insurance_cover_note.py | 149 +- .../tz_insurance_policy_holder_detail.py | 1 + .../tz_insurance_vehicle_detail.py | 1 + .../doctype/tz_region/test_tz_region.py | 1 + csf_tz/csf_tz/doctype/tz_region/tz_region.py | 1 + .../doctype/tz_village/test_tz_village.py | 1 + .../csf_tz/doctype/tz_village/tz_village.py | 1 + csf_tz/csf_tz/doctype/tz_ward/test_tz_ward.py | 1 + csf_tz/csf_tz/doctype/tz_ward/tz_ward.py | 1 + .../test_vehicle_fine_record.py | 3 +- .../vehicle_fine_record.py | 699 +++-- .../doctype/vehicle_sync_task/processor.py | 389 ++- .../csf_tz/doctype/vehicle_sync_task/queue.py | 292 +- .../vehicle_sync_task/vehicle_sync_task.py | 9 +- csf_tz/csf_tz/employee_contact_qr.js | 2 +- csf_tz/csf_tz/fees.js | 2 +- csf_tz/csf_tz/journal_entry.js | 1 - csf_tz/csf_tz/landed_cost_voucher.js | 1 - csf_tz/csf_tz/page/jobcards/jobcards.js | 4 +- csf_tz/csf_tz/page/jobcards/jobcards.py | 71 +- csf_tz/csf_tz/page/scan_qrcode/scan_qrcode.py | 21 +- csf_tz/csf_tz/payment_entry.js | 22 +- csf_tz/csf_tz/program_enrollment.js | 2 +- csf_tz/csf_tz/program_enrollment_tool.js | 2 +- csf_tz/csf_tz/property_setter.js | 2 +- csf_tz/csf_tz/purchase_invoice.js | 8 +- csf_tz/csf_tz/purchase_receipt.js | 6 +- csf_tz/csf_tz/quotation.js | 2 +- .../accounts_receivable_multi_currency.js | 1 - .../accounts_receivable_multi_currency.py | 553 ++-- ...ounts_receivable_summary_multi_currency.py | 230 +- .../accounts_receivable_utils.py | 614 +++-- .../av_sales_invoice_trend.py | 26 +- .../credit_note_list/credit_note_list.html | 10 +- .../credit_note_list/credit_note_list.js | 2 +- .../csf_tz_stock_movement.py | 900 +++---- ...salary_register_with_monthly_comparison.py | 161 +- .../excise_duty_stock/excise_duty_stock.py | 551 ++-- .../general_ledger_pro/general_ledger.py | 334 ++- .../general_ledger_pro/general_ledger_pro.py | 814 +++--- .../gross_profit_pro/gross_profit_pro.py | 379 ++- .../import_exchange_differences.py | 249 +- .../item_price_by_price_list.js | 2 +- .../item_price_by_price_list.py | 203 +- .../itemwise_stock_movement.js | 2 +- .../itemwise_stock_movement.py | 10 +- ...42\200\223_withholding_tax_statement.html" | 28 +- .../loan_repayment_details.py | 105 +- .../monthly_account_balance.py | 192 +- .../monthly_timesheet_report.py | 160 +- .../multi_currency_ledger.js | 1 - .../multi_currency_ledger.py | 479 ++-- .../output_vat_reconciliation.py | 172 +- .../particular_item_history_report.py | 390 +-- .../paye_report_mapping.py | 7 +- .../salary_register_csf.py | 909 +++---- .../salary_register_ctc.py | 513 ++-- .../salary_register_summary.py | 277 +- ...salary_register_summary_with_components.py | 266 +- ...egister_summary_with_monthly_comparison.py | 1661 ++++++------ .../stock_balance_pivot_warehouse.py | 84 +- .../stock_balance_pro/stock_balance_pro.py | 436 +-- .../trial_balance_report_in_usd.js | 2 +- .../trial_balance_report_in_usd.py | 136 +- .../vat_efiling_returns.py | 220 +- .../warehouse_wise_item_balance_and_value.py | 56 +- .../withholding_tax_payment_summary.js | 2 +- .../withholding_tax_payment_summary.py | 91 +- .../withholding_tax_summary_on_sales.js | 2 +- csf_tz/csf_tz/salary_slip.js | 6 +- csf_tz/csf_tz/sales_order.js | 2 +- csf_tz/csf_tz/stock_reconciliation.js | 2 +- csf_tz/csf_tz/student_applicant.js | 6 +- csf_tz/csf_tz/supplier.js | 8 +- csf_tz/csf_tz/warehouse.js | 8 +- csf_tz/csftz_hooks/additional_salary.py | 334 ++- csf_tz/csftz_hooks/attendance.py | 454 ++-- .../csftz_hooks/bank_charges_payment_entry.py | 96 +- csf_tz/csftz_hooks/budget.py | 18 +- csf_tz/csftz_hooks/customer.py | 43 +- .../employee_advance_payment_and_expense.py | 72 +- csf_tz/csftz_hooks/employee_checkin.py | 305 ++- csf_tz/csftz_hooks/employee_contact_qr.py | 68 +- csf_tz/csftz_hooks/exchange_calculations.py | 1176 ++++----- csf_tz/csftz_hooks/get_relation_json.py | 57 +- csf_tz/csftz_hooks/get_successor_json.py | 56 +- csf_tz/csftz_hooks/item_reposting.py | 90 +- csf_tz/csftz_hooks/items_revaluation.py | 129 +- csf_tz/csftz_hooks/landed_cost_voucher.py | 60 +- csf_tz/csftz_hooks/leave_encashment.py | 255 +- csf_tz/csftz_hooks/material_request.py | 104 +- csf_tz/csftz_hooks/payment_entry.py | 426 ++- csf_tz/csftz_hooks/payroll.py | 584 ++-- csf_tz/csftz_hooks/program_enrollment.py | 78 +- csf_tz/csftz_hooks/stock.py | 60 +- csf_tz/csftz_hooks/student_applicant.py | 97 +- csf_tz/custom_api.py | 174 +- csf_tz/hooks.py | 9 +- csf_tz/kcb/api/kcb_api.py | 361 ++- .../kcb_payments_initiation.py | 144 +- csf_tz/kcb/payments.py | 612 +++-- csf_tz/kcb/pgp.py | 16 +- csf_tz/kcb/utils/crypto_utils.py | 67 +- .../csf_tz_biometric_device.py | 1 + .../csf_tz_biometric_log.py | 1 + .../csf_tz_biometric_user.py | 1 + .../csf_tz_biometric_user_type.py | 1 + .../csf_tz_meal_type/csf_tz_meal_type.py | 1 + .../monkey_patches/db_transaction_writes.py | 50 +- csf_tz/overrides/additional_salary.py | 33 +- csf_tz/overrides/leave_encashment.py | 14 +- csf_tz/overrides/salary_slip.py | 172 +- ...stom_field_for_cusomer_suppliers_groups.py | 32 +- .../add_custom_fields_for_employee_advance.py | 48 +- ..._invoice_item_and_purchase_invoice_item.py | 82 +- ...om_fields_on_customer_for_auto_close_dn.py | 56 +- csf_tz/patches/create_the_stock_entry_type.py | 19 +- ...ance_overtime_calculation_custom_fields.py | 325 ++- ...ate_custom_fields_for_additional_salary.py | 199 +- ...fields_for_removed_edu_fields_in_csf_tz.py | 1719 ++++++------ .../delete_employee_custom_fields.py | 7 +- .../payroll_approval_custom_fields.py | 49 +- .../payroll_cost_center_custom_fields.py | 18 +- .../vfd_providers_updated_custom_fields.py | 433 ++- csf_tz/patches/delete_default_value_fields.py | 10 +- .../disable_signup_in_website_settings.py | 7 +- csf_tz/patches/fix_module_for_core_reports.py | 33 +- .../fixtures/old_fixtures_from_hooks.py | 466 ++-- .../migrate_vfd_providers_to_csf_tz.py | 10 +- .../property_setter/property_setter.py | 977 ++++--- .../remove_deleted_modules_metadata.py | 1 - .../remove_ot_component_custom_fields.py | 21 +- .../patches/remove_stock_entry_qty_field.py | 10 +- .../tz_post_code/create_tz_post_code.py | 525 ++-- ...ware_settings_values_to_csf_tz_settings.py | 24 +- ...m_currrent_employee_payroll_cost_center.py | 12 +- csf_tz/public/js/budget_check_utils.js | 1 - csf_tz/public/js/jobcards/Card.vue | 2 +- csf_tz/public/js/jobcards/JobCards.vue | 4 +- csf_tz/public/js/jobcards/bus.js | 2 +- csf_tz/public/js/jobcards/jobcards.bundle.js | 8 +- csf_tz/public/js/jobcards/jobcards.js | 14 +- csf_tz/public/js/po_shortcuts.js | 2 +- csf_tz/public/js/select_dialog.js | 1 - csf_tz/public/js/shortcuts.js | 2 +- csf_tz/public/js/to_console.js | 2 +- .../doctype/bin_list/bin_list.py | 4 +- .../doctype/bin_setup/bin_setup.py | 13 +- .../doctype/bin_setup/test_bin_setup.py | 4 +- .../doctype/item_number/item_number.py | 4 +- .../doctype/item_number/test_item_number.py | 4 +- .../doctype/order_track/order_track.js | 36 +- .../doctype/order_track/order_track.py | 4 +- .../doctype/order_track/test_order_track.py | 4 +- .../order_tracking_container.py | 4 +- .../purchase_and_stock_management_test.py | 4 +- ...test_purchase_and_stock_management_test.py | 4 +- .../ordered_items_to_be_delivered.py | 145 +- .../pending_ordered_items.py | 102 +- .../purchase_history/purchase_history.py | 109 +- .../reordering_items/reordering_items.py | 70 +- .../shipment_tracking/shipment_tracking.py | 136 +- .../supplier_contacts/supplier_contacts.py | 45 +- .../doctype/allert_custom/allert_custom.py | 4 +- .../allert_custom/test_allert_custom.py | 4 +- .../doctype/communications/communications.py | 4 +- .../communications/test_communications.py | 4 +- .../doctype/customer_item/customer_item.py | 4 +- .../doctype/marketing_dept/marketing_dept.py | 4 +- .../marketing_dept/test_marketing_dept.py | 4 +- .../doctype/past_sales/past_sales.py | 4 +- .../doctype/past_sales/test_past_sales.py | 4 +- .../doctype/past_serial_no/past_serial_no.py | 4 +- .../past_serial_no/test_past_serial_no.py | 4 +- .../doctype/payment_plan/payment_plan.py | 4 +- .../products_of_interest.py | 4 +- .../brand_sales_report/brand_sales_report.py | 105 +- .../customer_loan_assistance_report.py | 67 +- .../item_wise_leads_report.py | 51 +- .../items_marked_for_delivery.py | 105 +- .../previous_ams_customer_report.py | 118 +- .../sales_details_report.py | 92 +- .../spare_sales_report/spare_sales_report.py | 49 +- .../stanbic_payments_info.py | 1 + .../stanbic_payments_initiation.py | 360 ++- .../stanbic_payments_initiation/xml.py | 30 +- .../stanbic_setting/stanbic_setting.py | 1 + csf_tz/stanbic/payments.py | 77 +- csf_tz/stanbic/pgp.py | 10 +- csf_tz/stanbic/sftp.py | 348 ++- csf_tz/stanbic/xml.py | 6 +- .../authority_notification_settings_fields.py | 1 - csf_tz/utils/create_custom_fields.py | 114 +- csf_tz/utils/create_property_setter.py | 105 +- csf_tz/utils/fix_balance_qty.py | 57 +- csf_tz/utils/setup.py | 1 - csf_tz/vehicle_authority.py | 7 +- .../simplify_vfd_settings.py | 699 +++-- .../test_simplify_vfd_settings.py | 2 +- .../total_vfd_setting/total_vfd_setting.py | 540 ++-- .../doctype/vfd_provider/vfd_provider.py | 1 + .../vfd_provider_attribute.py | 1 + .../vfd_provider_posting.py | 1 + .../vfdplus_settings/vfdplus_settings.py | 663 +++-- csf_tz/vfd_providers/utils.py | 46 +- .../company_vfd_provider.py | 1 + csf_tz/vfd_support/__init__.py | 1 - csf_tz/vfd_support/customer.js | 2 +- csf_tz/vfd_support/sales_invoice.js | 5 +- csf_tz/vfd_support/sales_invoice.py | 331 +-- csf_tz/vfd_support/utils.py | 277 +- license.txt | 2 +- 265 files changed, 18196 insertions(+), 18647 deletions(-) diff --git a/.deepsource.toml b/.deepsource.toml index c2162e34..25bc3d76 100644 --- a/.deepsource.toml +++ b/.deepsource.toml @@ -6,4 +6,3 @@ enabled = true [analyzers.meta] runtime_version = "3.x.x" - diff --git a/csf_tz/__init__.py b/csf_tz/__init__.py index b8b28a8f..95acbb0d 100755 --- a/csf_tz/__init__.py +++ b/csf_tz/__init__.py @@ -1,7 +1,5 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals -import os import importlib +import os import frappe @@ -12,33 +10,33 @@ def load_monkey_patches(): - """ - Loads all modules present in monkey_patches to override some logic - in Frappe / ERPNext. Returns if patches have already been loaded earlier. - """ - global patches_loaded + """ + Loads all modules present in monkey_patches to override some logic + in Frappe / ERPNext. Returns if patches have already been loaded earlier. + """ + global patches_loaded - if patches_loaded: - return + if patches_loaded: + return - patches_loaded = True + patches_loaded = True - if app_name not in frappe.get_installed_apps(): - return + if app_name not in frappe.get_installed_apps(): + return - 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 + 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 - importlib.import_module(app_name + ".monkey_patches." + module_name[:-3]) + importlib.import_module(app_name + ".monkey_patches." + module_name[:-3]) old_get_hooks = frappe.get_hooks def get_hooks(*args, **kwargs): - load_monkey_patches() - return old_get_hooks(*args, **kwargs) + load_monkey_patches() + return old_get_hooks(*args, **kwargs) frappe.get_hooks = get_hooks @@ -47,17 +45,17 @@ def get_hooks(*args, **kwargs): def connect(*args, **kwargs): - """ - Patches frappe.connect to load monkey patches once a connection is - established with the database. - """ + """ + Patches frappe.connect to load monkey patches once a connection is + established with the database. + """ - old_connect(*args, **kwargs) - load_monkey_patches() + old_connect(*args, **kwargs) + load_monkey_patches() frappe.connect = connect def console(*data): - frappe.publish_realtime("out_to_console", data, user=frappe.session.user) + frappe.publish_realtime("out_to_console", data, user=frappe.session.user) diff --git a/csf_tz/api/selcom.py b/csf_tz/api/selcom.py index ff90f207..490c83f1 100644 --- a/csf_tz/api/selcom.py +++ b/csf_tz/api/selcom.py @@ -1,86 +1,85 @@ -from selcom_apigw_client import apigwClient import json + import frappe from frappe import _ +from selcom_apigw_client import apigwClient def create_order_log(method, status, request_json, response, reference): - doc = frappe.new_doc("Selcom Order Log") - doc.date = frappe.utils.today() - doc.time = frappe.utils.nowtime() - doc.method = method - doc.status = status - doc.reference = reference - doc.request_data = json.dumps(request_json, indent=4) - doc.response_data = json.dumps(response, indent=4) - doc.insert(ignore_permissions=True) - frappe.db.commit() + doc = frappe.new_doc("Selcom Order Log") + doc.date = frappe.utils.today() + doc.time = frappe.utils.nowtime() + doc.method = method + doc.status = status + doc.reference = reference + doc.request_data = json.dumps(request_json, indent=4) + doc.response_data = json.dumps(response, indent=4) + doc.insert(ignore_permissions=True) + frappe.db.commit() @frappe.whitelist() def create_order_minimal(): - # Initialize API client - apiKey = "" - apiSecret = "" - baseUrl = "https://apigw.selcommobile.com/v1" + # Initialize API client + apiKey = "" + apiSecret = "" + baseUrl = "https://apigw.selcommobile.com/v1" - client = apigwClient.Client(baseUrl, apiKey, apiSecret) + client = apigwClient.Client(baseUrl, apiKey, apiSecret) - # Order data - orderDict = { - "vendor": "TILL61056542", - "order_id": "1218d5Qb", - "buyer_email": "john@example.com", - "buyer_name": "John Joh", - "buyer_phone": "255789968024", - "amount": 8000, - "currency": "TZS", - "buyer_remarks": "None", - "merchant_remarks": "None", - "no_of_items": 1, - } + # Order data + orderDict = { + "vendor": "TILL61056542", + "order_id": "1218d5Qb", + "buyer_email": "john@example.com", + "buyer_name": "John Joh", + "buyer_phone": "255789968024", + "amount": 8000, + "currency": "TZS", + "buyer_remarks": "None", + "merchant_remarks": "None", + "no_of_items": 1, + } - # API endpoint - orderPath = "/checkout/create-order-minimal" + # API endpoint + orderPath = "/checkout/create-order-minimal" - # Send order request - try: - response = client.postFunc(orderPath, orderDict) - if response.get("resultcode") != "000": - frappe.log_error( - f"Error on Create Order {response.get('reference')} to Payment Gateway", - response, - ) - create_order_log( - "Create Order Minimal", - "Failed", - orderDict, - response, - reference=response.get("reference"), - ) - else: - frappe.msgprint(_("Order created successfully"), alert=True) - create_order_log( - "Create Order Minimal", - "Success", - orderDict, - response, - reference=response.get("reference"), - ) - return response + # Send order request + try: + response = client.postFunc(orderPath, orderDict) + if response.get("resultcode") != "000": + frappe.log_error( + f"Error on Create Order {response.get('reference')} to Payment Gateway", + response, + ) + create_order_log( + "Create Order Minimal", + "Failed", + orderDict, + response, + reference=response.get("reference"), + ) + else: + frappe.msgprint(_("Order created successfully"), alert=True) + create_order_log( + "Create Order Minimal", + "Success", + orderDict, + response, + reference=response.get("reference"), + ) + return response - except Exception as e: - frappe.log_error( - f"Error on Create Order {response.get('reference')} to Payment Gateway", - str(e), - ) - create_order_log( - "Create Order Minimal", - "Failed", - orderDict, - str(e), - reference=response.get("reference"), - ) - frappe.throw( - _("Failed to create order, please try again, or contact the administrator") - ) + except Exception as e: + frappe.log_error( + f"Error on Create Order {response.get('reference')} to Payment Gateway", + str(e), + ) + create_order_log( + "Create Order Minimal", + "Failed", + orderDict, + str(e), + reference=response.get("reference"), + ) + frappe.throw(_("Failed to create order, please try again, or contact the administrator")) diff --git a/csf_tz/api/utils.py b/csf_tz/api/utils.py index 9b0f6c87..3b7896a7 100644 --- a/csf_tz/api/utils.py +++ b/csf_tz/api/utils.py @@ -1,16 +1,15 @@ -from __future__ import unicode_literals import frappe -from frappe import _ + def msgThrow(msg, method="throw", alert=True): - if method == "validate": - frappe.msgprint(msg, alert=alert) - else: - frappe.throw(msg) + if method == "validate": + frappe.msgprint(msg, alert=alert) + else: + frappe.throw(msg) def msgPrint(msg, method="throw", alert=False): - if method == "validate": - frappe.msgprint(msg, alert=True) - else: - frappe.msgprint(msg, alert=alert) + if method == "validate": + frappe.msgprint(msg, alert=True) + else: + frappe.msgprint(msg, alert=alert) diff --git a/csf_tz/bank_api.py b/csf_tz/bank_api.py index 926baa88..5bc79f6b 100644 --- a/csf_tz/bank_api.py +++ b/csf_tz/bank_api.py @@ -1,508 +1,477 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Youssef Restom and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe -from frappe import _ -import json -import requests -from frappe.utils import get_host_name, flt -from time import sleep import binascii +import json import os +from datetime import datetime +from time import sleep from urllib.parse import quote, urlparse, urlunparse + +import frappe +import requests from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from frappe import _ +from frappe.utils import flt, get_host_name from frappe.utils.background_jobs import enqueue -from datetime import datetime from frappe.utils.password import get_decrypted_password + from csf_tz.csf_tz.doctype.csf_api_response_log.csf_api_response_log import add_log -class ToObject(object): - def __init__(self, data): - self.__dict__ = json.loads(data) +class ToObject: + def __init__(self, data): + self.__dict__ = json.loads(data) def set_callback_token(doc, method): - send_fee_details_to_bank = ( - frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 - ) - if not send_fee_details_to_bank: - return - doc.callback_token = binascii.hexlify(os.urandom(14)).decode() - series = frappe.get_value("Company", doc.company, "nmb_series") or "" - if not series: - frappe.throw(_("Please set NMB User Series in Company {0}".format(doc.company))) - reference = str(series) + "F" + str(doc.name) - if not doc.abbr: - doc.abbr = frappe.get_value("Company", doc.company, "abbr") or "" - doc.bank_reference = reference.replace("-", "").replace("FEE" + doc.abbr, "") - if method == "invoice_submission": - doc.save() - frappe.db.commit() + send_fee_details_to_bank = frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 + if not send_fee_details_to_bank: + return + doc.callback_token = binascii.hexlify(os.urandom(14)).decode() + series = frappe.get_value("Company", doc.company, "nmb_series") or "" + if not series: + frappe.throw(_(f"Please set NMB User Series in Company {doc.company}")) + reference = str(series) + "F" + str(doc.name) + if not doc.abbr: + doc.abbr = frappe.get_value("Company", doc.company, "abbr") or "" + doc.bank_reference = reference.replace("-", "").replace("FEE" + doc.abbr, "") + if method == "invoice_submission": + doc.save() + frappe.db.commit() def get_nmb_token(company): - url = frappe.get_value("Company", company, "nmb_url") - if not url: - frappe.throw(_("Please set NMB URL in Company {0}".format(company))) - url = url + str("auth") - username = frappe.get_value("Company", company, "nmb_username") - if not username: - frappe.throw(_("Please set NMB User Name in Company {0}".format(company))) - password = get_decrypted_password("Company", company, "nmb_password") - if not password: - frappe.throw(_("Please set NMB Password in Company {0}".format(company))) - data = { - "username": username, - "password": password, - } - for i in range(3): - try: - r = requests.post(url, data=json.dumps(data), timeout=5) - r.raise_for_status() - frappe.logger().debug({"get_nmb_token webhook_success": r.text}) - if json.loads(r.text): - add_log( - request_type="NMB token", - request_url=url, - request_header="no header", - request_body=json.dumps(data), - response_data=json.loads(r.text), - ) - if json.loads(r.text)["status"] == 1: - return json.loads(r.text)["token"] - else: - frappe.throw(json.loads(r.text)) - except Exception as e: - frappe.logger().debug({"get_nmb_token webhook_error": e, "try": i + 1}) - sleep(3 * i + 1) - if i != 2: - continue - else: - raise e + url = frappe.get_value("Company", company, "nmb_url") + if not url: + frappe.throw(_(f"Please set NMB URL in Company {company}")) + url = url + "auth" + username = frappe.get_value("Company", company, "nmb_username") + if not username: + frappe.throw(_(f"Please set NMB User Name in Company {company}")) + password = get_decrypted_password("Company", company, "nmb_password") + if not password: + frappe.throw(_(f"Please set NMB Password in Company {company}")) + data = { + "username": username, + "password": password, + } + for i in range(3): + try: + r = requests.post(url, data=json.dumps(data), timeout=5) + r.raise_for_status() + frappe.logger().debug({"get_nmb_token webhook_success": r.text}) + if json.loads(r.text): + add_log( + request_type="NMB token", + request_url=url, + request_header="no header", + request_body=json.dumps(data), + response_data=json.loads(r.text), + ) + if json.loads(r.text)["status"] == 1: + return json.loads(r.text)["token"] + else: + frappe.throw(json.loads(r.text)) + except Exception as e: + frappe.logger().debug({"get_nmb_token webhook_error": e, "try": i + 1}) + sleep(3 * i + 1) + if i != 2: + continue + else: + raise e def send_nmb(method, data, company): - url = frappe.get_value("Company", company, "nmb_url") - if not url: - frappe.throw(_("Please set NMB URL in Company {0}".format(company))) - data["token"] = get_nmb_token(company) - url = url + str(method) - for i in range(3): - try: - r = requests.post(url, data=json.dumps(data), timeout=5) - r.raise_for_status() - frappe.logger().debug({"send_nmb webhook_success": r.text}) - if json.loads(r.text): - add_log( - request_type="NMB " + method, - request_url=url, - request_header="no header", - request_body=json.dumps(data), - response_data=json.loads(r.text), - ) - if json.loads(r.text)["status"] == 1: - frappe.msgprint( - "Response from bank:

" + json.loads(r.text)["description"] - ) - return json.loads(r.text) - else: - print(json.loads(r.text)["description"]) - if json.loads(r.text)["description"] == "Duplicate Invoice Number": - return json.loads(r.text) - frappe.msgprint( - "Error detected at bank:

" - + json.loads(r.text)["description"] - ) - frappe.throw(json.loads(r.text)) - except Exception as e: - frappe.logger().debug({"send_nmb webhook_error": e, "try": i + 1}) - sleep(3 * i + 1) - if i != 2: - continue - else: - raise e + url = frappe.get_value("Company", company, "nmb_url") + if not url: + frappe.throw(_(f"Please set NMB URL in Company {company}")) + data["token"] = get_nmb_token(company) + url = url + str(method) + for i in range(3): + try: + r = requests.post(url, data=json.dumps(data), timeout=5) + r.raise_for_status() + frappe.logger().debug({"send_nmb webhook_success": r.text}) + if json.loads(r.text): + add_log( + request_type="NMB " + method, + request_url=url, + request_header="no header", + request_body=json.dumps(data), + response_data=json.loads(r.text), + ) + if json.loads(r.text)["status"] == 1: + frappe.msgprint("Response from bank:

" + json.loads(r.text)["description"]) + return json.loads(r.text) + else: + print(json.loads(r.text)["description"]) + if json.loads(r.text)["description"] == "Duplicate Invoice Number": + return json.loads(r.text) + frappe.msgprint("Error detected at bank:

" + json.loads(r.text)["description"]) + frappe.throw(json.loads(r.text)) + except Exception as e: + frappe.logger().debug({"send_nmb webhook_error": e, "try": i + 1}) + sleep(3 * i + 1) + if i != 2: + continue + else: + raise e @frappe.whitelist() def invoice_submission(doc=None, method=None, fees_name=None): - send_fee_details_to_bank = ( - frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 - ) - - partial_payment = frappe.get_value("Edu Tz Settings", "Edu Tz Settings", "partial_payment") - - # Handle None case and convert to string for bank API - if partial_payment is None or not partial_payment: - partial_payment = "FALSE" - else: - partial_payment = "TRUE" - - if not send_fee_details_to_bank: - return - if not doc and fees_name: - doc = frappe.get_doc("Fees", fees_name) - if not doc.callback_token: - frappe.msgprint( - _( - "This fee is not set with a token to be sent to the Bank. Generating the token..." - ), - alert=True, - ) - set_callback_token(doc, "invoice_submission") - series = frappe.get_value("Company", doc.company, "nmb_series") or "" - abbr = frappe.get_value("Company", doc.company, "abbr") or "" - if not series: - frappe.throw(_("Please set NMB User Series in Company {0}".format(doc.company))) - data = { - "reference": doc.bank_reference, - "student_name": doc.student_name, - "student_id": doc.student, - "amount": doc.grand_total, - "type": "Fees Invoice", - "code": 10, - "allow_partial": partial_payment, - "callback_url": "https://" - + get_host_name() - + "/api/method/csf_tz.bank_api.receive_callback?token=" - + doc.callback_token, - } - send_nmb("invoice_submission", data, doc.company) + send_fee_details_to_bank = frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 + + partial_payment = frappe.get_value("Edu Tz Settings", "Edu Tz Settings", "partial_payment") + + # Handle None case and convert to string for bank API + if partial_payment is None or not partial_payment: + partial_payment = "FALSE" + else: + partial_payment = "TRUE" + + if not send_fee_details_to_bank: + return + if not doc and fees_name: + doc = frappe.get_doc("Fees", fees_name) + if not doc.callback_token: + frappe.msgprint( + _("This fee is not set with a token to be sent to the Bank. Generating the token..."), + alert=True, + ) + set_callback_token(doc, "invoice_submission") + series = frappe.get_value("Company", doc.company, "nmb_series") or "" + if not series: + frappe.throw(_(f"Please set NMB User Series in Company {doc.company}")) + data = { + "reference": doc.bank_reference, + "student_name": doc.student_name, + "student_id": doc.student, + "amount": doc.grand_total, + "type": "Fees Invoice", + "code": 10, + "allow_partial": partial_payment, + "callback_url": "https://" + + get_host_name() + + "/api/method/csf_tz.bank_api.receive_callback?token=" + + doc.callback_token, + } + send_nmb("invoice_submission", data, doc.company) @frappe.whitelist(allow_guest=True) def receive_callback(*args, **kwargs): - r = frappe.request - url = url_fix(r.url.replace("+", " ")) - # http_method = r.method - body = r.get_data() - # headers = r.headers - message = {} - if body: - data = body.decode("utf-8") - msgs = ToObject(data) - atr_list = list(msgs.__dict__) - for atr in atr_list: - if getattr(msgs, atr): - message[atr] = getattr(msgs, atr) - else: - frappe.throw("This has no body!") - parsed_url = urlparse(url) - message["fees_token"] = parsed_url[4][6:] - message["doctype"] = "NMB Callback" - nmb_doc = frappe.get_doc(message) - - if nmb_doc.insert(ignore_permissions=True): - frappe.response["status"] = 1 - frappe.response["description"] = "success" - else: - frappe.response["description"] = "insert failed" - frappe.response["http_status_code"] = 409 - - enqueue( - method=make_payment_entry, - queue="short", - timeout=10000, - is_async=True, - kwargs=nmb_doc, - ) + r = frappe.request + url = url_fix(r.url.replace("+", " ")) + # http_method = r.method + body = r.get_data() + # headers = r.headers + message = {} + if body: + data = body.decode("utf-8") + msgs = ToObject(data) + atr_list = list(msgs.__dict__) + for atr in atr_list: + if getattr(msgs, atr): + message[atr] = getattr(msgs, atr) + else: + frappe.throw("This has no body!") + parsed_url = urlparse(url) + message["fees_token"] = parsed_url[4][6:] + message["doctype"] = "NMB Callback" + nmb_doc = frappe.get_doc(message) + + if nmb_doc.insert(ignore_permissions=True): + frappe.response["status"] = 1 + frappe.response["description"] = "success" + else: + frappe.response["description"] = "insert failed" + frappe.response["http_status_code"] = 409 + + enqueue( + method=make_payment_entry, + queue="short", + timeout=10000, + is_async=True, + kwargs=nmb_doc, + ) def make_payment_entry(method="callback", **kwargs): - for key, value in kwargs.items(): - nmb_doc = value - doc_info = get_fee_info(nmb_doc.reference) - accounts = get_fees_default_accounts(doc_info["company"]) - - nmb_amount = flt(nmb_doc.amount) - frappe.flags.ignore_account_permission = True - if doc_info["doctype"] == "Fees": - if method == "callback": - frappe.set_user("Administrator") - fees_name = doc_info["name"] - bank_reference, receivable_account = frappe.get_value( - "Fees", fees_name, ["bank_reference", "receivable_account"] - ) - if bank_reference == nmb_doc.reference: - payment_entry = get_payment_entry( - "Fees", - fees_name, - party_amount=nmb_amount, - bank_amount=nmb_amount, - party_type="Student", - payment_type="Receive", - ) - payment_entry.update( - { - "payment_date": nmb_doc.timestamp, - "posting_date": nmb_doc.timestamp, - "reference_no": nmb_doc.reference, - "reference_date": nmb_doc.timestamp, - "remarks": "Payment Entry against {0} {1} via NMB Bank Payment {2}".format( - "Fees", fees_name, nmb_doc.reference - ), - "paid_from": receivable_account, - "party_account": receivable_account, - } - ) - payment_entry.flags.ignore_permissions = True - # payment_entry.references = [] - # payment_entry.set_missing_values() - payment_entry.save() - payment_entry.submit() - return nmb_doc - - elif doc_info["doctype"] == "Student Applicant Fees": - doc = frappe.get_doc("Student Applicant Fees", doc_info["name"]) - if not doc.callback_token == nmb_doc.fees_token: - return - # Below remarked after introducing VFD in AV solutions - # jl_rows = [] - # debit_row = dict( - # account=accounts["bank"], - # debit_in_account_currency=nmb_amount, - # account_currency=accounts["currency"], - # cost_center=doc.cost_center, - # ) - # jl_rows.append(debit_row) - - # credit_row_1 = dict( - # account=accounts["income"], - # credit_in_account_currency=nmb_amount, - # account_currency=accounts["currency"], - # cost_center=doc.cost_center, - # ) - # jl_rows.append(credit_row_1) - - # user_remark = ( - # "Journal Entry against {0} {1} via NMB Bank Payment {2}".format( - # "Student Applicant Fees", doc_info["name"], nmb_doc.reference - # ) - # ) - # jv_doc = frappe.get_doc( - # dict( - # doctype="Journal Entry", - # posting_date=nmb_doc.timestamp, - # accounts=jl_rows, - # company=doc.company, - # multi_currency=0, - # user_remark=user_remark, - # ) - # ) - - # jv_doc.flags.ignore_permissions = True - # frappe.flags.ignore_account_permission = True - # jv_doc.save() - # jv_doc.submit() - # jv_url = frappe.utils.get_url_to_form(jv_doc.doctype, jv_doc.name) - # si_msgprint = "Journal Entry Created {1}".format( - # jv_url, jv_doc.name - # ) - # frappe.msgprint(_(si_msgprint)) - frappe.db.set_value( - "Student Applicant", doc.student, "application_status", "Approved" - ) - return nmb_doc + for _key, value in kwargs.items(): + nmb_doc = value + doc_info = get_fee_info(nmb_doc.reference) + + nmb_amount = flt(nmb_doc.amount) + frappe.flags.ignore_account_permission = True + if doc_info["doctype"] == "Fees": + if method == "callback": + frappe.set_user("Administrator") + fees_name = doc_info["name"] + bank_reference, receivable_account = frappe.get_value( + "Fees", fees_name, ["bank_reference", "receivable_account"] + ) + if bank_reference == nmb_doc.reference: + payment_entry = get_payment_entry( + "Fees", + fees_name, + party_amount=nmb_amount, + bank_amount=nmb_amount, + party_type="Student", + payment_type="Receive", + ) + payment_entry.update( + { + "payment_date": nmb_doc.timestamp, + "posting_date": nmb_doc.timestamp, + "reference_no": nmb_doc.reference, + "reference_date": nmb_doc.timestamp, + "remarks": "Payment Entry against {} {} via NMB Bank Payment {}".format( + "Fees", fees_name, nmb_doc.reference + ), + "paid_from": receivable_account, + "party_account": receivable_account, + } + ) + payment_entry.flags.ignore_permissions = True + # payment_entry.references = [] + # payment_entry.set_missing_values() + payment_entry.save() + payment_entry.submit() + return nmb_doc + + elif doc_info["doctype"] == "Student Applicant Fees": + doc = frappe.get_doc("Student Applicant Fees", doc_info["name"]) + if not doc.callback_token == nmb_doc.fees_token: + return + # Below remarked after introducing VFD in AV solutions + # jl_rows = [] + # debit_row = dict( + # account=accounts["bank"], + # debit_in_account_currency=nmb_amount, + # account_currency=accounts["currency"], + # cost_center=doc.cost_center, + # ) + # jl_rows.append(debit_row) + + # credit_row_1 = dict( + # account=accounts["income"], + # credit_in_account_currency=nmb_amount, + # account_currency=accounts["currency"], + # cost_center=doc.cost_center, + # ) + # jl_rows.append(credit_row_1) + + # user_remark = ( + # "Journal Entry against {0} {1} via NMB Bank Payment {2}".format( + # "Student Applicant Fees", doc_info["name"], nmb_doc.reference + # ) + # ) + # jv_doc = frappe.get_doc( + # dict( + # doctype="Journal Entry", + # posting_date=nmb_doc.timestamp, + # accounts=jl_rows, + # company=doc.company, + # multi_currency=0, + # user_remark=user_remark, + # ) + # ) + + # jv_doc.flags.ignore_permissions = True + # frappe.flags.ignore_account_permission = True + # jv_doc.save() + # jv_doc.submit() + # jv_url = frappe.utils.get_url_to_form(jv_doc.doctype, jv_doc.name) + # si_msgprint = "Journal Entry Created {1}".format( + # jv_url, jv_doc.name + # ) + # frappe.msgprint(_(si_msgprint)) + frappe.db.set_value("Student Applicant", doc.student, "application_status", "Approved") + return nmb_doc @frappe.whitelist(allow_guest=True) def receive_validate_reference(*args, **kwargs): - r = frappe.request - # uri = url_fix(r.url.replace("+"," ")) - # http_method = r.method - body = r.get_data() - # headers = r.headers - message = {} - if body: - data = body.decode("utf-8") - msgs = ToObject(data) - atr_list = list(msgs.__dict__) - for atr in atr_list: - if getattr(msgs, atr): - message[atr] = getattr(msgs, atr) - else: - frappe.throw("This has no body!") - - doc_info = get_fee_info(message["reference"]) - if doc_info["name"]: - doc = frappe.get_doc(doc_info["doctype"], doc_info["name"]) - response = dict( - status=1, - reference=doc.bank_reference, - student_name=doc.student_name, - student_id=doc.student, - amount=doc.grand_total, - type="Fees Invoice", - code=10, - allow_partial="FALSE", - callback_url="https://" - + get_host_name() - + "/api/method/csf_tz.bank_api.receive_callback?token=" - + doc.callback_token, - token=message["token"], - ) - return response - else: - frappe.response["status"] = 0 - frappe.response["description"] = "Not Exist" + r = frappe.request + # uri = url_fix(r.url.replace("+"," ")) + # http_method = r.method + body = r.get_data() + # headers = r.headers + message = {} + if body: + data = body.decode("utf-8") + msgs = ToObject(data) + atr_list = list(msgs.__dict__) + for atr in atr_list: + if getattr(msgs, atr): + message[atr] = getattr(msgs, atr) + else: + frappe.throw("This has no body!") + + doc_info = get_fee_info(message["reference"]) + if doc_info["name"]: + doc = frappe.get_doc(doc_info["doctype"], doc_info["name"]) + response = dict( + status=1, + reference=doc.bank_reference, + student_name=doc.student_name, + student_id=doc.student, + amount=doc.grand_total, + type="Fees Invoice", + code=10, + allow_partial="FALSE", + callback_url="https://" + + get_host_name() + + "/api/method/csf_tz.bank_api.receive_callback?token=" + + doc.callback_token, + token=message["token"], + ) + return response + else: + frappe.response["status"] = 0 + frappe.response["description"] = "Not Exist" def cancel_invoice(doc, method): - send_fee_details_to_bank = ( - frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 - ) - if not send_fee_details_to_bank: - return - data = { - "reference": str(doc.bank_reference), - } - message = send_nmb("invoice_cancel", data, doc.company) - frappe.msgprint(str(message)) + send_fee_details_to_bank = frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 + if not send_fee_details_to_bank: + return + data = { + "reference": str(doc.bank_reference), + } + message = send_nmb("invoice_cancel", data, doc.company) + frappe.msgprint(str(message)) def reconciliation(doc=None, method=None): - companys = frappe.get_all("Company") - for company in companys: - if not frappe.get_value("Company", company["name"], "nmb_username"): - continue - data = {"reconcile_date": datetime.today().strftime("%d-%m-%Y")} - frappe.msgprint(str(data)) - message = send_nmb("reconcilliation", data, company["name"]) - if message["status"] == 1 and len(message["transactions"]) > 0: - for i in message["transactions"]: - if ( - len( - frappe.get_all( - "NMB Callback", - filters=[ - ["NMB Callback", "reference", "=", i.reference], - ["NMB Callback", "receipt", "=", i.receipt], - ], - fields=["name"], - ) - ) - == 1 - ): - doc_info = get_fee_info(message["reference"]) - if doc_info["name"]: - message["fees_token"] = frappe.get_value( - doc_info["doctype"], doc_info["name"], "callback_token" - ) - message["doctype"] = "NMB Callback" - nmb_doc = frappe.get_doc(message) - enqueue( - method=make_payment_entry, - queue="short", - timeout=10000, - is_async=True, - kwargs=nmb_doc, - ) + companys = frappe.get_all("Company") + for company in companys: + if not frappe.get_value("Company", company["name"], "nmb_username"): + continue + data = {"reconcile_date": datetime.today().strftime("%d-%m-%Y")} + frappe.msgprint(str(data)) + message = send_nmb("reconcilliation", data, company["name"]) + if message["status"] == 1 and len(message["transactions"]) > 0: + for i in message["transactions"]: + if ( + len( + frappe.get_all( + "NMB Callback", + filters=[ + ["NMB Callback", "reference", "=", i.reference], + ["NMB Callback", "receipt", "=", i.receipt], + ], + fields=["name"], + ) + ) + == 1 + ): + doc_info = get_fee_info(message["reference"]) + if doc_info["name"]: + message["fees_token"] = frappe.get_value( + doc_info["doctype"], doc_info["name"], "callback_token" + ) + message["doctype"] = "NMB Callback" + nmb_doc = frappe.get_doc(message) + enqueue( + method=make_payment_entry, + queue="short", + timeout=10000, + is_async=True, + kwargs=nmb_doc, + ) def get_fee_info(bank_reference): - data = {"name": "", "doctype": ""} - doc_list = frappe.get_all( - "Fees", - filters=[ - ["Fees", "bank_reference", "=", bank_reference], - ["Fees", "docstatus", "=", 1], - ], - fields=["name", "company"], - ) - if len(doc_list): - data["name"] = doc_list[0]["name"] - data["doctype"] = "Fees" - data["company"] = doc_list[0]["company"] - return data - else: - doc_list = frappe.get_all( - "Student Applicant Fees", - filters=[ - ["Student Applicant Fees", "bank_reference", "=", bank_reference], - ["Student Applicant Fees", "docstatus", "=", 1], - ], - fields=["name", "company"], - ) - if len(doc_list): - data["name"] = doc_list[0]["name"] - data["doctype"] = "Student Applicant Fees" - data["company"] = doc_list[0]["company"] - return data + data = {"name": "", "doctype": ""} + doc_list = frappe.get_all( + "Fees", + filters=[ + ["Fees", "bank_reference", "=", bank_reference], + ["Fees", "docstatus", "=", 1], + ], + fields=["name", "company"], + ) + if len(doc_list): + data["name"] = doc_list[0]["name"] + data["doctype"] = "Fees" + data["company"] = doc_list[0]["company"] + return data + else: + doc_list = frappe.get_all( + "Student Applicant Fees", + filters=[ + ["Student Applicant Fees", "bank_reference", "=", bank_reference], + ["Student Applicant Fees", "docstatus", "=", 1], + ], + fields=["name", "company"], + ) + if len(doc_list): + data["name"] = doc_list[0]["name"] + data["doctype"] = "Student Applicant Fees" + data["company"] = doc_list[0]["company"] + return data def get_fees_default_accounts(company): - data = {"bank": "", "income": "", "currency": ""} - data["currency"] = frappe.get_value("Company", company, "default_currency") or "" - data["bank"] = frappe.get_value("Company", company, "fee_bank_account") or "" - if not data["bank"]: - data["bank"] = ( - frappe.get_value("Company", company, "default_bank_account") or "" - ) - data["income"] = ( - frappe.get_value("Company", company, "student_applicant_fees_revenue_account") - or "" - ) - if not data["income"]: - data["bank"] = ( - frappe.get_value("Company", company, "default_income_account") or "" - ) - if not data["bank"]: - frappe.throw(_("Please set Fee Bank Account in Company {0}".format(company))) - if not data["income"]: - frappe.throw( - _( - "Please set Student Applicant Fees Revenue Account in Company {0}".format( - company - ) - ) - ) - return data + data = {"bank": "", "income": "", "currency": ""} + data["currency"] = frappe.get_value("Company", company, "default_currency") or "" + data["bank"] = frappe.get_value("Company", company, "fee_bank_account") or "" + if not data["bank"]: + data["bank"] = frappe.get_value("Company", company, "default_bank_account") or "" + data["income"] = frappe.get_value("Company", company, "student_applicant_fees_revenue_account") or "" + if not data["income"]: + data["bank"] = frappe.get_value("Company", company, "default_income_account") or "" + if not data["bank"]: + frappe.throw(_(f"Please set Fee Bank Account in Company {company}")) + if not data["income"]: + frappe.throw(_(f"Please set Student Applicant Fees Revenue Account in Company {company}")) + return data @frappe.whitelist() def make_payment_entry_from_call(docname): - nmb_doc = frappe.get_doc("NMB Callback", docname) - make_payment_entry(method="frontend", kwargs=nmb_doc) + nmb_doc = frappe.get_doc("NMB Callback", docname) + make_payment_entry(method="frontend", kwargs=nmb_doc) @frappe.whitelist() def url_fix(url: str, charset: str = "utf-8") -> str: - """Fixes the URL by encoding the non-ASCII characters. - - Args: - url (str): The URL to fix. - charset (str, optional): The charset to use. Defaults to "utf-8". + """Fixes the URL by encoding the non-ASCII characters. - Examples: - >>> url_fix("http://example.com/äöüß") - 'http://example.com/%C3%A4%C3%B6%C3%BC%C3%9F' + Args: + url (str): The URL to fix. + charset (str, optional): The charset to use. Defaults to "utf-8". - >>> url_fix("http://example.com/漢字") - 'http://example.com/%E6%BC%A2%E5%AD%97' + Examples: + >>> url_fix("http://example.com/äöüß") + 'http://example.com/%C3%A4%C3%B6%C3%BC%C3%9F' - >>> url_fix("http://example.com/|pipe") - 'http://example.com/%7Cpipe' + >>> url_fix("http://example.com/漢字") + 'http://example.com/%E6%BC%A2%E5%AD%97' - >>> url_fix("http://example.com/page#fragment with space") - 'http://example.com/page%23fragment%20with%20space' + >>> url_fix("http://example.com/|pipe") + 'http://example.com/%7Cpipe' - >>> url_fix("http://example.com/{curly}) - 'http://example.com/%7Bcurly%7D' + >>> url_fix("http://example.com/page#fragment with space") + 'http://example.com/page%23fragment%20with%20space' - >>> url_fix("http://example.com/[square]) - 'http://example.com/%5Bsquare%5D' + >>> url_fix("http://example.com/{curly}) + 'http://example.com/%7Bcurly%7D' - """ - s = url.replace("\\", "/") + >>> url_fix("http://example.com/[square]) + 'http://example.com/%5Bsquare%5D' - if s.startswith("file://") and s[7:8].isalpha() and s[8:10] in (":/", "|/"): - s = f"file:///{s[7:]}" + """ + s = url.replace("\\", "/") - url = urlparse(s) - path = quote(url.path, safe="/%+$!*'(),") - qs = quote(url.query, safe=":&%=+$!*'(),") - anchor = quote(url.fragment, safe=":&%=+$!*'(),") - return urlunparse((url.scheme, url.netloc, path, qs, "", anchor)) + if s.startswith("file://") and s[7:8].isalpha() and s[8:10] in (":/", "|/"): + s = f"file:///{s[7:]}" + url = urlparse(s) + path = quote(url.path, safe="/%+$!*'(),") + qs = quote(url.query, safe=":&%=+$!*'(),") + anchor = quote(url.fragment, safe=":&%=+$!*'(),") + return urlunparse((url.scheme, url.netloc, path, qs, "", anchor)) diff --git a/csf_tz/budget_check.py b/csf_tz/budget_check.py index f387d339..6c700d94 100644 --- a/csf_tz/budget_check.py +++ b/csf_tz/budget_check.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2025, Aakvatech and contributors # For license information, please see license.txt @@ -12,11 +11,9 @@ budget validation when documents are saved in draft status. """ -from __future__ import unicode_literals import frappe -from frappe import _ -from frappe.utils import flt from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget +from frappe.utils import flt def validate_budget_on_draft(doc, method=None): @@ -114,7 +111,7 @@ def check_budget_before_submit(doctype, docname, setting_field=None): "Journal Entry": "enable_budget_check_button_for_journal_entry", "Material Request": "enable_budget_check_button_for_material_request", "Purchase Order": "enable_budget_check_button_for_purchase_order", - "Purchase Invoice": "enable_budget_check_button_for_purchase_invoice" + "Purchase Invoice": "enable_budget_check_button_for_purchase_invoice", } # Check if doctype is supported @@ -128,7 +125,7 @@ def check_budget_before_submit(doctype, docname, setting_field=None): # Check if budget check feature is enabled for this doctype if field_to_check: try: - is_enabled = frappe.db.get_single_value('CSF TZ Settings', field_to_check) + is_enabled = frappe.db.get_single_value("CSF TZ Settings", field_to_check) if not is_enabled: return except Exception: @@ -181,7 +178,7 @@ def check_budget_for_journal_entry(doc): } # Add other accounting dimensions if present - if hasattr(account, 'project') and account.project: + if hasattr(account, "project") and account.project: args["project"] = account.project # Calculate expense amount (debit - credit) @@ -219,15 +216,17 @@ def check_budget_for_buying_document(doc): else: posting_date = doc.get("posting_date") or doc.get("transaction_date") - args.update({ - "doctype": doc.doctype, - "company": doc.company, - "posting_date": posting_date, - }) + args.update( + { + "doctype": doc.doctype, + "company": doc.company, + "posting_date": posting_date, + } + ) # Ensure project field is included if present # This is critical for project-based budget validation - if hasattr(item, 'project') and item.project: + if hasattr(item, "project") and item.project: args["project"] = item.project # Calculate the item amount for budget validation @@ -237,4 +236,4 @@ def check_budget_for_buying_document(doc): # Let ERPNext's validation raise exceptions naturally # Pass expense_amount so it includes the current draft document's amount - validate_expense_against_budget(args, expense_amount=item_amount) \ No newline at end of file + validate_expense_against_budget(args, expense_amount=item_amount) diff --git a/csf_tz/config/accounts.py b/csf_tz/config/accounts.py index ef7e7903..41a2e12e 100644 --- a/csf_tz/config/accounts.py +++ b/csf_tz/config/accounts.py @@ -1,14 +1,11 @@ -from __future__ import unicode_literals from frappe import _ def get_data(): return [ - { "label": _("General Ledger"), "items": [ - { "type": "report", "is_query_report": True, @@ -16,8 +13,6 @@ def get_data(): "doctype": "GL Entry", "description": _("Accounting journal entries with Multi-Currency."), }, - ] + ], }, - - - ] \ No newline at end of file + ] diff --git a/csf_tz/config/csf_tz.py b/csf_tz/config/csf_tz.py index 24edf77f..1da32716 100644 --- a/csf_tz/config/csf_tz.py +++ b/csf_tz/config/csf_tz.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals from frappe import _ @@ -26,43 +25,43 @@ def get_data(): "type": "report", "name": "TRA Input VAT Returns eFiling", "doctype": "Purchase Invoice", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Withholding Tax Summary on Sales", "doctype": "Sales Invoice", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Withholding Tax Summary on Sales", "doctype": "Sales Invoice", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Withholding Tax Payment Summary", "doctype": "Sales Invoice", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "ITX 230.01.E – Withholding Tax Statement", "doctype": "Purchase Invoice", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Output VAT Reconciliation", "doctype": "EFD Z Report", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Credit Note List", "doctype": "Sales Invoice", - "is_query_report": True + "is_query_report": True, }, ], }, @@ -73,7 +72,7 @@ def get_data(): "type": "report", "name": "Employment History", "doctype": "Employee", - "is_query_report": True + "is_query_report": True, }, ], }, @@ -84,45 +83,45 @@ def get_data(): "type": "report", "name": "Multi-Currency Ledger", "doctype": "GL Entry", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Trial Balance Report in USD", "doctype": "GL Entry", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Itemwise Stock Movement", "doctype": "Stock Ledger Entry", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Warehouse wise Item Balance and Value", "doctype": "Stock Ledger Entry", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Accounts Receivable Multi Currency", "doctype": "Sales Invoice", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Stock Balance pivot warehouse", "doctype": "Stock Ledger Entry", - "is_query_report": True + "is_query_report": True, }, { "type": "report", "name": "Accounts Receivable Summary Multi Currency", "doctype": "Sales Invoice", - "is_query_report": True + "is_query_report": True, }, - ] + ], }, { "label": _("Settings"), diff --git a/csf_tz/config/desktop.py b/csf_tz/config/desktop.py index 73c22f6d..4899c481 100755 --- a/csf_tz/config/desktop.py +++ b/csf_tz/config/desktop.py @@ -1,7 +1,6 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals from frappe import _ + def get_data(): return [ { diff --git a/csf_tz/config/docs.py b/csf_tz/config/docs.py index 281883c9..3204ec75 100755 --- a/csf_tz/config/docs.py +++ b/csf_tz/config/docs.py @@ -7,5 +7,6 @@ # headline = "App that does everything" # sub_heading = "Yes, you got that right the first time, everything" + def get_context(context): context.brand_html = "CSF TZ" diff --git a/csf_tz/config/purchase_and_stock_management.py b/csf_tz/config/purchase_and_stock_management.py index 7df9ed3e..ec209ed7 100644 --- a/csf_tz/config/purchase_and_stock_management.py +++ b/csf_tz/config/purchase_and_stock_management.py @@ -1,5 +1,6 @@ from frappe import _ + def get_data(): return [ { @@ -31,16 +32,12 @@ def get_data(): "name": "Order Tracking", "description": _("Track orders from Suppliers."), }, - { - "type": "doctype", - "name": "Product Quality Inspection", - "label": _("Order Inspection") - }, + {"type": "doctype", "name": "Product Quality Inspection", "label": _("Order Inspection")}, { "type": "doctype", "name": "Purchase Receipt", }, - ] + ], }, { "label": _("Stock Management"), @@ -49,7 +46,7 @@ def get_data(): "type": "doctype", "name": "Stock Entry", }, - { + { "type": "doctype", "name": "Stock Transport", }, @@ -59,7 +56,7 @@ def get_data(): "name": "Stock Ledger", "doctype": "Stock Ledger Entry", }, - ] + ], }, { "label": _("Supplier"), @@ -69,18 +66,14 @@ def get_data(): "name": "Supplier", "description": _("Supplier database."), }, - { - "type": "doctype", - "name": "Supplier Type", - "description": _("Supplier Type master.") - }, + {"type": "doctype", "name": "Supplier Type", "description": _("Supplier Type master.")}, { "type": "doctype", "name": "Project", "label": _("Projects"), - "description": _("Supplier Type master.") + "description": _("Supplier Type master."), }, - ] + ], }, { "label": _("Items and Pricing"), @@ -106,38 +99,18 @@ def get_data(): "type": "doctype", "name": "Past Serial No", "description": _("Past Serial No."), - } - ] + }, + ], }, { "label": _("Purchase Reports"), "icon": "fa fa-list", "items": [ - { - "type": "report", - "is_query_report": True, - "name": "Items To Be Requested" - }, - { - "type": "report", - "is_query_report": True, - "name": "Reordering Items" - }, - { - "type": "report", - "is_query_report": True, - "name": "Pending Ordered Items" - }, - { - "type": "report", - "is_query_report": True, - "name": "Purchase History" - }, - { - "type": "report", - "is_query_report": True, - "name": "Pending Requests" - }, + {"type": "report", "is_query_report": True, "name": "Items To Be Requested"}, + {"type": "report", "is_query_report": True, "name": "Reordering Items"}, + {"type": "report", "is_query_report": True, "name": "Pending Ordered Items"}, + {"type": "report", "is_query_report": True, "name": "Purchase History"}, + {"type": "report", "is_query_report": True, "name": "Pending Requests"}, { "type": "report", "is_query_report": True, @@ -156,11 +129,9 @@ def get_data(): "name": "Supplier Contacts", "label": "Supplier Contacts", "doctype": "Address", - "route_options": { - "party_type": "Supplier" - } + "route_options": {"party_type": "Supplier"}, }, - ] + ], }, { "label": _("Stock Reports"), @@ -169,7 +140,7 @@ def get_data(): "type": "report", "is_query_report": True, "name": "Stock Balance", - "doctype": "Stock Ledger Entry" + "doctype": "Stock Ledger Entry", }, { "type": "report", @@ -177,11 +148,7 @@ def get_data(): "name": "Stock Projected Qty", "doctype": "Item", }, - { - "type": "page", - "name": "stock-balance", - "label": _("Stock Summary") - }, + {"type": "page", "name": "stock-balance", "label": _("Stock Summary")}, { "type": "report", "is_query_report": True, @@ -192,27 +159,27 @@ def get_data(): "type": "report", "is_query_report": True, "name": "Ordered Items To Be Delivered", - "doctype": "Delivery Note" + "doctype": "Delivery Note", }, { "type": "report", "name": "Item Shortage Report", "route": "Report/Bin/Item Shortage Report", - "doctype": "Purchase Receipt" + "doctype": "Purchase Receipt", }, { "type": "report", "is_query_report": True, "name": "Requested Items To Be Transferred", - "doctype": "Material Request" + "doctype": "Material Request", }, { "type": "report", "is_query_report": True, "name": "Itemwise Recommended Reorder Level", - "doctype": "Item" + "doctype": "Item", }, - ] + ], }, { "label": _("Purchase Analytics"), @@ -228,15 +195,15 @@ def get_data(): "type": "report", "is_query_report": True, "name": "Purchase Order Trends", - "doctype": "Purchase Order" + "doctype": "Purchase Order", }, { "type": "report", "is_query_report": True, "name": "Purchase Receipt Trends", - "doctype": "Purchase Receipt" + "doctype": "Purchase Receipt", }, - ] + ], }, { "label": _("Stock Analytics"), @@ -246,14 +213,9 @@ def get_data(): "type": "page", "name": "stock-analytics", "label": _("Stock Analytics"), - "icon": "fa fa-bar-chart" - }, - { - "type": "doctype", - "name": "Bin Setup", - "description": _("Bin Setup for warehouse") + "icon": "fa fa-bar-chart", }, - - ] + {"type": "doctype", "name": "Bin Setup", "description": _("Bin Setup for warehouse")}, + ], }, - ] + ] diff --git a/csf_tz/config/sales_and_marketing.py b/csf_tz/config/sales_and_marketing.py index 3c8a3748..c5effba6 100644 --- a/csf_tz/config/sales_and_marketing.py +++ b/csf_tz/config/sales_and_marketing.py @@ -1,6 +1,6 @@ -from __future__ import unicode_literals from frappe import _ + def get_data(): return [ { @@ -21,19 +21,14 @@ def get_data(): "type": "doctype", "name": "Customer Loan Assistance", "description": _("Customer Loan Assistance."), - } - ] + }, + ], }, { "label": _("Sales"), "icon": "fa fa-star", "items": [ - { - "type": "page", - "name": "pos", - "label": _("POS"), - "description": _("Point of Sale") - }, + {"type": "page", "name": "pos", "label": _("POS"), "description": _("Point of Sale")}, { "type": "doctype", "name": "Quotation", @@ -48,8 +43,8 @@ def get_data(): "type": "doctype", "name": "Sales Invoice", "description": _("Sales Invoices."), - } - ] + }, + ], }, { "label": _("Lead Follow Up"), @@ -58,29 +53,24 @@ def get_data(): { "type": "report", "is_query_report": True, - "label": _('Due Follow Up Communications'), + "label": _("Due Follow Up Communications"), "name": "Due Communications", - "doctype": "Lead" - }, - { - "type": "report", - "is_query_report": True, - "name": "Due Demonstrations", - "doctype": "Lead" + "doctype": "Lead", }, + {"type": "report", "is_query_report": True, "name": "Due Demonstrations", "doctype": "Lead"}, { "type": "report", "is_query_report": True, "name": "Loan Assistance Report", - "doctype": "Customer Loan Assistance" + "doctype": "Customer Loan Assistance", }, { "type": "report", "is_query_report": True, "name": "Item Wise Leads Report", - "doctype": "Quotation" + "doctype": "Quotation", }, - ] + ], }, { "label": _("Stock and Pricing"), @@ -89,20 +79,20 @@ def get_data(): "type": "doctype", "name": "Item Price", "description": _("Multiple Item prices."), - "route": "Report/Item Price" + "route": "Report/Item Price", }, { "type": "report", "is_query_report": True, "name": "Stock Balance", - "doctype": "Stock Ledger Entry" + "doctype": "Stock Ledger Entry", }, { "type": "doctype", "name": "Past Serial No", "description": _("Past Serial No."), - } - ] + }, + ], }, { "label": _("Marketing Reports"), @@ -114,12 +104,7 @@ def get_data(): "route": "List/Lead/Kanban/Sales Pipeline", "label": _("Sales Pipeline"), }, - { - "type": "report", - "is_query_report": True, - "name": "Lead Details", - "doctype": "Lead" - }, + {"type": "report", "is_query_report": True, "name": "Lead Details", "doctype": "Lead"}, { "type": "page", "name": "sales-funnel", @@ -130,41 +115,36 @@ def get_data(): "type": "report", "is_query_report": True, "name": "Customer Addresses And Contacts", - "doctype": "Contact" + "doctype": "Contact", }, { "type": "report", "is_query_report": True, "name": "Inactive Customers", - "doctype": "Sales Order" - }, - { - "type": "report", - "is_query_report": True, - "name": "Campaign Efficiency", - "doctype": "Lead" + "doctype": "Sales Order", }, + {"type": "report", "is_query_report": True, "name": "Campaign Efficiency", "doctype": "Lead"}, { "type": "report", "is_query_report": True, "name": "Lead Owner Efficiency", - "doctype": "Lead" + "doctype": "Lead", }, { "type": "report", "is_query_report": True, "label": _("Follow Up Communications Report"), "name": "Communications", - "doctype": "Lead" + "doctype": "Lead", }, { "type": "report", "is_query_report": True, "label": _("Demonstrations Report"), "name": "Demonstrations", - "doctype": "Lead" - } - ] + "doctype": "Lead", + }, + ], }, { "label": _("Sales Reports"), @@ -174,63 +154,63 @@ def get_data(): "type": "report", "is_query_report": True, "name": "Items Marked For Delivery", - "doctype": "Sales Invoice" + "doctype": "Sales Invoice", }, { "type": "report", "is_query_report": True, "name": "Sales Person-wise Transaction Summary", - "doctype": "Sales Order" + "doctype": "Sales Order", }, { "type": "report", "is_query_report": True, "name": "Item-wise Sales History", - "doctype": "Item" + "doctype": "Item", }, { "type": "report", "is_query_report": True, "name": "Sales Order Trends", - "doctype": "Sales Order" + "doctype": "Sales Order", }, { "type": "report", "is_query_report": True, "name": "Supplier-Wise Sales Analytics", - "doctype": "Stock Ledger Entry" + "doctype": "Stock Ledger Entry", }, { "type": "report", "is_query_report": True, "name": "Payment Plan Report", - "doctype": "Sales Invoice" + "doctype": "Sales Invoice", }, { "type": "report", "is_query_report": True, "name": "Item Wise Sales Order", - "doctype": "Sales Order" + "doctype": "Sales Order", }, { "type": "report", "is_query_report": True, "name": "Payment Plan Summary", - "doctype": "Sales Invoice" + "doctype": "Sales Invoice", }, { "type": "report", "is_query_report": True, "name": "Sales Type Report", - "doctype": "Sales Invoice" + "doctype": "Sales Invoice", }, { "type": "report", "is_query_report": True, "name": "Spare Sales Report", - "doctype": "Sales Invoice" + "doctype": "Sales Invoice", }, - ] + ], }, { "label": _("Customer Reports"), @@ -241,16 +221,16 @@ def get_data(): "is_query_report": True, "name": "Brand Sales Report", "label": "Brandwise Customer Details", - "doctype": "Sales Invoice" + "doctype": "Sales Invoice", }, { "type": "report", "is_query_report": True, "name": "Previous Ams Customer Report", "label": "Old Customers Details", - "doctype": "Sales Invoice" + "doctype": "Sales Invoice", }, - ] + ], }, { "label": _("Setup"), @@ -263,7 +243,7 @@ def get_data(): "link": "Tree/Customer Group", "description": _("Manage Customer Group Tree."), }, - { + { "type": "doctype", "name": "Item Installation Procedures", }, @@ -276,9 +256,9 @@ def get_data(): "type": "doctype", "name": "Loan Procedures", "label": "Supplier Loan Procedures", - "description": _("Loan Procedures.") - } - ] + "description": _("Loan Procedures."), + }, + ], }, { "label": _("Sales Analytics"), @@ -301,15 +281,14 @@ def get_data(): "type": "report", "is_query_report": True, "name": "Quotation Trends", - "doctype": "Quotation" + "doctype": "Quotation", }, { "type": "report", "is_query_report": True, "name": "Sales Order Trends", - "doctype": "Sales Order" + "doctype": "Sales Order", }, - ] - } - + ], + }, ] diff --git a/csf_tz/config/stock.py b/csf_tz/config/stock.py index dfc6ce77..8ee81faf 100644 --- a/csf_tz/config/stock.py +++ b/csf_tz/config/stock.py @@ -1,4 +1,3 @@ -from __future__ import unicode_literals from frappe import _ diff --git a/csf_tz/csf_tz/additional_salary.js b/csf_tz/csf_tz/additional_salary.js index 6991f79c..1b6318f5 100644 --- a/csf_tz/csf_tz/additional_salary.js +++ b/csf_tz/csf_tz/additional_salary.js @@ -41,7 +41,7 @@ frappe.ui.form.on('Additional Salary', { 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/bom_addittional_costs.js b/csf_tz/csf_tz/bom_addittional_costs.js index f631fe41..60b1007e 100644 --- a/csf_tz/csf_tz/bom_addittional_costs.js +++ b/csf_tz/csf_tz/bom_addittional_costs.js @@ -18,5 +18,5 @@ frappe.ui.form.on("BOM", { }; }); }, - + }); diff --git a/csf_tz/csf_tz/company.js b/csf_tz/csf_tz/company.js index ed8aef0e..3de366b9 100644 --- a/csf_tz/csf_tz/company.js +++ b/csf_tz/csf_tz/company.js @@ -1,5 +1,5 @@ frappe.ui.form.on("Company", { - + setup: function(frm) { frm.set_query("default_withholding_payable_account", function() { return { @@ -37,7 +37,7 @@ frappe.ui.form.on("Company", { }); }, - + refresh: function(frm) { frm.add_custom_button(__('Auto create accounts'), function() { frm.trigger("auto_create_account"); @@ -66,7 +66,7 @@ frappe.ui.form.on("Company", { primary_action_label: 'Submit', primary_action(values) { console.log(values); - + frappe.call({ method: 'csf_tz.custom_api.linking_tax_template', args: { @@ -82,15 +82,15 @@ frappe.ui.form.on("Company", { } } }); - + d.hide(); } }); - + d.show(); }, __("Setup")); - - + + }, auto_create_account: function(frm) { @@ -121,7 +121,7 @@ frappe.ui.form.on("Company", { } }) }, - + make_tax_category: function(frm) { frappe.call({ method: 'csf_tz.custom_api.create_tax_category', diff --git a/csf_tz/csf_tz/custom_field.js b/csf_tz/csf_tz/custom_field.js index 459179b9..ce24a929 100644 --- a/csf_tz/csf_tz/custom_field.js +++ b/csf_tz/csf_tz/custom_field.js @@ -34,7 +34,7 @@ frappe.listview_settings['Custom Field'] = { hide_days: doc.hide_days, options: doc.options, sort_options: doc.sort_options, - fetch_if_empty: doc.fetch_if_empty, + fetch_if_empty: doc.fetch_if_empty, fetch_from: doc.fetch_from, collapsible: doc.collapsible, non_negative: doc.non_negative, @@ -79,4 +79,4 @@ frappe.listview_settings['Custom Field'] = { a.remove(); }); } -}; \ No newline at end of file +}; diff --git a/csf_tz/csf_tz/customer.js b/csf_tz/csf_tz/customer.js index 3cdeaa16..3751e049 100644 --- a/csf_tz/csf_tz/customer.js +++ b/csf_tz/csf_tz/customer.js @@ -4,7 +4,7 @@ frappe.ui.form.on("Customer", { - + refresh: function(frm) { @@ -15,7 +15,7 @@ frappe.ui.form.on("Customer", { {party_type:'Customer', party:frm.doc.name}); }); - } + } }, - -}); \ No newline at end of file + +}); diff --git a/csf_tz/csf_tz/dashboard_chart_source/__init__.py b/csf_tz/csf_tz/dashboard_chart_source/__init__.py index 5b40ff09..28097927 100644 --- a/csf_tz/csf_tz/dashboard_chart_source/__init__.py +++ b/csf_tz/csf_tz/dashboard_chart_source/__init__.py @@ -2,6 +2,4 @@ from .multi_account_balance_timeline.multi_account_balance_timeline import MultiBankBalance # Register in module exports -__all__ = [ - 'MultiBankBalance' -] \ No newline at end of file +__all__ = ["MultiBankBalance"] diff --git a/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.py b/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.py index 0cdb3ec7..4f7af189 100644 --- a/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.py +++ b/csf_tz/csf_tz/dashboard_chart_source/multi_account_balance_timeline/multi_account_balance_timeline.py @@ -1,298 +1,309 @@ # Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt +from datetime import datetime, timedelta + import frappe -from frappe import _ -from frappe.utils import getdate, add_days, formatdate, flt, get_datetime -from frappe.utils.dashboard import cache_source from erpnext.accounts.utils import get_balance_on -from datetime import datetime, timedelta -import json +from frappe import _ +from frappe.utils import add_days, flt, formatdate, getdate + @frappe.whitelist() -def get(chart_name=None, chart=None, no_cache=None, filters=None, from_date=None, to_date=None, timespan=None, time_interval=None, heatmap_year=None): - """ - Main entry point for Multi_Account Balance Timeline dashboard chart source - This function is called by Frappe's dashboard framework via CSF_TZ module - """ - multi_balance = MultiBankBalance() - return multi_balance.get(chart_name, chart, no_cache, filters, from_date, to_date) +def get( + chart_name=None, + chart=None, + no_cache=None, + filters=None, + from_date=None, + to_date=None, + timespan=None, + time_interval=None, + heatmap_year=None, +): + """ + Main entry point for Multi_Account Balance Timeline dashboard chart source + This function is called by Frappe's dashboard framework via CSF_TZ module + """ + multi_balance = MultiBankBalance() + return multi_balance.get(chart_name, chart, no_cache, filters, from_date, to_date) + @frappe.whitelist() def get_sample_data(chart_name=None, **kwargs): - """ - Generate sample data for testing when no real transactions exist - This helps users see how the chart would look with data - """ - try: - multi_balance = MultiBankBalance() - return multi_balance.get_sample_data(**kwargs) - except Exception as e: - frappe.log_error(f"get_sample_data failed: {str(e)}", "Sample Data Error") - # Return minimal working sample data - return { - 'labels': ['Day 1', 'Day 2', 'Day 3', 'Day 4', 'Day 5'], - 'datasets': [ - { - 'name': 'Sample Account', - 'values': [1000, 1200, 1100, 1300, 1250], - 'chartType': 'line', - 'color': '#1f77b4' - } - ], - 'type': 'line', - 'is_sample_data': True, - 'sample_message': 'This is minimal sample data for demonstration.', - 'summary': { - 'total_balance': 1250, - 'account_count': 1, - 'highest_balance_account': 'Sample Account', - 'highest_balance': 1250, - 'as_of_date': frappe.utils.formatdate(frappe.utils.today()) - } - } + """ + Generate sample data for testing when no real transactions exist + This helps users see how the chart would look with data + """ + try: + multi_balance = MultiBankBalance() + return multi_balance.get_sample_data(**kwargs) + except Exception as e: + frappe.log_error(f"get_sample_data failed: {str(e)}", "Sample Data Error") + # Return minimal working sample data + return { + "labels": ["Day 1", "Day 2", "Day 3", "Day 4", "Day 5"], + "datasets": [ + { + "name": "Sample Account", + "values": [1000, 1200, 1100, 1300, 1250], + "chartType": "line", + "color": "#1f77b4", + } + ], + "type": "line", + "is_sample_data": True, + "sample_message": "This is minimal sample data for demonstration.", + "summary": { + "total_balance": 1250, + "account_count": 1, + "highest_balance_account": "Sample Account", + "highest_balance": 1250, + "as_of_date": frappe.utils.formatdate(frappe.utils.today()), + }, + } + @frappe.whitelist() def create_sample_accounts(company): - """ - Create sample bank accounts for testing purposes + """ + Create sample bank accounts for testing purposes - Args: - company (str): Company name to create accounts for + Args: + company (str): Company name to create accounts for + + Returns: + list: Created account names + """ + multi_balance = MultiBankBalance() + return multi_balance.create_sample_accounts(company) - Returns: - list: Created account names - """ - multi_balance = MultiBankBalance() - return multi_balance.create_sample_accounts(company) @frappe.whitelist() def debug_chart_data(company=None): - """ - Debug function to test chart data generation step by step - """ - try: - if not company: - company = frappe.defaults.get_global_default("company") or "Test Company" - - multi_balance = MultiBankBalance() - - # Test step by step - result = { - "step": "starting", - "company": company, - "error": None - } - - # Step 1: Test filter validation - try: - filters = multi_balance.validate_and_process_filters({"company": company}) - result["step"] = "filters_validated" - result["filters"] = filters - except Exception as e: - result["error"] = f"Filter validation failed: {str(e)}" - return result - - # Step 2: Test date validation - try: - from_date, to_date = multi_balance.validate_date_range(None, None) - result["step"] = "dates_validated" - result["from_date"] = str(from_date) - result["to_date"] = str(to_date) - except Exception as e: - result["error"] = f"Date validation failed: {str(e)}" - return result - - # Step 3: Test account retrieval - try: - accounts = multi_balance.get_bank_accounts(company, "Bank") - result["step"] = "accounts_retrieved" - result["account_count"] = len(accounts) if accounts else 0 - result["accounts"] = [acc.get('name', 'Unknown') for acc in (accounts or [])] - except Exception as e: - result["error"] = f"Account retrieval failed: {str(e)}" - return result - - # Step 4: Test sample data generation - try: - sample_data = multi_balance.get_sample_data(company=company) - result["step"] = "sample_data_generated" - result["sample_data_keys"] = list(sample_data.keys()) - result["dataset_count"] = len(sample_data.get('datasets', [])) - except Exception as e: - result["error"] = f"Sample data generation failed: {str(e)}" - return result - - result["step"] = "completed_successfully" - return result - - except Exception as e: - return { - "step": "failed", - "error": f"Debug function failed: {str(e)}", - "company": company - } + """ + Debug function to test chart data generation step by step + """ + try: + if not company: + company = frappe.defaults.get_global_default("company") or "Test Company" + + multi_balance = MultiBankBalance() + + # Test step by step + result = {"step": "starting", "company": company, "error": None} + + # Step 1: Test filter validation + try: + filters = multi_balance.validate_and_process_filters({"company": company}) + result["step"] = "filters_validated" + result["filters"] = filters + except Exception as e: + result["error"] = f"Filter validation failed: {str(e)}" + return result + + # Step 2: Test date validation + try: + from_date, to_date = multi_balance.validate_date_range(None, None) + result["step"] = "dates_validated" + result["from_date"] = str(from_date) + result["to_date"] = str(to_date) + except Exception as e: + result["error"] = f"Date validation failed: {str(e)}" + return result + + # Step 3: Test account retrieval + try: + accounts = multi_balance.get_bank_accounts(company, "Bank") + result["step"] = "accounts_retrieved" + result["account_count"] = len(accounts) if accounts else 0 + result["accounts"] = [acc.get("name", "Unknown") for acc in (accounts or [])] + except Exception as e: + result["error"] = f"Account retrieval failed: {str(e)}" + return result + + # Step 4: Test sample data generation + try: + sample_data = multi_balance.get_sample_data(company=company) + result["step"] = "sample_data_generated" + result["sample_data_keys"] = list(sample_data.keys()) + result["dataset_count"] = len(sample_data.get("datasets", [])) + except Exception as e: + result["error"] = f"Sample data generation failed: {str(e)}" + return result + + result["step"] = "completed_successfully" + return result + + except Exception as e: + return {"step": "failed", "error": f"Debug function failed: {str(e)}", "company": company} + class MultiBankBalance: - """ - Custom dashboard chart source for displaying multiple bank account balances - Extends the functionality of the standard Account Balance Timeline - """ - - def get(self, chart_name=None, chart=None, no_cache=None, filters=None, from_date=None, to_date=None): - """ - Main entry point called by Frappe dashboard framework via CSF_TZ module - - Args: - chart_name (str): Name of the dashboard chart - chart (dict): Chart configuration object - no_cache (bool): Whether to bypass cache - filters (dict): Chart filters including company, account_type, etc. - from_date (str): Start date for data retrieval - to_date (str): End date for data retrieval - - Returns: - dict: Formatted chart data with labels and datasets - """ - try: - # Parse filters if they come as JSON string - if isinstance(filters, str): - import json - try: - filters = json.loads(filters) - except (json.JSONDecodeError, TypeError): - filters = {} - - # Validate and process inputs - filters = self.validate_and_process_filters(filters) - from_date, to_date = self.validate_date_range(from_date, to_date) - - # Get bank accounts for the company - accounts = self.get_bank_accounts( - company=filters.get('company'), - account_type=filters.get('account_type', 'Bank'), - include_inactive=filters.get('include_inactive', False), - currency=filters.get('currency') - ) - - # If no accounts found, return empty chart with helpful message - if not accounts or len(accounts) == 0: - return self.empty_chart_data(_("No bank accounts found for the selected criteria. Please create bank accounts first.")) - - # Get balance data for all accounts - balance_data = self.get_account_balances(accounts, from_date, to_date) - - # Format data for chart display - chart_data = self.format_chart_data(balance_data, accounts, from_date, to_date) - - # Check if we have any real data - has_real_data = False - if balance_data: - for date_data in balance_data.values(): - if any(balance != 0 for balance in date_data.values()): - has_real_data = True - break - - # If we have accounts but no real transaction data, add helpful message - if not has_real_data: - chart_data['no_data_message'] = _( - "Found {0} bank account(s) but no transaction data in the selected date range. " - "Create some transactions to see balance trends." - ).format(len(accounts)) - chart_data['empty'] = True - - return chart_data - - except Exception as e: - import traceback - error_msg = f"Error in MultiBankBalance.get: {str(e)}\n{traceback.format_exc()}" - frappe.log_error(error_msg, "Multi_Account Balance Timeline Error") - - # Return empty chart with error message - return self.empty_chart_data(_("Error retrieving bank balance data. Please check the error logs for details.")) - - def validate_and_process_filters(self, filters): - """ - Validate and process chart filters - - Args: - filters (dict): Raw filters from chart - - Returns: - dict: Processed and validated filters - """ - if not filters: - filters = {} - - # Company is required - if not filters.get('company'): - frappe.throw(_("Company filter is required for Multi_Bank Balance chart")) - - # Validate company exists and user has access - if not frappe.db.exists('Company', filters.get('company')): - frappe.throw(_("Invalid company: {0}").format(filters.get('company'))) - - # Set default account type if not provided - if not filters.get('account_type'): - filters['account_type'] = 'Bank' - - # Validate account type - valid_account_types = ['Bank', 'Cash'] - if filters.get('account_type') not in valid_account_types: - filters['account_type'] = 'Bank' - - return filters - - def validate_date_range(self, from_date, to_date): - """ - Validate and set default date range - - Args: - from_date (str): Start date - to_date (str): End date - - Returns: - tuple: (from_date, to_date) as date objects - """ - if not from_date: - from_date = add_days(frappe.utils.today(), -365) # Default to 1 year ago - - if not to_date: - to_date = frappe.utils.today() - - from_date = getdate(from_date) - to_date = getdate(to_date) - - # Ensure from_date is not after to_date - if from_date > to_date: - frappe.throw(_("From Date cannot be after To Date")) - - # Limit the date range to prevent performance issues - max_days = 1095 # 3 years - if (to_date - from_date).days > max_days: - frappe.throw(_("Date range cannot exceed {0} days").format(max_days)) - - return from_date, to_date - - def get_bank_accounts(self, company, account_type="Bank", include_inactive=False, currency=None): - """ - Retrieve all bank accounts for the specified company - - Args: - company (str): Company name - account_type (str): Type of accounts to retrieve (Bank/Cash) - include_inactive (bool): Whether to include disabled accounts - currency (str): Filter by currency (optional) - - Returns: - list: List of account dictionaries - """ - conditions = [] - values = [company, account_type] - - # Base query - query = """ - SELECT + """ + Custom dashboard chart source for displaying multiple bank account balances + Extends the functionality of the standard Account Balance Timeline + """ + + def get(self, chart_name=None, chart=None, no_cache=None, filters=None, from_date=None, to_date=None): + """ + Main entry point called by Frappe dashboard framework via CSF_TZ module + + Args: + chart_name (str): Name of the dashboard chart + chart (dict): Chart configuration object + no_cache (bool): Whether to bypass cache + filters (dict): Chart filters including company, account_type, etc. + from_date (str): Start date for data retrieval + to_date (str): End date for data retrieval + + Returns: + dict: Formatted chart data with labels and datasets + """ + try: + # Parse filters if they come as JSON string + if isinstance(filters, str): + import json + + try: + filters = json.loads(filters) + except (json.JSONDecodeError, TypeError): + filters = {} + + # Validate and process inputs + filters = self.validate_and_process_filters(filters) + from_date, to_date = self.validate_date_range(from_date, to_date) + + # Get bank accounts for the company + accounts = self.get_bank_accounts( + company=filters.get("company"), + account_type=filters.get("account_type", "Bank"), + include_inactive=filters.get("include_inactive", False), + currency=filters.get("currency"), + ) + + # If no accounts found, return empty chart with helpful message + if not accounts or len(accounts) == 0: + return self.empty_chart_data( + _("No bank accounts found for the selected criteria. Please create bank accounts first.") + ) + + # Get balance data for all accounts + balance_data = self.get_account_balances(accounts, from_date, to_date) + + # Format data for chart display + chart_data = self.format_chart_data(balance_data, accounts, from_date, to_date) + + # Check if we have any real data + has_real_data = False + if balance_data: + for date_data in balance_data.values(): + if any(balance != 0 for balance in date_data.values()): + has_real_data = True + break + + # If we have accounts but no real transaction data, add helpful message + if not has_real_data: + chart_data["no_data_message"] = _( + "Found {0} bank account(s) but no transaction data in the selected date range. " + "Create some transactions to see balance trends." + ).format(len(accounts)) + chart_data["empty"] = True + + return chart_data + + except Exception as e: + import traceback + + error_msg = f"Error in MultiBankBalance.get: {str(e)}\n{traceback.format_exc()}" + frappe.log_error(error_msg, "Multi_Account Balance Timeline Error") + + # Return empty chart with error message + return self.empty_chart_data( + _("Error retrieving bank balance data. Please check the error logs for details.") + ) + + def validate_and_process_filters(self, filters): + """ + Validate and process chart filters + + Args: + filters (dict): Raw filters from chart + + Returns: + dict: Processed and validated filters + """ + if not filters: + filters = {} + + # Company is required + if not filters.get("company"): + frappe.throw(_("Company filter is required for Multi_Bank Balance chart")) + + # Validate company exists and user has access + if not frappe.db.exists("Company", filters.get("company")): + frappe.throw(_("Invalid company: {0}").format(filters.get("company"))) + + # Set default account type if not provided + if not filters.get("account_type"): + filters["account_type"] = "Bank" + + # Validate account type + valid_account_types = ["Bank", "Cash"] + if filters.get("account_type") not in valid_account_types: + filters["account_type"] = "Bank" + + return filters + + def validate_date_range(self, from_date, to_date): + """ + Validate and set default date range + + Args: + from_date (str): Start date + to_date (str): End date + + Returns: + tuple: (from_date, to_date) as date objects + """ + if not from_date: + from_date = add_days(frappe.utils.today(), -365) # Default to 1 year ago + + if not to_date: + to_date = frappe.utils.today() + + from_date = getdate(from_date) + to_date = getdate(to_date) + + # Ensure from_date is not after to_date + if from_date > to_date: + frappe.throw(_("From Date cannot be after To Date")) + + # Limit the date range to prevent performance issues + max_days = 1095 # 3 years + if (to_date - from_date).days > max_days: + frappe.throw(_("Date range cannot exceed {0} days").format(max_days)) + + return from_date, to_date + + def get_bank_accounts(self, company, account_type="Bank", include_inactive=False, currency=None): + """ + Retrieve all bank accounts for the specified company + + Args: + company (str): Company name + account_type (str): Type of accounts to retrieve (Bank/Cash) + include_inactive (bool): Whether to include disabled accounts + currency (str): Filter by currency (optional) + + Returns: + list: List of account dictionaries + """ + values = [company, account_type] + + # Base query + query = """ + SELECT name, account_name, account_type, @@ -306,682 +317,684 @@ def get_bank_accounts(self, company, account_type="Bank", include_inactive=False AND account_type = %s AND is_group = 0 """ - - # Add conditions based on parameters - if not include_inactive: - query += " AND disabled = 0" - - if currency: - query += " AND account_currency = %s" - values.append(currency) - - query += " ORDER BY account_name" - - accounts = frappe.db.sql(query, values, as_dict=True) - - # Filter accounts based on user permissions - allowed_accounts = [] - for account in accounts: - if self.has_account_permission(account.name): - allowed_accounts.append(account) - - return allowed_accounts - - def has_account_permission(self, account): - """ - Check if current user has permission to view the account - - Args: - account (str): Account name - - Returns: - bool: True if user has permission - """ - try: - # Use ERPNext's permission system - return frappe.has_permission('Account', 'read', account) - except: - # Default to True if permission check fails - return True - - def get_account_balances(self, accounts, from_date, to_date): - """ - Calculate balances for multiple accounts over time period - - Args: - accounts (list): List of account dictionaries - from_date (date): Start date - to_date (date): End date - - Returns: - dict: Account balances organized by date and account - """ - if not accounts: - return {} - - account_names = [acc['name'] for acc in accounts] - - # Get all GL entries for these accounts in the date range - gl_entries = frappe.db.sql(""" - SELECT + + # Add conditions based on parameters + if not include_inactive: + query += " AND disabled = 0" + + if currency: + query += " AND account_currency = %s" + values.append(currency) + + query += " ORDER BY account_name" + + accounts = frappe.db.sql(query, values, as_dict=True) + + # Filter accounts based on user permissions + allowed_accounts = [] + for account in accounts: + if self.has_account_permission(account.name): + allowed_accounts.append(account) + + return allowed_accounts + + def has_account_permission(self, account): + """ + Check if current user has permission to view the account + + Args: + account (str): Account name + + Returns: + bool: True if user has permission + """ + try: + # Use ERPNext's permission system + return frappe.has_permission("Account", "read", account) + except Exception: + # Default to True if permission check fails + return True + + def get_account_balances(self, accounts, from_date, to_date): + """ + Calculate balances for multiple accounts over time period + + Args: + accounts (list): List of account dictionaries + from_date (date): Start date + to_date (date): End date + + Returns: + dict: Account balances organized by date and account + """ + if not accounts: + return {} + + account_names = [acc["name"] for acc in accounts] + + # Get all GL entries for these accounts in the date range + gl_entries = frappe.db.sql( + """ + SELECT account, posting_date, SUM(debit - credit) as net_amount FROM `tabGL Entry` - WHERE account IN ({0}) + WHERE account IN ({}) AND posting_date BETWEEN %s AND %s AND is_cancelled = 0 GROUP BY account, posting_date ORDER BY posting_date, account - """.format(','.join(['%s'] * len(account_names))), - account_names + [from_date, to_date], as_dict=True) - - # Build running balances - balance_data = {} - account_running_balances = {} - - # Initialize running balances with opening balances - for account in accounts: - opening_balance = get_balance_on(account['name'], from_date) - account_running_balances[account['name']] = flt(opening_balance) - - # Generate date range - date_range = self.get_date_range(from_date, to_date) - - # Initialize balance data structure - for date in date_range: - balance_data[date] = {} - for account in accounts: - balance_data[date][account['name']] = account_running_balances[account['name']] - - # Process GL entries and update running balances - current_date = from_date - for entry in gl_entries: - entry_date = entry['posting_date'] - account = entry['account'] - - # Update running balance for this account - if account in account_running_balances: - account_running_balances[account] += flt(entry['net_amount']) - - # Update all dates from this entry date onwards - for date in date_range: - if date >= entry_date: - balance_data[date][account] = account_running_balances[account] - - return balance_data - - def get_date_range(self, from_date, to_date, interval='daily'): - """ - Generate list of dates between from_date and to_date - - Args: - from_date (date): Start date - to_date (date): End date - interval (str): Date interval (daily, weekly, monthly) - - Returns: - list: List of dates - """ - dates = [] - current_date = from_date - - if interval == 'daily': - while current_date <= to_date: - dates.append(current_date) - current_date = add_days(current_date, 1) - elif interval == 'weekly': - while current_date <= to_date: - dates.append(current_date) - current_date = add_days(current_date, 7) - elif interval == 'monthly': - while current_date <= to_date: - dates.append(current_date) - # Add one month (approximate) - if current_date.month == 12: - current_date = current_date.replace(year=current_date.year + 1, month=1) - else: - current_date = current_date.replace(month=current_date.month + 1) - - # Ensure to_date is included if not already - if dates and dates[-1] != to_date: - dates.append(to_date) - - return dates - - def format_chart_data(self, balance_data, accounts, from_date, to_date): - """ - Format balance data for chart consumption - - Args: - balance_data (dict): Raw balance data by date and account - accounts (list): List of account dictionaries - from_date (date): Start date - to_date (date): End date + """.format(",".join(["%s"] * len(account_names))), + account_names + [from_date, to_date], + as_dict=True, + ) + + # Build running balances + balance_data = {} + account_running_balances = {} + + # Initialize running balances with opening balances + for account in accounts: + opening_balance = get_balance_on(account["name"], from_date) + account_running_balances[account["name"]] = flt(opening_balance) + + # Generate date range + date_range = self.get_date_range(from_date, to_date) + + # Initialize balance data structure + for date in date_range: + balance_data[date] = {} + for account in accounts: + balance_data[date][account["name"]] = account_running_balances[account["name"]] + + # Process GL entries and update running balances + for entry in gl_entries: + entry_date = entry["posting_date"] + account = entry["account"] + + # Update running balance for this account + if account in account_running_balances: + account_running_balances[account] += flt(entry["net_amount"]) + + # Update all dates from this entry date onwards + for date in date_range: + if date >= entry_date: + balance_data[date][account] = account_running_balances[account] + + return balance_data + + def get_date_range(self, from_date, to_date, interval="daily"): + """ + Generate list of dates between from_date and to_date + + Args: + from_date (date): Start date + to_date (date): End date + interval (str): Date interval (daily, weekly, monthly) + + Returns: + list: List of dates + """ + dates = [] + current_date = from_date + + if interval == "daily": + while current_date <= to_date: + dates.append(current_date) + current_date = add_days(current_date, 1) + elif interval == "weekly": + while current_date <= to_date: + dates.append(current_date) + current_date = add_days(current_date, 7) + elif interval == "monthly": + while current_date <= to_date: + dates.append(current_date) + # Add one month (approximate) + if current_date.month == 12: + current_date = current_date.replace(year=current_date.year + 1, month=1) + else: + current_date = current_date.replace(month=current_date.month + 1) + + # Ensure to_date is included if not already + if dates and dates[-1] != to_date: + dates.append(to_date) + + return dates + + def format_chart_data(self, balance_data, accounts, from_date, to_date): + """ + Format balance data for chart consumption + + Args: + balance_data (dict): Raw balance data by date and account + accounts (list): List of account dictionaries + from_date (date): Start date + to_date (date): End date + + Returns: + dict: Formatted chart data + """ + if not balance_data or not accounts: + return self.empty_chart_data(_("No data available")) + + # Prepare labels (dates) + labels = [] + dates = sorted(balance_data.keys()) + + # Ensure we have valid dates + if not dates: + return self.empty_chart_data(_("No date data available")) + + for date in dates: + labels.append(formatdate(date, "MMM d")) + + # Prepare datasets (one per account) + datasets = [] + colors = self.get_chart_colors(len(accounts)) + + for idx, account in enumerate(accounts): + account_name = account["name"] + account_label = account["account_name"] or account_name + + # Ensure account_label is not empty + if not account_label or account_label.strip() == "": + account_label = f"Account {idx + 1}" + + # Get balance values for this account + values = [] + for date in dates: + balance = balance_data.get(date, {}).get(account_name, 0) + # Ensure balance is a valid number + balance_value = flt(balance, 2) + values.append(balance_value) + + # Ensure we have valid color + color = colors[idx % len(colors)] + if not color or color.strip() == "": + color = "#1f77b4" # Default blue color + + # Create dataset for this account + dataset = { + "name": str(account_label), # Ensure string + "values": values, + "chartType": "line", + "color": color, + } + datasets.append(dataset) + + # Ensure we have at least one dataset + if not datasets: + return self.empty_chart_data(_("No account data available")) + + # Calculate summary statistics + summary = self.calculate_summary_stats(balance_data, accounts) + + chart_data = { + "labels": labels, + "datasets": datasets, + "type": "line", + "summary": summary, + "account_count": len(accounts), + } + + return chart_data + + def get_sample_data(self, **kwargs): + """ + Generate sample data for testing and demonstration purposes + + Args: + **kwargs: Optional parameters (company, account_type, etc.) + + Returns: + dict: Sample chart data with realistic-looking balance trends + """ + import random + + # Create sample accounts + sample_accounts = [ + {"name": "Sample Bank Account 1", "account_name": "Main Checking Account"}, + {"name": "Sample Bank Account 2", "account_name": "Business Savings Account"}, + {"name": "Sample Bank Account 3", "account_name": "Petty Cash Account"}, + ] + + # Generate date range (last 30 days) + end_date = datetime.now().date() + start_date = end_date - timedelta(days=30) + + # Generate sample balance data + sample_balance_data = {} + current_date = start_date + + # Starting balances for each account + account_balances = { + "Sample Bank Account 1": 50000.00, # Main checking starts high + "Sample Bank Account 2": 25000.00, # Savings moderate + "Sample Bank Account 3": 2000.00, # Petty cash low + } + + while current_date <= end_date: + daily_balances = {} + + for account in sample_accounts: + account_name = account["name"] + current_balance = account_balances[account_name] + + # Simulate realistic daily changes + if account_name == "Sample Bank Account 1": # Main checking - more volatile + change = random.uniform(-2000, 3000) + elif account_name == "Sample Bank Account 2": # Savings - stable growth + change = random.uniform(-100, 200) + else: # Petty cash - small changes + change = random.uniform(-50, 100) + + # Apply change and ensure minimum balance + new_balance = max(0, current_balance + change) + account_balances[account_name] = new_balance + daily_balances[account_name] = new_balance + + sample_balance_data[current_date] = daily_balances + current_date += timedelta(days=1) + + # Format the sample data using existing formatting method + chart_data = self.format_chart_data(sample_balance_data, sample_accounts, start_date, end_date) + + # Add sample data indicator + chart_data["is_sample_data"] = True + chart_data["sample_message"] = _( + "This is sample data for demonstration. Create bank accounts and transactions to see real data." + ) + + return chart_data + + @frappe.whitelist() + def create_sample_accounts(self, company): + """ + Create sample bank accounts for testing purposes + + Args: + company (str): Company name to create accounts for + + Returns: + list: Created account names + """ + if not frappe.has_permission("Account", "create"): + frappe.throw(_("You don't have permission to create accounts")) + + # Check if company exists + if not frappe.db.exists("Company", company): + frappe.throw(_("Company {0} does not exist").format(company)) + + # Get company's chart of accounts root + company_doc = frappe.get_doc("Company", company) + + # Find or create Bank Accounts group + bank_accounts_group = None + try: + bank_accounts_group = frappe.db.get_value( + "Account", {"company": company, "account_name": "Bank Accounts", "is_group": 1} + ) + except Exception: + pass + + if not bank_accounts_group: + # Create Bank Accounts group under Assets + assets_account = frappe.db.get_value( + "Account", {"company": company, "account_name": "Assets", "is_group": 1} + ) + + if assets_account: + bank_group = frappe.get_doc( + { + "doctype": "Account", + "account_name": "Bank Accounts", + "parent_account": assets_account, + "company": company, + "is_group": 1, + "account_type": "Bank", + } + ) + bank_group.insert() + bank_accounts_group = bank_group.name + + # Sample accounts to create + sample_accounts = [ + {"account_name": "Sample Checking Account", "account_type": "Bank"}, + {"account_name": "Sample Savings Account", "account_type": "Bank"}, + {"account_name": "Sample Cash Account", "account_type": "Cash"}, + ] + + created_accounts = [] + + for account_info in sample_accounts: + account_name = f"{account_info['account_name']} - {company}" + + # Check if account already exists + if frappe.db.exists("Account", account_name): + continue + + try: + account = frappe.get_doc( + { + "doctype": "Account", + "account_name": account_info["account_name"], + "parent_account": bank_accounts_group, + "company": company, + "is_group": 0, + "account_type": account_info["account_type"], + "account_currency": company_doc.default_currency, + } + ) + account.insert() + created_accounts.append(account.name) + + except Exception as e: + frappe.log_error(f"Failed to create sample account {account_info['account_name']}: {str(e)}") + + if created_accounts: + frappe.msgprint( + _("Created {0} sample accounts: {1}").format( + len(created_accounts), ", ".join([acc.split(" - ")[0] for acc in created_accounts]) + ), + title=_("Sample Accounts Created"), + indicator="green", + ) + + return created_accounts + + def calculate_summary_stats(self, balance_data, accounts): + """ + Calculate summary statistics for the chart + + Args: + balance_data (dict): Balance data + accounts (list): Account list + + Returns: + dict: Summary statistics + """ + if not balance_data: + return {} + + dates = sorted(balance_data.keys()) + latest_date = dates[-1] if dates else None + + if not latest_date: + return {} + + latest_balances = balance_data.get(latest_date, {}) + total_balance = sum(flt(balance) for balance in latest_balances.values()) + + # Find account with highest balance + max_balance = 0 + max_account = "" + for account in accounts: + balance = latest_balances.get(account["name"], 0) + if balance > max_balance: + max_balance = balance + max_account = account["account_name"] or account["name"] + + return { + "total_balance": flt(total_balance, 2), + "account_count": len(accounts), + "highest_balance_account": max_account, + "highest_balance": flt(max_balance, 2), + "as_of_date": formatdate(latest_date), + } + + def get_chart_colors(self, count): + """ + Get color palette for chart lines + + Args: + count (int): Number of colors needed + + Returns: + list: List of color codes + """ + # Ensure count is valid + if not count or count <= 0: + count = 1 + + base_colors = [ + "#1f77b4", # Blue + "#ff7f0e", # Orange + "#2ca02c", # Green + "#d62728", # Red + "#9467bd", # Purple + "#8c564b", # Brown + "#e377c2", # Pink + "#7f7f7f", # Gray + "#bcbd22", # Olive + "#17becf", # Cyan + ] + + # Ensure we have valid base colors + if not base_colors: + base_colors = ["#1f77b4"] # Fallback to blue + + # Extend colors if more are needed + colors = [] + for i in range(count): + color = base_colors[i % len(base_colors)] + # Ensure color is valid + if not color or not isinstance(color, str) or color.strip() == "": + color = "#1f77b4" # Default blue + colors.append(color) + + return colors + + def empty_chart_data(self, message): + """ + Return empty chart data with message + + Args: + message (str): Message to display + + Returns: + dict: Empty chart data structure + """ + # Ensure message is a string + if not message or not isinstance(message, str): + message = "No data available" + + return { + "labels": [], + "datasets": [], + "type": "line", + "message": str(message), + "empty": True, + "summary": { + "total_balance": 0, + "account_count": 0, + "highest_balance_account": "", + "highest_balance": 0, + "as_of_date": formatdate(frappe.utils.today()), + }, + } - Returns: - dict: Formatted chart data - """ - if not balance_data or not accounts: - return self.empty_chart_data(_("No data available")) - - # Prepare labels (dates) - labels = [] - dates = sorted(balance_data.keys()) - - # Ensure we have valid dates - if not dates: - return self.empty_chart_data(_("No date data available")) - - for date in dates: - labels.append(formatdate(date, "MMM d")) - - # Prepare datasets (one per account) - datasets = [] - colors = self.get_chart_colors(len(accounts)) - - for idx, account in enumerate(accounts): - account_name = account['name'] - account_label = account['account_name'] or account_name - - # Ensure account_label is not empty - if not account_label or account_label.strip() == '': - account_label = f"Account {idx + 1}" - - # Get balance values for this account - values = [] - for date in dates: - balance = balance_data.get(date, {}).get(account_name, 0) - # Ensure balance is a valid number - balance_value = flt(balance, 2) - values.append(balance_value) - - # Ensure we have valid color - color = colors[idx % len(colors)] - if not color or color.strip() == '': - color = '#1f77b4' # Default blue color - - # Create dataset for this account - dataset = { - 'name': str(account_label), # Ensure string - 'values': values, - 'chartType': 'line', - 'color': color - } - datasets.append(dataset) - - # Ensure we have at least one dataset - if not datasets: - return self.empty_chart_data(_("No account data available")) - - # Calculate summary statistics - summary = self.calculate_summary_stats(balance_data, accounts) - - chart_data = { - 'labels': labels, - 'datasets': datasets, - 'type': 'line', - 'summary': summary, - 'account_count': len(accounts) - } - - return chart_data - - def get_sample_data(self, **kwargs): - """ - Generate sample data for testing and demonstration purposes - Args: - **kwargs: Optional parameters (company, account_type, etc.) - - Returns: - dict: Sample chart data with realistic-looking balance trends - """ - import random - from datetime import datetime, timedelta - - # Create sample accounts - sample_accounts = [ - {'name': 'Sample Bank Account 1', 'account_name': 'Main Checking Account'}, - {'name': 'Sample Bank Account 2', 'account_name': 'Business Savings Account'}, - {'name': 'Sample Bank Account 3', 'account_name': 'Petty Cash Account'}, - ] - - # Generate date range (last 30 days) - end_date = datetime.now().date() - start_date = end_date - timedelta(days=30) - - # Generate sample balance data - sample_balance_data = {} - current_date = start_date - - # Starting balances for each account - account_balances = { - 'Sample Bank Account 1': 50000.00, # Main checking starts high - 'Sample Bank Account 2': 25000.00, # Savings moderate - 'Sample Bank Account 3': 2000.00, # Petty cash low - } - - while current_date <= end_date: - daily_balances = {} - - for account in sample_accounts: - account_name = account['name'] - current_balance = account_balances[account_name] - - # Simulate realistic daily changes - if account_name == 'Sample Bank Account 1': # Main checking - more volatile - change = random.uniform(-2000, 3000) - elif account_name == 'Sample Bank Account 2': # Savings - stable growth - change = random.uniform(-100, 200) - else: # Petty cash - small changes - change = random.uniform(-50, 100) - - # Apply change and ensure minimum balance - new_balance = max(0, current_balance + change) - account_balances[account_name] = new_balance - daily_balances[account_name] = new_balance - - sample_balance_data[current_date] = daily_balances - current_date += timedelta(days=1) - - # Format the sample data using existing formatting method - chart_data = self.format_chart_data(sample_balance_data, sample_accounts, start_date, end_date) - - # Add sample data indicator - chart_data['is_sample_data'] = True - chart_data['sample_message'] = _("This is sample data for demonstration. Create bank accounts and transactions to see real data.") - - return chart_data - - @frappe.whitelist() - def create_sample_accounts(self, company): - """ - Create sample bank accounts for testing purposes +# Additional utility functions for the Multi_Bank Balance source - Args: - company (str): Company name to create accounts for - Returns: - list: Created account names - """ - if not frappe.has_permission("Account", "create"): - frappe.throw(_("You don't have permission to create accounts")) - - # Check if company exists - if not frappe.db.exists("Company", company): - frappe.throw(_("Company {0} does not exist").format(company)) - - # Get company's chart of accounts root - company_doc = frappe.get_doc("Company", company) - - # Find or create Bank Accounts group - bank_accounts_group = None - try: - bank_accounts_group = frappe.db.get_value("Account", { - "company": company, - "account_name": "Bank Accounts", - "is_group": 1 - }) - except: - pass - - if not bank_accounts_group: - # Create Bank Accounts group under Assets - assets_account = frappe.db.get_value("Account", { - "company": company, - "account_name": "Assets", - "is_group": 1 - }) - - if assets_account: - bank_group = frappe.get_doc({ - "doctype": "Account", - "account_name": "Bank Accounts", - "parent_account": assets_account, - "company": company, - "is_group": 1, - "account_type": "Bank" - }) - bank_group.insert() - bank_accounts_group = bank_group.name - - # Sample accounts to create - sample_accounts = [ - { - "account_name": "Sample Checking Account", - "account_type": "Bank" - }, - { - "account_name": "Sample Savings Account", - "account_type": "Bank" - }, - { - "account_name": "Sample Cash Account", - "account_type": "Cash" - } - ] - - created_accounts = [] - - for account_info in sample_accounts: - account_name = f"{account_info['account_name']} - {company}" - - # Check if account already exists - if frappe.db.exists("Account", account_name): - continue - - try: - account = frappe.get_doc({ - "doctype": "Account", - "account_name": account_info['account_name'], - "parent_account": bank_accounts_group, - "company": company, - "is_group": 0, - "account_type": account_info['account_type'], - "account_currency": company_doc.default_currency - }) - account.insert() - created_accounts.append(account.name) - - except Exception as e: - frappe.log_error(f"Failed to create sample account {account_info['account_name']}: {str(e)}") - - if created_accounts: - frappe.msgprint( - _("Created {0} sample accounts: {1}").format( - len(created_accounts), - ", ".join([acc.split(" - ")[0] for acc in created_accounts]) - ), - title=_("Sample Accounts Created"), - indicator="green" - ) - - return created_accounts - - def calculate_summary_stats(self, balance_data, accounts): - """ - Calculate summary statistics for the chart - - Args: - balance_data (dict): Balance data - accounts (list): Account list - - Returns: - dict: Summary statistics - """ - if not balance_data: - return {} - - dates = sorted(balance_data.keys()) - latest_date = dates[-1] if dates else None - - if not latest_date: - return {} - - latest_balances = balance_data.get(latest_date, {}) - total_balance = sum(flt(balance) for balance in latest_balances.values()) - - # Find account with highest balance - max_balance = 0 - max_account = "" - for account in accounts: - balance = latest_balances.get(account['name'], 0) - if balance > max_balance: - max_balance = balance - max_account = account['account_name'] or account['name'] - - return { - 'total_balance': flt(total_balance, 2), - 'account_count': len(accounts), - 'highest_balance_account': max_account, - 'highest_balance': flt(max_balance, 2), - 'as_of_date': formatdate(latest_date) - } - - def get_chart_colors(self, count): - """ - Get color palette for chart lines +def get_default_bank_account(company): + """ + Get the default bank account for a company - Args: - count (int): Number of colors needed + Args: + company (str): Company name - Returns: - list: List of color codes - """ - # Ensure count is valid - if not count or count <= 0: - count = 1 - - base_colors = [ - '#1f77b4', # Blue - '#ff7f0e', # Orange - '#2ca02c', # Green - '#d62728', # Red - '#9467bd', # Purple - '#8c564b', # Brown - '#e377c2', # Pink - '#7f7f7f', # Gray - '#bcbd22', # Olive - '#17becf', # Cyan - ] - - # Ensure we have valid base colors - if not base_colors: - base_colors = ['#1f77b4'] # Fallback to blue - - # Extend colors if more are needed - colors = [] - for i in range(count): - color = base_colors[i % len(base_colors)] - # Ensure color is valid - if not color or not isinstance(color, str) or color.strip() == '': - color = '#1f77b4' # Default blue - colors.append(color) - - return colors - - def empty_chart_data(self, message): - """ - Return empty chart data with message + Returns: + str: Default bank account name + """ + return frappe.db.get_value("Company", company, "default_bank_account") - Args: - message (str): Message to display - Returns: - dict: Empty chart data structure - """ - # Ensure message is a string - if not message or not isinstance(message, str): - message = "No data available" - - return { - 'labels': [], - 'datasets': [], - 'type': 'line', - 'message': str(message), - 'empty': True, - 'summary': { - 'total_balance': 0, - 'account_count': 0, - 'highest_balance_account': '', - 'highest_balance': 0, - 'as_of_date': formatdate(frappe.utils.today()) - } - } +def validate_chart_permissions(chart_name): + """ + Validate if user has permission to view the chart -# Additional utility functions for the Multi_Bank Balance source + Args: + chart_name (str): Chart name -def get_default_bank_account(company): - """ - Get the default bank account for a company - - Args: - company (str): Company name - - Returns: - str: Default bank account name - """ - return frappe.db.get_value('Company', company, 'default_bank_account') + Returns: + bool: Permission status + """ + return frappe.has_permission("Dashboard Chart", "read", chart_name) -def validate_chart_permissions(chart_name): - """ - Validate if user has permission to view the chart - - Args: - chart_name (str): Chart name - - Returns: - bool: Permission status - """ - return frappe.has_permission('Dashboard Chart', 'read', chart_name) def get_account_currencies(company): - """ - Get all currencies used in bank accounts for a company + """ + Get all currencies used in bank accounts for a company - Args: - company (str): Company name + Args: + company (str): Company name - Returns: - list: List of currencies - """ - return frappe.db.sql(""" + Returns: + list: List of currencies + """ + return frappe.db.sql( + """ SELECT DISTINCT account_currency FROM `tabAccount` WHERE company = %s AND account_type IN ('Bank', 'Cash') AND account_currency IS NOT NULL ORDER BY account_currency - """, company, pluck=True) + """, + company, + pluck=True, + ) + @frappe.whitelist() def create_test_transactions(company, account_name=None, amount=10000): - """ - Create test transactions for a bank account to generate chart data - - Args: - company (str): Company name - account_name (str): Specific account name (optional) - amount (float): Transaction amount (default: 10000) - - Returns: - dict: Result with created transactions - """ - if not frappe.has_permission("Journal Entry", "create"): - frappe.throw(_("You don't have permission to create Journal Entries")) - - # Get bank accounts - if account_name: - accounts = [{"name": account_name}] - else: - accounts = frappe.db.get_all("Account", - filters={ - "company": company, - "account_type": ["in", ["Bank", "Cash"]], - "is_group": 0, - "disabled": 0 - }, - fields=["name", "account_name"], - limit=3 # Limit to first 3 accounts - ) - - if not accounts: - frappe.throw(_("No bank accounts found for company {0}").format(company)) - - # Get a cash account for balancing entries - cash_account = frappe.db.get_value("Account", { - "company": company, - "account_type": "Cash", - "is_group": 0, - "disabled": 0 - }) - - if not cash_account: - # Try to find any asset account for balancing - cash_account = frappe.db.get_value("Account", { - "company": company, - "root_type": "Asset", - "is_group": 0, - "disabled": 0 - }) - - if not cash_account: - frappe.throw(_("No cash or asset account found for balancing entries")) - - created_entries = [] - - # Create test transactions for each account - for account in accounts: - try: - # Create a deposit transaction - je = frappe.get_doc({ - "doctype": "Journal Entry", - "company": company, - "posting_date": frappe.utils.add_days(frappe.utils.today(), -30), - "entry_type": "Bank Entry", - "accounts": [ - { - "account": account["name"], - "debit_in_account_currency": float(amount), - "credit_in_account_currency": 0 - }, - { - "account": cash_account, - "debit_in_account_currency": 0, - "credit_in_account_currency": float(amount) - } - ] - }) - je.insert() - je.submit() - created_entries.append({ - "journal_entry": je.name, - "account": account["name"], - "amount": amount, - "type": "deposit" - }) - - # Create a withdrawal transaction - je2 = frappe.get_doc({ - "doctype": "Journal Entry", - "company": company, - "posting_date": frappe.utils.add_days(frappe.utils.today(), -15), - "entry_type": "Bank Entry", - "accounts": [ - { - "account": account["name"], - "debit_in_account_currency": 0, - "credit_in_account_currency": float(amount) * 0.3 # 30% withdrawal - }, - { - "account": cash_account, - "debit_in_account_currency": float(amount) * 0.3, - "credit_in_account_currency": 0 - } - ] - }) - je2.insert() - je2.submit() - created_entries.append({ - "journal_entry": je2.name, - "account": account["name"], - "amount": float(amount) * 0.3, - "type": "withdrawal" - }) - - except Exception as e: - frappe.log_error(f"Failed to create test transaction for {account['name']}: {str(e)}") - - if created_entries: - frappe.msgprint( - _("Created {0} test transactions. You can now view the Multi_Account Balance Timeline chart with real data.").format(len(created_entries)), - title=_("Test Transactions Created"), - indicator="green" - ) - - return { - "success": True, - "created_entries": created_entries, - "message": f"Created {len(created_entries)} test transactions" - } \ No newline at end of file + """ + Create test transactions for a bank account to generate chart data + + Args: + company (str): Company name + account_name (str): Specific account name (optional) + amount (float): Transaction amount (default: 10000) + + Returns: + dict: Result with created transactions + """ + if not frappe.has_permission("Journal Entry", "create"): + frappe.throw(_("You don't have permission to create Journal Entries")) + + # Get bank accounts + if account_name: + accounts = [{"name": account_name}] + else: + accounts = frappe.db.get_all( + "Account", + filters={ + "company": company, + "account_type": ["in", ["Bank", "Cash"]], + "is_group": 0, + "disabled": 0, + }, + fields=["name", "account_name"], + limit=3, # Limit to first 3 accounts + ) + + if not accounts: + frappe.throw(_("No bank accounts found for company {0}").format(company)) + + # Get a cash account for balancing entries + cash_account = frappe.db.get_value( + "Account", {"company": company, "account_type": "Cash", "is_group": 0, "disabled": 0} + ) + + if not cash_account: + # Try to find any asset account for balancing + cash_account = frappe.db.get_value( + "Account", {"company": company, "root_type": "Asset", "is_group": 0, "disabled": 0} + ) + + if not cash_account: + frappe.throw(_("No cash or asset account found for balancing entries")) + + created_entries = [] + + # Create test transactions for each account + for account in accounts: + try: + # Create a deposit transaction + je = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": company, + "posting_date": frappe.utils.add_days(frappe.utils.today(), -30), + "entry_type": "Bank Entry", + "accounts": [ + { + "account": account["name"], + "debit_in_account_currency": float(amount), + "credit_in_account_currency": 0, + }, + { + "account": cash_account, + "debit_in_account_currency": 0, + "credit_in_account_currency": float(amount), + }, + ], + } + ) + je.insert() + je.submit() + created_entries.append( + {"journal_entry": je.name, "account": account["name"], "amount": amount, "type": "deposit"} + ) + + # Create a withdrawal transaction + je2 = frappe.get_doc( + { + "doctype": "Journal Entry", + "company": company, + "posting_date": frappe.utils.add_days(frappe.utils.today(), -15), + "entry_type": "Bank Entry", + "accounts": [ + { + "account": account["name"], + "debit_in_account_currency": 0, + "credit_in_account_currency": float(amount) * 0.3, # 30% withdrawal + }, + { + "account": cash_account, + "debit_in_account_currency": float(amount) * 0.3, + "credit_in_account_currency": 0, + }, + ], + } + ) + je2.insert() + je2.submit() + created_entries.append( + { + "journal_entry": je2.name, + "account": account["name"], + "amount": float(amount) * 0.3, + "type": "withdrawal", + } + ) + + except Exception as e: + frappe.log_error(f"Failed to create test transaction for {account['name']}: {str(e)}") + + if created_entries: + frappe.msgprint( + _( + "Created {0} test transactions. You can now view the Multi_Account Balance Timeline chart with real data." + ).format(len(created_entries)), + title=_("Test Transactions Created"), + indicator="green", + ) + + return { + "success": True, + "created_entries": created_entries, + "message": f"Created {len(created_entries)} test transactions", + } diff --git a/csf_tz/csf_tz/delivery_note.js b/csf_tz/csf_tz/delivery_note.js index 986d5d30..4abb786c 100644 --- a/csf_tz/csf_tz/delivery_note.js +++ b/csf_tz/csf_tz/delivery_note.js @@ -1,6 +1,6 @@ frappe.ui.keys.add_shortcut({ shortcut: 'ctrl+q', - action: () => { + action: () => { const current_doc = $('.data-row.editable-row').parent().attr("data-name"); const item_row = locals["Delivery Note Item"][current_doc]; frappe.call({ @@ -62,7 +62,7 @@ frappe.ui.keys.add_shortcut({ tr.find('.check-warehouse').attr('data-batchQty',element.actual_qty); } tbody.find('.check-warehouse').on('change', function() { - $('input.check-warehouse').not(this).prop('checked', false); + $('input.check-warehouse').not(this).prop('checked', false); }); }); d.set_primary_action("Select", function() { @@ -76,13 +76,13 @@ frappe.ui.keys.add_shortcut({ cur_frm.refresh_fields(); }); cur_frm.rec_dialog = d; - d.show(); + d.show(); } else { frappe.show_alert({message:__('There is No Records'), indicator:'red'}, 5); } } - }); + }); }, page: this.page, description: __('Select Item Warehouse'), @@ -141,8 +141,8 @@ frappe.ui.form.on("Delivery Note", { frm.trigger("tax_category"); } } - }); + }); } - }, 1000); + }, 1000); }, }); diff --git a/csf_tz/csf_tz/doctype/authority_notification_role/__init__.py b/csf_tz/csf_tz/doctype/authority_notification_role/__init__.py index 8b137891..e69de29b 100644 --- a/csf_tz/csf_tz/doctype/authority_notification_role/__init__.py +++ b/csf_tz/csf_tz/doctype/authority_notification_role/__init__.py @@ -1 +0,0 @@ - diff --git a/csf_tz/csf_tz/doctype/authority_notification_role/authority_notification_role.py b/csf_tz/csf_tz/doctype/authority_notification_role/authority_notification_role.py index cfbad2f5..0c85e85a 100644 --- a/csf_tz/csf_tz/doctype/authority_notification_role/authority_notification_role.py +++ b/csf_tz/csf_tz/doctype/authority_notification_role/authority_notification_role.py @@ -5,4 +5,4 @@ class AuthorityNotificationRole(Document): - pass + pass diff --git a/csf_tz/csf_tz/doctype/bank_charges_pattern/bank_charges_pattern.py b/csf_tz/csf_tz/doctype/bank_charges_pattern/bank_charges_pattern.py index 7c96fd3e..ec45cff9 100644 --- a/csf_tz/csf_tz/doctype/bank_charges_pattern/bank_charges_pattern.py +++ b/csf_tz/csf_tz/doctype/bank_charges_pattern/bank_charges_pattern.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class BankChargesPattern(Document): pass diff --git a/csf_tz/csf_tz/doctype/bank_charges_pattern/test_bank_charges_pattern.py b/csf_tz/csf_tz/doctype/bank_charges_pattern/test_bank_charges_pattern.py index 76ea0a59..c2c97848 100644 --- a/csf_tz/csf_tz/doctype/bank_charges_pattern/test_bank_charges_pattern.py +++ b/csf_tz/csf_tz/doctype/bank_charges_pattern/test_bank_charges_pattern.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestBankChargesPattern(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/csf_api_response_log/csf_api_response_log.py b/csf_tz/csf_tz/doctype/csf_api_response_log/csf_api_response_log.py index b2f08202..17936924 100644 --- a/csf_tz/csf_tz/doctype/csf_api_response_log/csf_api_response_log.py +++ b/csf_tz/csf_tz/doctype/csf_api_response_log/csf_api_response_log.py @@ -1,32 +1,30 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe.model.document import Document class CSFAPIResponseLog(Document): - pass + pass def add_log( - request_type, - request_url, - request_header=None, - request_body=None, - response_data=None, - status_code=None, + request_type, + request_url, + request_header=None, + request_body=None, + response_data=None, + status_code=None, ): - doc = frappe.new_doc("CSF API Response Log") - doc.request_type = str(request_type) - doc.request_url = str(request_url) - doc.request_header = str(request_header) or "" - doc.request_body = str(request_body) or "" - doc.response_data = str(response_data) or "" - doc.user_id = frappe.session.user - doc.status_code = status_code or "" - doc.save(ignore_permissions=True) - frappe.db.commit() - return doc.name + doc = frappe.new_doc("CSF API Response Log") + doc.request_type = str(request_type) + doc.request_url = str(request_url) + doc.request_header = str(request_header) or "" + doc.request_body = str(request_body) or "" + doc.response_data = str(response_data) or "" + doc.user_id = frappe.session.user + doc.status_code = status_code or "" + doc.save(ignore_permissions=True) + frappe.db.commit() + return doc.name diff --git a/csf_tz/csf_tz/doctype/csf_api_response_log/test_csf_api_response_log.py b/csf_tz/csf_tz/doctype/csf_api_response_log/test_csf_api_response_log.py index f6384cca..22a9cae9 100644 --- a/csf_tz/csf_tz/doctype/csf_api_response_log/test_csf_api_response_log.py +++ b/csf_tz/csf_tz/doctype/csf_api_response_log/test_csf_api_response_log.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Aakvatech and Contributors # See license.txt -from __future__ import unicode_literals # import frappe import unittest + class TestCSFAPIResponseLog(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/csf_tz_bank_charges/csf_tz_bank_charges.py b/csf_tz/csf_tz/doctype/csf_tz_bank_charges/csf_tz_bank_charges.py index 4fda7bb6..c24aa30c 100644 --- a/csf_tz/csf_tz/doctype/csf_tz_bank_charges/csf_tz_bank_charges.py +++ b/csf_tz/csf_tz/doctype/csf_tz_bank_charges/csf_tz_bank_charges.py @@ -2,140 +2,131 @@ # For license information, please see license.txt import frappe +from erpnext.accounts.party import get_party_account from frappe import _ from frappe.model.document import Document -from erpnext.accounts.party import get_party_account class CSFTZBankCharges(Document): - def validate(self): - self.total_bank_charges = sum( - row.debit_amount - for row in self.get("csf_tz_bank_charges_detail") - if row.debit_amount > 0 - ) + def validate(self): + self.total_bank_charges = sum( + row.debit_amount for row in self.get("csf_tz_bank_charges_detail") if row.debit_amount > 0 + ) - def on_submit(self): - self.payments = [] - for detail in self.get("csf_tz_bank_charges_detail"): - pe_details = self.create_pe( - detail.debit_amount, detail.value_date, detail.reference_number - ) + def on_submit(self): + self.flags.payments = [] + for detail in self.get("csf_tz_bank_charges_detail"): + pe_details = self.create_pe(detail.debit_amount, detail.value_date, detail.reference_number) - detail.ref_docname = pe_details["reference_name"] - detail.ref_doctype = pe_details["reference_type"] - self.payments.append(pe_details) + detail.ref_docname = pe_details["reference_name"] + detail.ref_doctype = pe_details["reference_type"] + self.flags.payments.append(pe_details) - pi = frappe.new_doc("Purchase Invoice") - pi.posting_date = self.posting_date - pi.supplier = self.bank_supplier - pi.exchange_rate = self.exchange_rate - pi.currency = self.currency - pi.company = self.company - pi.append( - "items", - { - "item_code": "Bank Charges", - "qty": 1, - "rate": self.total_bank_charges, - "amount": self.total_bank_charges, - }, - ) + pi = frappe.new_doc("Purchase Invoice") + pi.posting_date = self.posting_date + pi.supplier = self.bank_supplier + pi.exchange_rate = self.exchange_rate + pi.currency = self.currency + pi.company = self.company + pi.append( + "items", + { + "item_code": "Bank Charges", + "qty": 1, + "rate": self.total_bank_charges, + "amount": self.total_bank_charges, + }, + ) - try: - pi.save(ignore_permissions=True) - pi.submit() - self.ref_pi = pi.name - frappe.msgprint(f"PI {pi.name} created successfully") + try: + pi.save(ignore_permissions=True) + pi.submit() + self.db_set("ref_pi", pi.name) + frappe.msgprint(f"PI {pi.name} created successfully") - self.invoices = [ - { - "invoice_type": pi.doctype, - "invoice_number": pi.name, - "invoice_date": pi.posting_date, - "amount": pi.grand_total, - "outstanding_amount": pi.outstanding_amount, - "currency": pi.currency, - "exchange_rate": self.exchange_rate, - } - ] - # self.payment_reconcile() - except Exception as e: - frappe.throw(_("Error while creating Purchase Invoice: {0}").format(str(e))) + self.flags.invoices = [ + { + "invoice_type": pi.doctype, + "invoice_number": pi.name, + "invoice_date": pi.posting_date, + "amount": pi.grand_total, + "outstanding_amount": pi.outstanding_amount, + "currency": pi.currency, + "exchange_rate": self.exchange_rate, + } + ] + # self.payment_reconcile() + except Exception as e: + frappe.throw(_("Error while creating Purchase Invoice: {0}").format(str(e))) - def create_pe(self, debit_amount, value_date, reference_number): - pe = frappe.new_doc("Payment Entry") - pe.naming_series = "PE-" - pe.posting_date = value_date - pe.payment_type = "Pay" - pe.party_type = "Supplier" - pe.party = self.bank_supplier - pe.company = self.company - pe.paid_amount = debit_amount - pe.received_amount = debit_amount - pe.source_exchange_rate = self.exchange_rate - pe.paid_from = self.account - pe.paid_from_account_currency = self.currency - pe.reference_date = value_date - pe.reference_no = reference_number + def create_pe(self, debit_amount, value_date, reference_number): + pe = frappe.new_doc("Payment Entry") + pe.naming_series = "PE-" + pe.posting_date = value_date + pe.payment_type = "Pay" + pe.party_type = "Supplier" + pe.party = self.bank_supplier + pe.company = self.company + pe.paid_amount = debit_amount + pe.received_amount = debit_amount + pe.source_exchange_rate = self.exchange_rate + pe.paid_from = self.account + pe.paid_from_account_currency = self.currency + pe.reference_date = value_date + pe.reference_no = reference_number - try: - pe.save(ignore_permissions=True) - pe.submit() - frappe.msgprint(f"PE {pe.name} created successfully") - return { - "reference_type": pe.doctype, - "reference_name": pe.name, - "posting_date": pe.posting_date, - "amount": pe.unallocated_amount, - "unallocated_amount": pe.unallocated_amount, - "difference_amount": 0, - "currency": pe.paid_from_account_currency, - "exchange_rate": self.exchange_rate, - } - except Exception as e: - frappe.throw(_("Error while creating Payment Entry: {0}").format(str(e))) + try: + pe.save(ignore_permissions=True) + pe.submit() + frappe.msgprint(f"PE {pe.name} created successfully") + return { + "reference_type": pe.doctype, + "reference_name": pe.name, + "posting_date": pe.posting_date, + "amount": pe.unallocated_amount, + "unallocated_amount": pe.unallocated_amount, + "difference_amount": 0, + "currency": pe.paid_from_account_currency, + "exchange_rate": self.exchange_rate, + } + except Exception as e: + frappe.throw(_("Error while creating Payment Entry: {0}").format(str(e))) - def payment_reconcile(self): - exit - try: - reconcile = frappe.new_doc("Payment Reconciliation") - reconcile.party_type = "Supplier" - reconcile.party = self.bank_supplier - reconcile.company = self.company - reconcile.receivable_payable_account = get_party_account( - "Supplier", self.bank_supplier, self.company - ) + def payment_reconcile(self): + try: + reconcile = frappe.new_doc("Payment Reconciliation") + reconcile.party_type = "Supplier" + reconcile.party = self.bank_supplier + reconcile.company = self.company + reconcile.receivable_payable_account = get_party_account( + "Supplier", self.bank_supplier, self.company + ) - reconcile.invoices = [] - for invoice in self.invoices: - invoice_entry = reconcile.append("invoices", {}) - invoice_entry.invoice_type = invoice["invoice_type"] - invoice_entry.invoice_number = invoice["invoice_number"] - invoice_entry.invoice_date = invoice["invoice_date"] - invoice_entry.amount = invoice["amount"] - invoice_entry.outstanding_amount = invoice["outstanding_amount"] - invoice_entry.currency = invoice["currency"] - invoice_entry.exchange_rate = invoice["exchange_rate"] + reconcile.invoices = [] + for invoice in self.flags.invoices: + invoice_entry = reconcile.append("invoices", {}) + invoice_entry.invoice_type = invoice["invoice_type"] + invoice_entry.invoice_number = invoice["invoice_number"] + invoice_entry.invoice_date = invoice["invoice_date"] + invoice_entry.amount = invoice["amount"] + invoice_entry.outstanding_amount = invoice["outstanding_amount"] + invoice_entry.currency = invoice["currency"] + invoice_entry.exchange_rate = invoice["exchange_rate"] - reconcile.payments = [] - for payment in self.payments: - payment_entry = reconcile.append("payments", {}) - payment_entry.reference_type = payment["reference_type"] - payment_entry.reference_name = payment["reference_name"] - payment_entry.posting_date = payment["posting_date"] - payment_entry.amount = payment["amount"] - payment_entry.unallocated_amount = payment["unallocated_amount"] - payment_entry.difference_amount = payment["difference_amount"] - payment_entry.currency = payment["currency"] - payment_entry.exchange_rate = payment["exchange_rate"] + reconcile.payments = [] + for payment in self.flags.payments: + payment_entry = reconcile.append("payments", {}) + payment_entry.reference_type = payment["reference_type"] + payment_entry.reference_name = payment["reference_name"] + payment_entry.posting_date = payment["posting_date"] + payment_entry.amount = payment["amount"] + payment_entry.unallocated_amount = payment["unallocated_amount"] + payment_entry.difference_amount = payment["difference_amount"] + payment_entry.currency = payment["currency"] + payment_entry.exchange_rate = payment["exchange_rate"] - reconcile.allocate_entries( - {"invoices": self.invoices, "payments": self.payments} - ) + reconcile.allocate_entries({"invoices": self.flags.invoices, "payments": self.flags.payments}) - reconcile.reconcile() - except Exception as e: - frappe.throw( - _("An error occurred during payment reconciliation: {0}").format(str(e)) - ) + reconcile.reconcile() + except Exception as e: + frappe.throw(_("An error occurred during payment reconciliation: {0}").format(str(e))) diff --git a/csf_tz/csf_tz/doctype/csf_tz_bank_charges_detail/csf_tz_bank_charges_detail.py b/csf_tz/csf_tz/doctype/csf_tz_bank_charges_detail/csf_tz_bank_charges_detail.py index e7b14968..10bea971 100644 --- a/csf_tz/csf_tz/doctype/csf_tz_bank_charges_detail/csf_tz_bank_charges_detail.py +++ b/csf_tz/csf_tz/doctype/csf_tz_bank_charges_detail/csf_tz_bank_charges_detail.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class CSFTZBankChargesDetail(Document): pass diff --git a/csf_tz/csf_tz/doctype/csf_tz_settings/csf_tz_settings.py b/csf_tz/csf_tz/doctype/csf_tz_settings/csf_tz_settings.py index 42f381bf..f7b03b5d 100644 --- a/csf_tz/csf_tz/doctype/csf_tz_settings/csf_tz_settings.py +++ b/csf_tz/csf_tz/doctype/csf_tz_settings/csf_tz_settings.py @@ -1,8 +1,6 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ diff --git a/csf_tz/csf_tz/doctype/csf_tz_settings/test_csf_tz_settings.py b/csf_tz/csf_tz/doctype/csf_tz_settings/test_csf_tz_settings.py index 42e29e1a..f91f90fb 100644 --- a/csf_tz/csf_tz/doctype/csf_tz_settings/test_csf_tz_settings.py +++ b/csf_tz/csf_tz/doctype/csf_tz_settings/test_csf_tz_settings.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and Contributors # See license.txt -from __future__ import unicode_literals # import frappe import unittest + class TestCSFTZSettings(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/efd_z_report/efd_z_report.js b/csf_tz/csf_tz/doctype/efd_z_report/efd_z_report.js index 3b4fd3dc..f6acd28e 100644 --- a/csf_tz/csf_tz/doctype/efd_z_report/efd_z_report.js +++ b/csf_tz/csf_tz/doctype/efd_z_report/efd_z_report.js @@ -33,7 +33,7 @@ frappe.ui.form.on('EFD Z Report Invoice', { frm.set_value("total_vat_ticked", sum_vat_ticked); frm.set_value("total_turnover_exempted__sp_relief_ticked", sum_turnover_exempted_sp_relief_ticked); frm.set_value("total_turnover_ticked", sum_turnover_ticked); - + } }) @@ -53,4 +53,4 @@ frappe.ui.form.on('EFD Z Report', { const calculate_total_turnover = (frm) => { frm.doc.total_turnover = frm.doc.net_amount + frm.doc.total_vat +frm.doc.total_turnover_ex_sr; refresh_field("total_turnover"); -} \ No newline at end of file +} diff --git a/csf_tz/csf_tz/doctype/efd_z_report/efd_z_report.py b/csf_tz/csf_tz/doctype/efd_z_report/efd_z_report.py index 54e5a0f1..e2e0d7c5 100644 --- a/csf_tz/csf_tz/doctype/efd_z_report/efd_z_report.py +++ b/csf_tz/csf_tz/doctype/efd_z_report/efd_z_report.py @@ -1,144 +1,145 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals +from datetime import datetime + import frappe from frappe import _ from frappe.model.document import Document -from datetime import datetime from frappe.utils import flt class EFDZReport(Document): - def validate(self): - if not self.efd_z_report_invoices: - frappe.throw(_("No Sales Invoie Found in the table")) - - def get_number_of_ticked(self): - total_checked = 0 - for i in self.efd_z_report_invoices: - if i.include: - total_checked += 1 - return total_checked + def validate(self): + if not self.efd_z_report_invoices: + frappe.throw(_("No Sales Invoie Found in the table")) - @frappe.whitelist() - def get_sales_invoice(self): + def get_number_of_ticked(self): + total_checked = 0 + for i in self.efd_z_report_invoices: + if i.include: + total_checked += 1 + return total_checked - date = datetime.strptime( - str(self.z_report_date_time), "%Y-%m-%d %H:%M:%S" - ).date() - time = datetime.strptime( - str(self.z_report_date_time), "%Y-%m-%d %H:%M:%S" - ).time() + @frappe.whitelist() + def get_sales_invoice(self): + date = datetime.strptime(str(self.z_report_date_time), "%Y-%m-%d %H:%M:%S").date() + time = datetime.strptime(str(self.z_report_date_time), "%Y-%m-%d %H:%M:%S").time() - condition = ( - "docstatus = 1 and (efd_z_report = '' OR efd_z_report is null) and status !='Return' and posting_date <= '" - + str(date) - + "' and IF(IF(posting_date = '" - + str(date) - + "', IF(posting_time < '" - + str(time) - + "',1,'PostingTime'),'PostingDate') = 1 or IF(posting_date = '" - + str(date) - + "',IF(posting_time < '" - + str(time) - + "',1,'PostingTime'),'PostingDate') = 'PostingDate',1,0)" - ) + condition = ( + "docstatus = 1 and (efd_z_report = '' OR efd_z_report is null) and status !='Return' and posting_date <= '" + + str(date) + + "' and IF(IF(posting_date = '" + + str(date) + + "', IF(posting_time < '" + + str(time) + + "',1,'PostingTime'),'PostingDate') = 1 or IF(posting_date = '" + + str(date) + + "',IF(posting_time < '" + + str(time) + + "',1,'PostingTime'),'PostingDate') = 'PostingDate',1,0)" + ) - condition += ( - " and ( electronic_fiscal_device = '" + self.electronic_fiscal_device + "'" - "or electronic_fiscal_device is null or electronic_fiscal_device = '')" - ) + condition += ( + " and ( electronic_fiscal_device = '" + self.electronic_fiscal_device + "'" + "or electronic_fiscal_device is null or electronic_fiscal_device = '')" + ) - query = """ select * + query = f""" select * from `tabSales Invoice` - where {0}""".format( - condition - ) + where {condition}""" - sales_invoices = frappe.db.sql(query, as_dict=True) + sales_invoices = frappe.db.sql(query, as_dict=True) - if not sales_invoices: - frappe.throw("No Sales Invoice Fetch") + if not sales_invoices: + frappe.throw("No Sales Invoice Fetch") - for i in sales_invoices: - if i.base_total_taxes_and_charges == 0: - amt_ex__sr = i.base_total - else: - if i.base_net_total != i.base_total: - amt_ex__sr = i.base_grand_total - ( - i.base_net_total + i.base_total_taxes_and_charges - ) - else: - amt_ex__sr = i.base_grand_total - ( - (i.base_total_taxes_and_charges / 0.18) - + i.base_total_taxes_and_charges - ) - if amt_ex__sr < 0: - amt_ex__sr = 0 - self.append( - "efd_z_report_invoices", - { - "invoice_number": i.name, - "invoice_date": i.posting_date, - "amt_excl_vat": flt(i.base_net_total, 2), - "vat": flt(i.base_total_taxes_and_charges, 2), - "amt_ex__sr": amt_ex__sr, - "invoice_amount": flt(i.base_rounded_total, 2), - "invoice_currency": i.currency, - }, - ) - return True + for i in sales_invoices: + if i.base_total_taxes_and_charges == 0: + amt_ex__sr = i.base_total + else: + if i.base_net_total != i.base_total: + amt_ex__sr = i.base_grand_total - (i.base_net_total + i.base_total_taxes_and_charges) + else: + amt_ex__sr = i.base_grand_total - ( + (i.base_total_taxes_and_charges / 0.18) + i.base_total_taxes_and_charges + ) + if amt_ex__sr < 0: + amt_ex__sr = 0 + self.append( + "efd_z_report_invoices", + { + "invoice_number": i.name, + "invoice_date": i.posting_date, + "amt_excl_vat": flt(i.base_net_total, 2), + "vat": flt(i.base_total_taxes_and_charges, 2), + "amt_ex__sr": amt_ex__sr, + "invoice_amount": flt(i.base_rounded_total, 2), + "invoice_currency": i.currency, + }, + ) + return True - def before_submit(self): - if abs(flt(self.total_turnover, 2) - flt(self.total_turnover_ticked, 2)) > (self.allowable_difference or 0): - frappe.throw(_("Sales Invoice Amount {0} is not equal to Money Entered {1}".format(flt(self.total_turnover_ticked, 2), flt(self.total_turnover, 2)))) - if abs(flt(self.net_amount, 2) - flt(self.total_excluding_vat_ticked, 2)) > (self.allowable_difference or 0): - frappe.throw( - _("Total Excluding VAT {0} is not equal to Total Excluding VAT (Ticked) {1}".format(flt(self.net_amount, 2), flt(self.total_excluding_vat_ticked, 2))) - ) - if abs(flt(self.total_vat, 2) - flt(self.total_vat_ticked, 2)) > (self.allowable_difference or 0): - frappe.throw(_("Total VAT {0} is not equal to Total VAT (Ticked) {1}".format(flt(self.total_vat, 2), flt(self.total_vat_ticked, 2)))) - if abs(flt(self.total_turnover_ex_sr, 2) - flt(self.total_turnover_exempted__sp_relief_ticked, 2)) > (self.allowable_difference or 0): - frappe.throw( - _( - "Total Turnover Exempted / Sp. Relief {0} is not equal to Total Turnover Exempted / Sp. Relief (Ticked) {1}".format( - flt(self.total_turnover_ex_sr, 2), flt(self.total_turnover_exempted__sp_relief_ticked, 2) - ) - ) - ) - if self.get_number_of_ticked() != self.receipts_issued: - frappe.throw( - _( - "The Number of Sales Invoice (Include is checked) in the table is not equal to Receipts Issued" - ) - ) - to_remove = [] - for invoice in self.efd_z_report_invoices: - if not invoice.include: - to_remove.append(invoice) - else: - invoice_doc = frappe.get_doc("Sales Invoice", invoice.invoice_number) - if invoice_doc.efd_z_report: - frappe.throw( - _( - "The Sales Invoice {0} is linked to EFD Z Report {1}".format( - invoice.invoice_number, invoice_doc.efd_z_report - ) - ) - ) - else: - invoice_doc.efd_z_report = self.name - invoice_doc.flags.ignore_permissions = True - invoice_doc.save() - [self.remove(invoice) for invoice in to_remove] + def before_submit(self): + if abs(flt(self.total_turnover, 2) - flt(self.total_turnover_ticked, 2)) > ( + self.allowable_difference or 0 + ): + frappe.throw( + _( + f"Sales Invoice Amount {flt(self.total_turnover_ticked, 2)} is not equal to Money Entered {flt(self.total_turnover, 2)}" + ) + ) + if abs(flt(self.net_amount, 2) - flt(self.total_excluding_vat_ticked, 2)) > ( + self.allowable_difference or 0 + ): + frappe.throw( + _( + f"Total Excluding VAT {flt(self.net_amount, 2)} is not equal to Total Excluding VAT (Ticked) {flt(self.total_excluding_vat_ticked, 2)}" + ) + ) + if abs(flt(self.total_vat, 2) - flt(self.total_vat_ticked, 2)) > (self.allowable_difference or 0): + frappe.throw( + _( + f"Total VAT {flt(self.total_vat, 2)} is not equal to Total VAT (Ticked) {flt(self.total_vat_ticked, 2)}" + ) + ) + if abs(flt(self.total_turnover_ex_sr, 2) - flt(self.total_turnover_exempted__sp_relief_ticked, 2)) > ( + self.allowable_difference or 0 + ): + frappe.throw( + _( + f"Total Turnover Exempted / Sp. Relief {flt(self.total_turnover_ex_sr, 2)} is not equal to Total Turnover Exempted / Sp. Relief (Ticked) {flt(self.total_turnover_exempted__sp_relief_ticked, 2)}" + ) + ) + if self.get_number_of_ticked() != self.receipts_issued: + frappe.throw( + _( + "The Number of Sales Invoice (Include is checked) in the table is not equal to Receipts Issued" + ) + ) + to_remove = [] + for invoice in self.efd_z_report_invoices: + if not invoice.include: + to_remove.append(invoice) + else: + invoice_doc = frappe.get_doc("Sales Invoice", invoice.invoice_number) + if invoice_doc.efd_z_report: + frappe.throw( + _( + f"The Sales Invoice {invoice.invoice_number} is linked to EFD Z Report {invoice_doc.efd_z_report}" + ) + ) + else: + invoice_doc.efd_z_report = self.name + invoice_doc.flags.ignore_permissions = True + invoice_doc.save() + [self.remove(invoice) for invoice in to_remove] - def on_cancel(self): - for invoice in self.efd_z_report_invoices: - if invoice.include: - invoice_doc = frappe.get_doc("Sales Invoice", invoice.invoice_number) - if invoice_doc.efd_z_report: - invoice_doc.efd_z_report = "" - invoice_doc.flags.ignore_permissions = True - invoice_doc.save() + def on_cancel(self): + for invoice in self.efd_z_report_invoices: + if invoice.include: + invoice_doc = frappe.get_doc("Sales Invoice", invoice.invoice_number) + if invoice_doc.efd_z_report: + invoice_doc.efd_z_report = "" + invoice_doc.flags.ignore_permissions = True + invoice_doc.save() diff --git a/csf_tz/csf_tz/doctype/efd_z_report/test_efd_z_report.py b/csf_tz/csf_tz/doctype/efd_z_report/test_efd_z_report.py index 292df86c..b35f315e 100644 --- a/csf_tz/csf_tz/doctype/efd_z_report/test_efd_z_report.py +++ b/csf_tz/csf_tz/doctype/efd_z_report/test_efd_z_report.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Aakvatech and Contributors # See license.txt -from __future__ import unicode_literals import unittest + class TestEFDZReport(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/efd_z_report_invoice/efd_z_report_invoice.py b/csf_tz/csf_tz/doctype/efd_z_report_invoice/efd_z_report_invoice.py index 2ff4e2ad..93d88241 100644 --- a/csf_tz/csf_tz/doctype/efd_z_report_invoice/efd_z_report_invoice.py +++ b/csf_tz/csf_tz/doctype/efd_z_report_invoice/efd_z_report_invoice.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals from frappe.model.document import Document + class EFDZReportInvoice(Document): pass diff --git a/csf_tz/csf_tz/doctype/efd_z_report_invoice/test_efd_z_report_invoice.py b/csf_tz/csf_tz/doctype/efd_z_report_invoice/test_efd_z_report_invoice.py index 6cce1a99..5d6c0218 100644 --- a/csf_tz/csf_tz/doctype/efd_z_report_invoice/test_efd_z_report_invoice.py +++ b/csf_tz/csf_tz/doctype/efd_z_report_invoice/test_efd_z_report_invoice.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Aakvatech and Contributors # See license.txt -from __future__ import unicode_literals import unittest + class TestEFDZReportInvoice(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/electronic_fiscal_device/electronic_fiscal_device.py b/csf_tz/csf_tz/doctype/electronic_fiscal_device/electronic_fiscal_device.py index bee62720..d7afcaf9 100644 --- a/csf_tz/csf_tz/doctype/electronic_fiscal_device/electronic_fiscal_device.py +++ b/csf_tz/csf_tz/doctype/electronic_fiscal_device/electronic_fiscal_device.py @@ -1,9 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals from frappe.model.document import Document + class ElectronicFiscalDevice(Document): pass diff --git a/csf_tz/csf_tz/doctype/electronic_fiscal_device/test_electronic_fiscal_device.py b/csf_tz/csf_tz/doctype/electronic_fiscal_device/test_electronic_fiscal_device.py index 706ba000..7c266a5e 100644 --- a/csf_tz/csf_tz/doctype/electronic_fiscal_device/test_electronic_fiscal_device.py +++ b/csf_tz/csf_tz/doctype/electronic_fiscal_device/test_electronic_fiscal_device.py @@ -1,8 +1,7 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Aakvatech and Contributors # See license.txt -from __future__ import unicode_literals import unittest + class TestElectronicFiscalDevice(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/foreign_import_transaction/foreign_import_transaction.js b/csf_tz/csf_tz/doctype/foreign_import_transaction/foreign_import_transaction.js index c77f3261..91de26a0 100644 --- a/csf_tz/csf_tz/doctype/foreign_import_transaction/foreign_import_transaction.js +++ b/csf_tz/csf_tz/doctype/foreign_import_transaction/foreign_import_transaction.js @@ -17,7 +17,7 @@ frappe.ui.form.on('Foreign Import Transaction', { }); }); } - + if (frm.doc.docstatus === 1) { frm.add_custom_button(__('View Exchange Report'), function() { frappe.route_options = { @@ -111,7 +111,7 @@ frappe.ui.form.on('Foreign Import Transaction', { } }, __('Debug')); } - + // Set color indicator based on status if (frm.doc.status && frm.dashboard) { let color = { @@ -136,7 +136,7 @@ frappe.ui.form.on('Foreign Import Transaction', { frm.dashboard.add_indicator(message, frm.doc.total_gain_loss >= 0 ? 'green' : 'red'); } }, - + purchase_invoice: function(frm) { if (frm.doc.purchase_invoice) { frappe.call({ @@ -148,7 +148,7 @@ frappe.ui.form.on('Foreign Import Transaction', { callback: function(r) { if (r.message) { let pi = r.message; - + // Check if it's a foreign currency invoice frappe.db.get_value('Company', pi.company, 'default_currency') .then(result => { @@ -157,7 +157,7 @@ frappe.ui.form.on('Foreign Import Transaction', { frm.set_value('purchase_invoice', ''); return; } - + // Set fields from PI frm.set_value({ 'supplier': pi.supplier, @@ -195,12 +195,12 @@ frappe.ui.form.on('Foreign Import Payment Details', { 'payment_amount_base': pe.base_paid_amount, 'payment_exchange_rate': pe.source_exchange_rate }); - + // Calculate exchange difference let original_rate = flt(frm.doc.original_exchange_rate); let payment_rate = flt(pe.source_exchange_rate); let paid_amount = flt(pe.paid_amount); - + if (original_rate !== payment_rate) { let exchange_diff = paid_amount * (payment_rate - original_rate); frappe.model.set_value(cdt, cdn, 'exchange_difference', exchange_diff); diff --git a/csf_tz/csf_tz/doctype/foreign_import_transaction/foreign_import_transaction.py b/csf_tz/csf_tz/doctype/foreign_import_transaction/foreign_import_transaction.py index 280ef758..599fc1f5 100644 --- a/csf_tz/csf_tz/doctype/foreign_import_transaction/foreign_import_transaction.py +++ b/csf_tz/csf_tz/doctype/foreign_import_transaction/foreign_import_transaction.py @@ -3,186 +3,181 @@ import frappe from frappe.model.document import Document -from frappe.utils import flt, getdate, nowdate -from frappe import _ +from frappe.utils import flt class ForeignImportTransaction(Document): - def validate(self): - self.validate_currency() - self.calculate_totals() - self.set_status() - - def on_submit(self): - self.update_status("Active") - - def on_cancel(self): - self.cancel_related_journal_entries() - self.update_status("Cancelled") - - def validate_currency(self): - """Validate that the purchase invoice is in foreign currency""" - if not self.currency: - frappe.throw("Currency is required") - - company_currency = frappe.get_cached_value( - "Company", self.company, "default_currency" - ) - if self.currency == company_currency: - frappe.throw( - f"Foreign Import Transaction can only be created for foreign currency invoices. Company currency is {company_currency}" - ) - - def calculate_totals(self): - """Calculate total gains and losses""" - total_gain = 0 - total_loss = 0 - - for diff in self.exchange_differences: - if diff.difference_type == "Gain": - total_gain += flt(diff.amount) - else: - total_loss += flt(diff.amount) - - self.total_gain_loss = total_gain - total_loss - self.net_difference = self.total_gain_loss - - def set_status(self): - """Set status based on completion""" - if self.docstatus == 0: - self.status = "Draft" - elif self.docstatus == 1: - # Check if all payments are made - invoice_amount = flt(self.invoice_amount_foreign) - total_paid = sum([flt(p.payment_amount_foreign) for p in self.payments]) - - if total_paid >= invoice_amount: - self.status = "Completed" - else: - self.status = "Active" - else: - self.status = "Cancelled" - - def update_status(self, status): - """Update status without triggering validations""" - frappe.db.set_value(self.doctype, self.name, "status", status) - - def cancel_related_journal_entries(self): - """Cancel all related journal entries""" - for diff in self.exchange_differences: - if diff.journal_entry: - try: - je = frappe.get_doc("Journal Entry", diff.journal_entry) - if je.docstatus == 1: - je.cancel() - except Exception as e: - frappe.log_error( - f"Error cancelling Journal Entry {diff.journal_entry}: {str(e)}" - ) - - def add_exchange_difference( - self, - reference_type, - reference_name, - difference_type, - amount, - posting_date, - remarks, - journal_entry=None, - ): - """Add exchange difference entry""" - diff_row = self.append("exchange_differences", {}) - diff_row.reference_type = reference_type - diff_row.reference_name = reference_name - diff_row.difference_type = difference_type - diff_row.amount = round(amount, 3) - diff_row.posting_date = posting_date - diff_row.remarks = remarks - diff_row.journal_entry = journal_entry - - self.calculate_totals() - self.save() - - return diff_row - - def add_payment_detail(self, payment_entry): - """Add payment detail from Payment Entry""" - payment_doc = frappe.get_doc("Payment Entry", payment_entry) - - payment_row = self.append("payments", {}) - payment_row.payment_entry = payment_entry - payment_row.payment_date = payment_doc.posting_date - payment_row.payment_amount_foreign = payment_doc.paid_amount - payment_row.payment_amount_base = payment_doc.base_paid_amount - payment_row.payment_exchange_rate = payment_doc.source_exchange_rate - - # Calculate exchange difference - original_rate = flt(self.original_exchange_rate) - payment_rate = flt(payment_doc.source_exchange_rate) - paid_amount = flt(payment_doc.paid_amount) - - if original_rate != payment_rate: - exchange_diff = paid_amount * (payment_rate - original_rate) - payment_row.exchange_difference = exchange_diff - - self.save() - return payment_row - - def add_lcv_detail(self, lcv_name): - """Add LCV detail from Landed Cost Voucher""" - lcv_doc = frappe.get_doc("Landed Cost Voucher", lcv_name) - - lcv_row = self.append("landed_cost_vouchers", {}) - lcv_row.landed_cost_voucher = lcv_name - lcv_row.lcv_date = lcv_doc.posting_date - lcv_row.lcv_amount_base = lcv_doc.total_taxes_and_charges - lcv_row.exchange_rate_used = flt(lcv_doc.get("conversion_rate", 1)) - - # Get allocated amount from LCV items - allocated_amount = 0 - for item in lcv_doc.items: - allocated_amount += flt(item.applicable_charges) - lcv_row.allocated_to_items = allocated_amount - - self.save() - return lcv_row - - @frappe.whitelist() - def recalculate_differences(self): - """Manually recalculate all exchange differences""" - # Import here to avoid circular imports - from csf_tz.csftz_hooks.exchange_calculations import ( - recalculate_import_differences, - ) - - return recalculate_import_differences(self.name) - - @frappe.whitelist() - def get_exchange_summary(self): - """Get summary of exchange differences""" - summary = { - "total_gain": 0, - "total_loss": 0, - "payment_differences": 0, - "lcv_differences": 0, - "manual_entries": 0, - } - - for diff in self.exchange_differences: - amount = flt(diff.amount) - if diff.difference_type == "Gain": - summary["total_gain"] += amount - else: - summary["total_loss"] += amount - - # Categorize by reference type - if diff.reference_type == "Payment Entry": - summary["payment_differences"] += amount - elif diff.reference_type == "Landed Cost Voucher": - summary["lcv_differences"] += amount - else: - summary["manual_entries"] += amount - - summary["net_difference"] = summary["total_gain"] - summary["total_loss"] - - return summary + def validate(self): + self.validate_currency() + self.calculate_totals() + self.set_status() + + def on_submit(self): + self.update_status("Active") + + def on_cancel(self): + self.cancel_related_journal_entries() + self.update_status("Cancelled") + + def validate_currency(self): + """Validate that the purchase invoice is in foreign currency""" + if not self.currency: + frappe.throw("Currency is required") + + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + if self.currency == company_currency: + frappe.throw( + f"Foreign Import Transaction can only be created for foreign currency invoices. Company currency is {company_currency}" + ) + + def calculate_totals(self): + """Calculate total gains and losses""" + total_gain = 0 + total_loss = 0 + + for diff in self.exchange_differences: + if diff.difference_type == "Gain": + total_gain += flt(diff.amount) + else: + total_loss += flt(diff.amount) + + self.total_gain_loss = total_gain - total_loss + self.net_difference = self.total_gain_loss + + def set_status(self): + """Set status based on completion""" + if self.docstatus == 0: + self.status = "Draft" + elif self.docstatus == 1: + # Check if all payments are made + invoice_amount = flt(self.invoice_amount_foreign) + total_paid = sum([flt(p.payment_amount_foreign) for p in self.payments]) + + if total_paid >= invoice_amount: + self.status = "Completed" + else: + self.status = "Active" + else: + self.status = "Cancelled" + + def update_status(self, status): + """Update status without triggering validations""" + frappe.db.set_value(self.doctype, self.name, "status", status) + + def cancel_related_journal_entries(self): + """Cancel all related journal entries""" + for diff in self.exchange_differences: + if diff.journal_entry: + try: + je = frappe.get_doc("Journal Entry", diff.journal_entry) + if je.docstatus == 1: + je.cancel() + except Exception as e: + frappe.log_error(f"Error cancelling Journal Entry {diff.journal_entry}: {str(e)}") + + def add_exchange_difference( + self, + reference_type, + reference_name, + difference_type, + amount, + posting_date, + remarks, + journal_entry=None, + ): + """Add exchange difference entry""" + diff_row = self.append("exchange_differences", {}) + diff_row.reference_type = reference_type + diff_row.reference_name = reference_name + diff_row.difference_type = difference_type + diff_row.amount = round(amount, 3) + diff_row.posting_date = posting_date + diff_row.remarks = remarks + diff_row.journal_entry = journal_entry + + self.calculate_totals() + self.save() + + return diff_row + + def add_payment_detail(self, payment_entry): + """Add payment detail from Payment Entry""" + payment_doc = frappe.get_doc("Payment Entry", payment_entry) + + payment_row = self.append("payments", {}) + payment_row.payment_entry = payment_entry + payment_row.payment_date = payment_doc.posting_date + payment_row.payment_amount_foreign = payment_doc.paid_amount + payment_row.payment_amount_base = payment_doc.base_paid_amount + payment_row.payment_exchange_rate = payment_doc.source_exchange_rate + + # Calculate exchange difference + original_rate = flt(self.original_exchange_rate) + payment_rate = flt(payment_doc.source_exchange_rate) + paid_amount = flt(payment_doc.paid_amount) + + if original_rate != payment_rate: + exchange_diff = paid_amount * (payment_rate - original_rate) + payment_row.exchange_difference = exchange_diff + + self.save() + return payment_row + + def add_lcv_detail(self, lcv_name): + """Add LCV detail from Landed Cost Voucher""" + lcv_doc = frappe.get_doc("Landed Cost Voucher", lcv_name) + + lcv_row = self.append("landed_cost_vouchers", {}) + lcv_row.landed_cost_voucher = lcv_name + lcv_row.lcv_date = lcv_doc.posting_date + lcv_row.lcv_amount_base = lcv_doc.total_taxes_and_charges + lcv_row.exchange_rate_used = flt(lcv_doc.get("conversion_rate", 1)) + + # Get allocated amount from LCV items + allocated_amount = 0 + for item in lcv_doc.items: + allocated_amount += flt(item.applicable_charges) + lcv_row.allocated_to_items = allocated_amount + + self.save() + return lcv_row + + @frappe.whitelist() + def recalculate_differences(self): + """Manually recalculate all exchange differences""" + # Import here to avoid circular imports + from csf_tz.csftz_hooks.exchange_calculations import ( + recalculate_import_differences, + ) + + return recalculate_import_differences(self.name) + + @frappe.whitelist() + def get_exchange_summary(self): + """Get summary of exchange differences""" + summary = { + "total_gain": 0, + "total_loss": 0, + "payment_differences": 0, + "lcv_differences": 0, + "manual_entries": 0, + } + + for diff in self.exchange_differences: + amount = flt(diff.amount) + if diff.difference_type == "Gain": + summary["total_gain"] += amount + else: + summary["total_loss"] += amount + + # Categorize by reference type + if diff.reference_type == "Payment Entry": + summary["payment_differences"] += amount + elif diff.reference_type == "Landed Cost Voucher": + summary["lcv_differences"] += amount + else: + summary["manual_entries"] += amount + + summary["net_difference"] = summary["total_gain"] - summary["total_loss"] + + return summary diff --git a/csf_tz/csf_tz/doctype/foreign_import_transaction/test_foreign_import_transaction.py b/csf_tz/csf_tz/doctype/foreign_import_transaction/test_foreign_import_transaction.py index d8074568..390350df 100644 --- a/csf_tz/csf_tz/doctype/foreign_import_transaction/test_foreign_import_transaction.py +++ b/csf_tz/csf_tz/doctype/foreign_import_transaction/test_foreign_import_transaction.py @@ -2,434 +2,456 @@ # See license.txt import frappe +from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate -from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice -from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry class TestForeignImportTransaction(FrappeTestCase): - def setUp(self): - """Set up test data""" - self.company = "_Test Company" - self.supplier = "_Test Supplier USD" - self.currency = "USD" - self.original_rate = 2500.0 # 1 USD = 2500 TZS - self.payment_rate = 2600.0 # 1 USD = 2600 TZS (currency strengthened) - - # Create test supplier group if not exists - if not frappe.db.exists("Supplier Group", "_Test Supplier Group"): - supplier_group = frappe.get_doc({ - "doctype": "Supplier Group", - "supplier_group_name": "_Test Supplier Group" - }) - supplier_group.insert(ignore_permissions=True) - - # Create test supplier if not exists - if not frappe.db.exists("Supplier", self.supplier): - supplier_doc = frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": self.supplier, - "supplier_group": "_Test Supplier Group", - "supplier_type": "Company" - }) - supplier_doc.insert(ignore_permissions=True) - - # Create Foreign Import Settings if not exists - if not frappe.db.exists("Foreign Import Settings"): - settings = frappe.get_doc({ - "doctype": "Foreign Import Settings", - "company": self.company, - "exchange_difference_threshold": 0.01, - "auto_create_journal_entries": 0, # Disable to avoid account setup issues - "enable_lcv_exchange_tracking": 1 - }) - settings.insert(ignore_permissions=True) - - def tearDown(self): - """Clean up test data""" - # Cancel and delete test documents - frappe.db.rollback() - - def test_automatic_tracker_creation_on_foreign_pi_submit(self): - """Test that Foreign Import Transaction is created automatically when foreign PI is submitted""" - # Create foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - do_not_submit=True - ) - - # Check no tracker exists before submission - tracker_before = frappe.db.exists("Foreign Import Transaction", {"purchase_invoice": pi.name}) - self.assertIsNone(tracker_before) - - # Submit the Purchase Invoice - pi.submit() - - # Check tracker is created after submission - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - self.assertIsNotNone(tracker_name) - - # Verify tracker details - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - self.assertEqual(tracker.purchase_invoice, pi.name) - self.assertEqual(tracker.supplier, pi.supplier) - self.assertEqual(tracker.currency, pi.currency) - self.assertEqual(tracker.original_exchange_rate, pi.conversion_rate) - self.assertEqual(tracker.invoice_amount_foreign, pi.grand_total) - self.assertEqual(tracker.invoice_amount_base, pi.base_grand_total) - self.assertEqual(tracker.status, "Active") - self.assertEqual(tracker.docstatus, 1) - - def test_no_tracker_creation_for_base_currency_pi(self): - """Test that no tracker is created for base currency Purchase Invoice""" - # Get company's default currency - company_currency = frappe.get_cached_value("Company", self.company, "default_currency") - - # Create base currency Purchase Invoice - pi = make_purchase_invoice( - supplier="_Test Supplier", - currency=company_currency, # Use actual company currency - rate=100, - do_not_submit=True - ) - - pi.submit() - - # Check no tracker is created - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - self.assertIsNone(tracker_name) - - def test_payment_entry_linking_and_exchange_calculation(self): - """Test that Payment Entry links to tracker and calculates exchange differences""" - # Create foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - qty=10, # Total: 1000 USD - do_not_submit=True - ) - pi.submit() - - # Get the created tracker - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Create Payment Entry with different exchange rate - pe = get_payment_entry("Purchase Invoice", pi.name) - pe.source_exchange_rate = self.payment_rate # Different rate - pe.paid_amount = 500 # Pay half the invoice - pe.base_paid_amount = 500 * self.payment_rate - pe.insert() - pe.submit() - - # Reload tracker to check if payment was linked - tracker.reload() - - # Verify payment was linked - self.assertEqual(len(tracker.payments), 1) - payment_row = tracker.payments[0] - self.assertEqual(payment_row.payment_entry, pe.name) - self.assertEqual(payment_row.payment_amount_foreign, 500) - self.assertEqual(payment_row.payment_exchange_rate, self.payment_rate) - - # Verify exchange difference calculation - expected_diff = 500 * (self.payment_rate - self.original_rate) # 500 * (2600 - 2500) = 50,000 - self.assertEqual(payment_row.exchange_difference, expected_diff) - - # Verify exchange difference entry was created - self.assertEqual(len(tracker.exchange_differences), 1) - diff_row = tracker.exchange_differences[0] - self.assertEqual(diff_row.reference_type, "Payment Entry") - self.assertEqual(diff_row.reference_name, pe.name) - self.assertEqual(diff_row.difference_type, "Gain") # Rate increased - self.assertEqual(diff_row.amount, expected_diff) - - # Verify status is still Active (partial payment) - self.assertEqual(tracker.status, "Active") - - def test_status_change_to_completed_on_full_payment(self): - """Test that status changes to Completed when full payment is made""" - # Create foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - qty=10, # Total: 1000 USD - do_not_submit=True - ) - pi.submit() - - # Get the created tracker - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Create Payment Entry for full amount - pe = get_payment_entry("Purchase Invoice", pi.name) - pe.source_exchange_rate = self.payment_rate - pe.paid_amount = 1000 # Full payment - pe.base_paid_amount = 1000 * self.payment_rate - pe.insert() - pe.submit() - - # Reload tracker - tracker.reload() - - # Verify status changed to Completed - self.assertEqual(tracker.status, "Completed") - - def test_exchange_loss_calculation(self): - """Test exchange loss calculation when currency weakens""" - # Create foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - qty=10, - do_not_submit=True - ) - pi.submit() - - # Get the created tracker - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Create Payment Entry with lower exchange rate (currency weakened) - weaker_rate = 2400.0 # 1 USD = 2400 TZS (currency weakened) - pe = get_payment_entry("Purchase Invoice", pi.name) - pe.source_exchange_rate = weaker_rate - pe.paid_amount = 500 - pe.base_paid_amount = 500 * weaker_rate - pe.insert() - pe.submit() - - # Reload tracker - tracker.reload() - - # Verify exchange loss calculation - expected_diff = 500 * (weaker_rate - self.original_rate) # 500 * (2400 - 2500) = -50,000 - payment_row = tracker.payments[0] - self.assertEqual(payment_row.exchange_difference, expected_diff) - - # Verify exchange difference entry shows Loss - diff_row = tracker.exchange_differences[0] - self.assertEqual(diff_row.difference_type, "Loss") - self.assertEqual(diff_row.amount, abs(expected_diff)) # Amount is always positive - - def test_tracker_cancellation_on_pi_cancel(self): - """Test that tracker is cancelled when Purchase Invoice is cancelled""" - # Create and submit foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - do_not_submit=True - ) - pi.submit() - - # Get the created tracker - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - self.assertEqual(tracker.docstatus, 1) - - # Cancel the Purchase Invoice - pi.cancel() - - # Reload tracker and verify it's cancelled - tracker.reload() - self.assertEqual(tracker.docstatus, 2) - self.assertEqual(tracker.status, "Cancelled") - - def test_currency_validation(self): - """Test that tracker validates foreign currency requirement""" - # Get company's default currency - company_currency = frappe.get_cached_value("Company", self.company, "default_currency") - - # Create tracker manually with same currency as company - tracker = frappe.get_doc({ - "doctype": "Foreign Import Transaction", - "purchase_invoice": "TEST-PI-001", - "supplier": self.supplier, - "currency": company_currency, # Same as company currency - "company": self.company - }) - - # Should throw error on validation - with self.assertRaises(frappe.ValidationError): - tracker.insert() - - def test_totals_calculation(self): - """Test that totals are calculated correctly""" - # Create foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - qty=10, - do_not_submit=True - ) - pi.submit() - - # Get the created tracker - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Add manual exchange differences - tracker.add_exchange_difference( - "Manual Entry", "TEST-001", "Gain", 25000, nowdate(), "Test gain" - ) - tracker.add_exchange_difference( - "Manual Entry", "TEST-002", "Loss", 15000, nowdate(), "Test loss" - ) - - # Verify totals - self.assertEqual(tracker.total_gain_loss, 10000) # 25000 - 15000 - self.assertEqual(tracker.net_difference, 10000) - - def test_exchange_summary_method(self): - """Test the get_exchange_summary method""" - # Create foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - qty=10, - do_not_submit=True - ) - pi.submit() - - # Get the created tracker - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Create Payment Entry - pe = get_payment_entry("Purchase Invoice", pi.name) - pe.source_exchange_rate = self.payment_rate - pe.paid_amount = 500 - pe.base_paid_amount = 500 * self.payment_rate - pe.insert() - pe.submit() - - # Reload tracker - tracker.reload() - - # Get exchange summary - summary = tracker.get_exchange_summary() - - # Verify summary - expected_gain = 500 * (self.payment_rate - self.original_rate) - self.assertEqual(summary["total_gain"], expected_gain) - self.assertEqual(summary["total_loss"], 0) - self.assertEqual(summary["payment_differences"], expected_gain) - self.assertEqual(summary["lcv_differences"], 0) - self.assertEqual(summary["manual_entries"], 0) - self.assertEqual(summary["net_difference"], expected_gain) - - def test_no_duplicate_tracker_creation(self): - """Test that duplicate trackers are not created for same PI""" - # Create foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - do_not_submit=True - ) - pi.submit() - - # Get initial tracker count - initial_count = frappe.db.count("Foreign Import Transaction", {"purchase_invoice": pi.name}) - self.assertEqual(initial_count, 1) - - # Try to trigger tracker creation again (simulate hook being called again) - from csf_tz.csftz_hooks.exchange_calculations import create_import_tracker - create_import_tracker(pi, "on_submit") - - # Verify no duplicate tracker was created - final_count = frappe.db.count("Foreign Import Transaction", {"purchase_invoice": pi.name}) - self.assertEqual(final_count, 1) - - def test_payment_currency_mismatch_no_linking(self): - """Test that payment with different currency doesn't link to tracker""" - # Create foreign currency Purchase Invoice in USD - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, # USD - conversion_rate=self.original_rate, - rate=100, - do_not_submit=True - ) - pi.submit() - - # Get the created tracker - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Create Payment Entry in different currency (EUR) - pe = get_payment_entry("Purchase Invoice", pi.name) - pe.paid_to_account_currency = "EUR" # Different currency - pe.source_exchange_rate = 2800.0 # EUR rate - pe.paid_amount = 500 - pe.base_paid_amount = 500 * 2800 - pe.insert() - pe.submit() - - # Reload tracker and verify no payment was linked - tracker.reload() - self.assertEqual(len(tracker.payments), 0) - self.assertEqual(len(tracker.exchange_differences), 0) - - def test_recalculate_differences_method(self): - """Test the recalculate_differences method""" - # Create foreign currency Purchase Invoice - pi = make_purchase_invoice( - supplier=self.supplier, - currency=self.currency, - conversion_rate=self.original_rate, - rate=100, - qty=10, - do_not_submit=True - ) - pi.submit() - - # Get the created tracker - tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": pi.name}, "name") - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Create Payment Entry - pe = get_payment_entry("Purchase Invoice", pi.name) - pe.source_exchange_rate = self.payment_rate - pe.paid_amount = 500 - pe.base_paid_amount = 500 * self.payment_rate - pe.insert() - pe.submit() - - # Reload tracker - tracker.reload() - initial_differences_count = len(tracker.exchange_differences) - - # Clear exchange differences manually - tracker.exchange_differences = [] - tracker.save() - - # Verify differences are cleared - tracker.reload() - self.assertEqual(len(tracker.exchange_differences), 0) - - # Recalculate differences - result = tracker.recalculate_differences() - self.assertTrue(result) - - # Verify differences are recalculated - tracker.reload() - self.assertEqual(len(tracker.exchange_differences), initial_differences_count) + def setUp(self): + """Set up test data""" + self.company = "_Test Company" + self.supplier = "_Test Supplier USD" + self.currency = "USD" + self.original_rate = 2500.0 # 1 USD = 2500 TZS + self.payment_rate = 2600.0 # 1 USD = 2600 TZS (currency strengthened) + + # Create test supplier group if not exists + if not frappe.db.exists("Supplier Group", "_Test Supplier Group"): + supplier_group = frappe.get_doc( + {"doctype": "Supplier Group", "supplier_group_name": "_Test Supplier Group"} + ) + supplier_group.insert(ignore_permissions=True) + + # Create test supplier if not exists + if not frappe.db.exists("Supplier", self.supplier): + supplier_doc = frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": self.supplier, + "supplier_group": "_Test Supplier Group", + "supplier_type": "Company", + } + ) + supplier_doc.insert(ignore_permissions=True) + + # Create Foreign Import Settings if not exists + if not frappe.db.exists("Foreign Import Settings"): + settings = frappe.get_doc( + { + "doctype": "Foreign Import Settings", + "company": self.company, + "exchange_difference_threshold": 0.01, + "auto_create_journal_entries": 0, # Disable to avoid account setup issues + "enable_lcv_exchange_tracking": 1, + } + ) + settings.insert(ignore_permissions=True) + + def tearDown(self): + """Clean up test data""" + # Cancel and delete test documents + frappe.db.rollback() + + def test_automatic_tracker_creation_on_foreign_pi_submit(self): + """Test that Foreign Import Transaction is created automatically when foreign PI is submitted""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + do_not_submit=True, + ) + + # Check no tracker exists before submission + tracker_before = frappe.db.exists("Foreign Import Transaction", {"purchase_invoice": pi.name}) + self.assertIsNone(tracker_before) + + # Submit the Purchase Invoice + pi.submit() + + # Check tracker is created after submission + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + self.assertIsNotNone(tracker_name) + + # Verify tracker details + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + self.assertEqual(tracker.purchase_invoice, pi.name) + self.assertEqual(tracker.supplier, pi.supplier) + self.assertEqual(tracker.currency, pi.currency) + self.assertEqual(tracker.original_exchange_rate, pi.conversion_rate) + self.assertEqual(tracker.invoice_amount_foreign, pi.grand_total) + self.assertEqual(tracker.invoice_amount_base, pi.base_grand_total) + self.assertEqual(tracker.status, "Active") + self.assertEqual(tracker.docstatus, 1) + + def test_no_tracker_creation_for_base_currency_pi(self): + """Test that no tracker is created for base currency Purchase Invoice""" + # Get company's default currency + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + + # Create base currency Purchase Invoice + pi = make_purchase_invoice( + supplier="_Test Supplier", + currency=company_currency, # Use actual company currency + rate=100, + do_not_submit=True, + ) + + pi.submit() + + # Check no tracker is created + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + self.assertIsNone(tracker_name) + + def test_payment_entry_linking_and_exchange_calculation(self): + """Test that Payment Entry links to tracker and calculates exchange differences""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, # Total: 1000 USD + do_not_submit=True, + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry with different exchange rate + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = self.payment_rate # Different rate + pe.paid_amount = 500 # Pay half the invoice + pe.base_paid_amount = 500 * self.payment_rate + pe.insert() + pe.submit() + + # Reload tracker to check if payment was linked + tracker.reload() + + # Verify payment was linked + self.assertEqual(len(tracker.payments), 1) + payment_row = tracker.payments[0] + self.assertEqual(payment_row.payment_entry, pe.name) + self.assertEqual(payment_row.payment_amount_foreign, 500) + self.assertEqual(payment_row.payment_exchange_rate, self.payment_rate) + + # Verify exchange difference calculation + expected_diff = 500 * (self.payment_rate - self.original_rate) # 500 * (2600 - 2500) = 50,000 + self.assertEqual(payment_row.exchange_difference, expected_diff) + + # Verify exchange difference entry was created + self.assertEqual(len(tracker.exchange_differences), 1) + diff_row = tracker.exchange_differences[0] + self.assertEqual(diff_row.reference_type, "Payment Entry") + self.assertEqual(diff_row.reference_name, pe.name) + self.assertEqual(diff_row.difference_type, "Gain") # Rate increased + self.assertEqual(diff_row.amount, expected_diff) + + # Verify status is still Active (partial payment) + self.assertEqual(tracker.status, "Active") + + def test_status_change_to_completed_on_full_payment(self): + """Test that status changes to Completed when full payment is made""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, # Total: 1000 USD + do_not_submit=True, + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry for full amount + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = self.payment_rate + pe.paid_amount = 1000 # Full payment + pe.base_paid_amount = 1000 * self.payment_rate + pe.insert() + pe.submit() + + # Reload tracker + tracker.reload() + + # Verify status changed to Completed + self.assertEqual(tracker.status, "Completed") + + def test_exchange_loss_calculation(self): + """Test exchange loss calculation when currency weakens""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, + do_not_submit=True, + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry with lower exchange rate (currency weakened) + weaker_rate = 2400.0 # 1 USD = 2400 TZS (currency weakened) + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = weaker_rate + pe.paid_amount = 500 + pe.base_paid_amount = 500 * weaker_rate + pe.insert() + pe.submit() + + # Reload tracker + tracker.reload() + + # Verify exchange loss calculation + expected_diff = 500 * (weaker_rate - self.original_rate) # 500 * (2400 - 2500) = -50,000 + payment_row = tracker.payments[0] + self.assertEqual(payment_row.exchange_difference, expected_diff) + + # Verify exchange difference entry shows Loss + diff_row = tracker.exchange_differences[0] + self.assertEqual(diff_row.difference_type, "Loss") + self.assertEqual(diff_row.amount, abs(expected_diff)) # Amount is always positive + + def test_tracker_cancellation_on_pi_cancel(self): + """Test that tracker is cancelled when Purchase Invoice is cancelled""" + # Create and submit foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + do_not_submit=True, + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + self.assertEqual(tracker.docstatus, 1) + + # Cancel the Purchase Invoice + pi.cancel() + + # Reload tracker and verify it's cancelled + tracker.reload() + self.assertEqual(tracker.docstatus, 2) + self.assertEqual(tracker.status, "Cancelled") + + def test_currency_validation(self): + """Test that tracker validates foreign currency requirement""" + # Get company's default currency + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") + + # Create tracker manually with same currency as company + tracker = frappe.get_doc( + { + "doctype": "Foreign Import Transaction", + "purchase_invoice": "TEST-PI-001", + "supplier": self.supplier, + "currency": company_currency, # Same as company currency + "company": self.company, + } + ) + + # Should throw error on validation + with self.assertRaises(frappe.ValidationError): + tracker.insert() + + def test_totals_calculation(self): + """Test that totals are calculated correctly""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, + do_not_submit=True, + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Add manual exchange differences + tracker.add_exchange_difference("Manual Entry", "TEST-001", "Gain", 25000, nowdate(), "Test gain") + tracker.add_exchange_difference("Manual Entry", "TEST-002", "Loss", 15000, nowdate(), "Test loss") + + # Verify totals + self.assertEqual(tracker.total_gain_loss, 10000) # 25000 - 15000 + self.assertEqual(tracker.net_difference, 10000) + + def test_exchange_summary_method(self): + """Test the get_exchange_summary method""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, + do_not_submit=True, + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = self.payment_rate + pe.paid_amount = 500 + pe.base_paid_amount = 500 * self.payment_rate + pe.insert() + pe.submit() + + # Reload tracker + tracker.reload() + + # Get exchange summary + summary = tracker.get_exchange_summary() + + # Verify summary + expected_gain = 500 * (self.payment_rate - self.original_rate) + self.assertEqual(summary["total_gain"], expected_gain) + self.assertEqual(summary["total_loss"], 0) + self.assertEqual(summary["payment_differences"], expected_gain) + self.assertEqual(summary["lcv_differences"], 0) + self.assertEqual(summary["manual_entries"], 0) + self.assertEqual(summary["net_difference"], expected_gain) + + def test_no_duplicate_tracker_creation(self): + """Test that duplicate trackers are not created for same PI""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + do_not_submit=True, + ) + pi.submit() + + # Get initial tracker count + initial_count = frappe.db.count("Foreign Import Transaction", {"purchase_invoice": pi.name}) + self.assertEqual(initial_count, 1) + + # Try to trigger tracker creation again (simulate hook being called again) + from csf_tz.csftz_hooks.exchange_calculations import create_import_tracker + + create_import_tracker(pi, "on_submit") + + # Verify no duplicate tracker was created + final_count = frappe.db.count("Foreign Import Transaction", {"purchase_invoice": pi.name}) + self.assertEqual(final_count, 1) + + def test_payment_currency_mismatch_no_linking(self): + """Test that payment with different currency doesn't link to tracker""" + # Create foreign currency Purchase Invoice in USD + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, # USD + conversion_rate=self.original_rate, + rate=100, + do_not_submit=True, + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry in different currency (EUR) + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.paid_to_account_currency = "EUR" # Different currency + pe.source_exchange_rate = 2800.0 # EUR rate + pe.paid_amount = 500 + pe.base_paid_amount = 500 * 2800 + pe.insert() + pe.submit() + + # Reload tracker and verify no payment was linked + tracker.reload() + self.assertEqual(len(tracker.payments), 0) + self.assertEqual(len(tracker.exchange_differences), 0) + + def test_recalculate_differences_method(self): + """Test the recalculate_differences method""" + # Create foreign currency Purchase Invoice + pi = make_purchase_invoice( + supplier=self.supplier, + currency=self.currency, + conversion_rate=self.original_rate, + rate=100, + qty=10, + do_not_submit=True, + ) + pi.submit() + + # Get the created tracker + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", {"purchase_invoice": pi.name}, "name" + ) + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Create Payment Entry + pe = get_payment_entry("Purchase Invoice", pi.name) + pe.source_exchange_rate = self.payment_rate + pe.paid_amount = 500 + pe.base_paid_amount = 500 * self.payment_rate + pe.insert() + pe.submit() + + # Reload tracker + tracker.reload() + initial_differences_count = len(tracker.exchange_differences) + + # Clear exchange differences manually + tracker.exchange_differences = [] + tracker.save() + + # Verify differences are cleared + tracker.reload() + self.assertEqual(len(tracker.exchange_differences), 0) + + # Recalculate differences + result = tracker.recalculate_differences() + self.assertTrue(result) + + # Verify differences are recalculated + tracker.reload() + self.assertEqual(len(tracker.exchange_differences), initial_differences_count) diff --git a/csf_tz/csf_tz/doctype/latra_licenses/latra_licenses.py b/csf_tz/csf_tz/doctype/latra_licenses/latra_licenses.py index 7184b38b..b777e056 100644 --- a/csf_tz/csf_tz/doctype/latra_licenses/latra_licenses.py +++ b/csf_tz/csf_tz/doctype/latra_licenses/latra_licenses.py @@ -1,26 +1,24 @@ # Copyright (c) 2026, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import time from time import sleep import frappe import requests -from frappe.utils import cint -from frappe.utils import getdate, now_datetime, nowdate from frappe.model.document import Document +from frappe.utils import cint, getdate, now_datetime, nowdate +from csf_tz.csf_tz.doctype.vehicle_fine_record.vehicle_fine_record import ( + is_valid_number_plate, + normalize_number_plate, +) from csf_tz.vehicle_authority import ( get_unique_vehicle_plates, is_authority_notification_event_enabled, send_authority_notification, ) -from csf_tz.csf_tz.doctype.vehicle_fine_record.vehicle_fine_record import ( - is_valid_number_plate, - normalize_number_plate, -) LATRA_GQL_URL = "https://rrims.latra.go.tz:8086/graphql" LATRA_LOGIN_URL = "https://rrims.latra.go.tz:8086/user/login" @@ -248,6 +246,8 @@ def send_pending_latra_offence_notifications(): _notify_latra_offence(row.name, values, is_new=False, old_status=last_status) frappe.db.commit() + + def sync_all_latra_licenses(token): plates = get_unique_vehicle_plates( normalize_number_plate=normalize_number_plate, @@ -261,9 +261,7 @@ def sync_all_latra_licenses(token): licenses_by_plate = {} for lic in all_licenses: - vehicle_reg = normalize_number_plate( - (lic.get("vehicle") or {}).get("vehicleRegistrationNumber") - ) + vehicle_reg = normalize_number_plate((lic.get("vehicle") or {}).get("vehicleRegistrationNumber")) if not vehicle_reg: continue licenses_by_plate.setdefault(vehicle_reg, []).append(lic) @@ -342,7 +340,14 @@ def notify_latra_license_expiry(): for row in frappe.get_all( "Latra Licenses", - fields=["name", "vehicle", "license_number", "license_status", "expire_date", "authority_last_expiry_notification_key"], + fields=[ + "name", + "vehicle", + "license_number", + "license_status", + "expire_date", + "authority_last_expiry_notification_key", + ], limit_page_length=0, ): if not row.expire_date: @@ -433,14 +438,10 @@ def _notify_latra_offence(docname, values, is_new=False, old_status=None): def _log_sync_summary(license_result, offence_result): try: license_message = ( - license_result.get("message") - if isinstance(license_result, dict) - else str(license_result) + license_result.get("message") if isinstance(license_result, dict) else str(license_result) ) offence_message = ( - offence_result.get("message") - if isinstance(offence_result, dict) - else str(offence_result) + offence_result.get("message") if isinstance(offence_result, dict) else str(offence_result) ) frappe.log_error( title="LATRA Sync Summary", @@ -565,6 +566,7 @@ def _refresh_token_locked(): def _token_cache_key(): return f"{frappe.local.site}:latra_access_token" + def _fetch_all_offences(token): page_size = 500 page_index = 0 @@ -575,7 +577,7 @@ def _fetch_all_offences(token): if result in (TOKEN_EXPIRED, "RATE_LIMITED", None): return result - page = (result.get("allMyClientOffencesPageable") or {}) + page = result.get("allMyClientOffencesPageable") or {} content = page.get("content") or [] total_elements = cint(page.get("totalElements") or 0) @@ -685,6 +687,8 @@ def _call_offences_graphql_page(token, first=0, size=500): message=f"Non-JSON response on page {first}: {response.text[:500] if response else 'No response'}", ) return None + + def _fetch_all_licenses(token): page_size = 200 page_index = 0 @@ -695,7 +699,7 @@ def _fetch_all_licenses(token): if result in (TOKEN_EXPIRED, "RATE_LIMITED", None): return result - content = ((result.get("findMyCurrentLicensesPageable") or {}).get("content") or []) + content = (result.get("findMyCurrentLicensesPageable") or {}).get("content") or [] if not content: break @@ -737,9 +741,7 @@ def _call_license_page(token, first=0, size=200): if attempt > 0: sleep(5 * attempt) - response = requests.post( - LATRA_GQL_URL, json=payload, headers=headers, timeout=timeout - ) + response = requests.post(LATRA_GQL_URL, json=payload, headers=headers, timeout=timeout) if response.status_code == 401: return TOKEN_EXPIRED @@ -801,11 +803,9 @@ def _parse_date(value): def _get_place_issued(license_row): - branch = ( - ((license_row.get("licenseInfoDetail") or {}).get("currentLicenseApplication") or {}) - .get("branch") - or {} - ) + branch = ((license_row.get("licenseInfoDetail") or {}).get("currentLicenseApplication") or {}).get( + "branch" + ) or {} district = branch.get("district") or {} branch_name = branch.get("name") or "" district_name = district.get("districtName") or "" diff --git a/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py b/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py index 6b225b57..f35e4d32 100644 --- a/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py +++ b/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals # import frappe from frappe.model.document import Document + class NMBCallback(Document): pass diff --git a/csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py b/csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py index 8a6e55b6..736abd53 100644 --- a/csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py +++ b/csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and Contributors # See license.txt -from __future__ import unicode_literals # import frappe import unittest + class TestNMBCallback(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.py b/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.py index 90a8767d..fe2c6493 100644 --- a/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.py +++ b/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.py @@ -1,48 +1,50 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe -from frappe.model.document import Document -from frappe import _ import binascii import os -from csf_tz.bank_api import invoice_submission, cancel_invoice + +import frappe +from frappe import _ +from frappe.model.document import Document + +from csf_tz.bank_api import cancel_invoice, invoice_submission + class StudentApplicantFees(Document): def after_insert(self): if not check_send_fee_details_to_bank(self.company): return self.callback_token = binascii.hexlify(os.urandom(14)).decode() + self.db_set("callback_token", self.callback_token) series = frappe.get_value("Company", self.company, "nmb_series") or "" if not series: - frappe.throw(_("Please set NMB User Series in Company {0}".format(self.company))) - reference = str(series) + 'R' + str(self.name) + frappe.throw(_(f"Please set NMB User Series in Company {self.company}")) + reference = str(series) + "R" + str(self.name) if not self.abbr: self.abbr = frappe.get_value("Company", self.company, "abbr") or "" - self.bank_reference = reference.replace('-', '').replace('RFEE'+self.abbr,'') - + self.db_set("abbr", self.abbr) + self.bank_reference = reference.replace("-", "").replace("RFEE" + self.abbr, "") + self.db_set("bank_reference", self.bank_reference) def on_submit(self): if not check_send_fee_details_to_bank(self.company): return invoice_submission(self) - def on_cancel(self): - if check_send_fee_details_to_bank(self.company): + if check_send_fee_details_to_bank(self.company): cancel_invoice(self, "on_cancel") doc = frappe.get_doc("Student Applicant", self.student) doc.bank_reference = None doc.student_applicant_fee = None doc.application_status = "Applied" doc.db_update() - + def check_send_fee_details_to_bank(company): send_fee_details_to_bank = frappe.get_value("Company", company, "send_fee_details_to_bank") or 0 if not send_fee_details_to_bank: return False else: - return True \ No newline at end of file + return True diff --git a/csf_tz/csf_tz/doctype/student_applicant_fees/test_student_applicant_fees.py b/csf_tz/csf_tz/doctype/student_applicant_fees/test_student_applicant_fees.py index 98bb30f9..90dc2f17 100644 --- a/csf_tz/csf_tz/doctype/student_applicant_fees/test_student_applicant_fees.py +++ b/csf_tz/csf_tz/doctype/student_applicant_fees/test_student_applicant_fees.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and Contributors # See license.txt -from __future__ import unicode_literals # import frappe import unittest + class TestStudentApplicantFees(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/tra_tax_inv/test_tra_tax_inv.py b/csf_tz/csf_tz/doctype/tra_tax_inv/test_tra_tax_inv.py index 83e8468a..6029315b 100644 --- a/csf_tz/csf_tz/doctype/tra_tax_inv/test_tra_tax_inv.py +++ b/csf_tz/csf_tz/doctype/tra_tax_inv/test_tra_tax_inv.py @@ -1,20 +1,22 @@ # Copyright (c) 2025, Aakvatech and Contributors # See license.txt -import frappe import unittest +import frappe + + class TestTRATAXInv(unittest.TestCase): - def test_tra_tax_inv_creation(self): - """Test basic TRA Tax Inv creation""" - doc = frappe.new_doc("TRA Tax Inv") - doc.verification_code = "TEST123_123456" - doc.type = "Sales" - doc.verification_status = "Pending" - - # This should not raise an error - doc.validate() - - self.assertEqual(doc.type, "Sales") - self.assertEqual(doc.verification_status, "Pending") - self.assertEqual(doc.verification_code, "TEST123_123456") + def test_tra_tax_inv_creation(self): + """Test basic TRA Tax Inv creation""" + doc = frappe.new_doc("TRA Tax Inv") + doc.verification_code = "TEST123_123456" + doc.type = "Sales" + doc.verification_status = "Pending" + + # This should not raise an error + doc.validate() + + self.assertEqual(doc.type, "Sales") + self.assertEqual(doc.verification_status, "Pending") + self.assertEqual(doc.verification_code, "TEST123_123456") diff --git a/csf_tz/csf_tz/doctype/tra_tax_inv/tra_tax_inv.py b/csf_tz/csf_tz/doctype/tra_tax_inv/tra_tax_inv.py index 35a4cb42..fb4681a7 100644 --- a/csf_tz/csf_tz/doctype/tra_tax_inv/tra_tax_inv.py +++ b/csf_tz/csf_tz/doctype/tra_tax_inv/tra_tax_inv.py @@ -1,1291 +1,1228 @@ # Copyright (c) 2025, Aakvatech and contributors # For license information, please see license.txt + import frappe -from frappe.model.document import Document import requests from bs4 import BeautifulSoup -from datetime import datetime +from frappe.model.document import Document class TRATAXInv(Document): - def validate(self): - """Validate the document""" - pass + def validate(self): + """Validate the document""" + pass - def create_purchase_invoice(self): - """Create Purchase Invoice from TRA Tax Inv""" - return create_invoice_from_tra_tax_inv(self.name, "Purchase Invoice") + def create_purchase_invoice(self): + """Create Purchase Invoice from TRA Tax Inv""" + return create_invoice_from_tra_tax_inv(self.name, "Purchase Invoice") - def create_sales_invoice(self): - """Create Sales Invoice from TRA Tax Inv""" - return create_invoice_from_tra_tax_inv(self.name, "Sales Invoice") + def create_sales_invoice(self): + """Create Sales Invoice from TRA Tax Inv""" + return create_invoice_from_tra_tax_inv(self.name, "Sales Invoice") @frappe.whitelist(allow_guest=True) def verify_tra_receipt(verification_code=None, qr_code_data=None): - """ - Verify TRA receipt and create TRA TAX Inv document - - Args: - verification_code (str): The verification code from TRA receipt - qr_code_data (str): Alternative parameter - full URL from QR code - - Returns: - dict: Simple response with success status and document info - """ - try: - # Handle both verification_code and qr_code_data parameters - if qr_code_data and not verification_code: - # Extract verification code from QR code URL - if "verify.tra.go.tz/" in qr_code_data: - verification_code = qr_code_data.split("verify.tra.go.tz/")[-1] - - else: - return { - "success": False, - "message": "Invalid QR code data format", - "qr_code_data": qr_code_data, - } - - # Also handle case where verification_code itself contains the full URL - if verification_code and "verify.tra.go.tz/" in verification_code: - verification_code = verification_code.split("verify.tra.go.tz/")[-1] - - if not verification_code: - return {"success": False, "message": "No verification code provided"} - - # Try TRA verification - receipt_data = {} - verification_success = False - - try: - verification_result = fetch_tra_verification(verification_code) - if "error" not in verification_result: - receipt_data = extract_receipt_data( - verification_result["verification_data"] - ) - verification_success = True - - except Exception as e: - frappe.logger().error(f"TRA verification failed: {str(e)}") - - # Always create TRA TAX Inv document - tra_result = create_tra_tax_inv_document_safe( - verification_code, receipt_data, {"success": verification_success} - ) - - if tra_result.get("success"): - return { - "success": True, - "verification_code": verification_code, - "message": tra_result.get( - "message", "TRA TAX Inv created successfully" - ), - "doc_name": tra_result.get("doc_name", ""), - "company_name": tra_result.get("company_name", ""), - "receipt_number": tra_result.get("receipt_number", ""), - "total": tra_result.get("grand_total", 0), - } - else: - return { - "success": False, - "verification_code": verification_code, - "message": f"Failed to create document: {tra_result.get('message', 'Unknown error')}", - } - - except Exception as e: - return { - "success": False, - "verification_code": verification_code, - "message": f"Critical error: {str(e)}", - } + """ + Verify TRA receipt and create TRA TAX Inv document + + Args: + verification_code (str): The verification code from TRA receipt + qr_code_data (str): Alternative parameter - full URL from QR code + + Returns: + dict: Simple response with success status and document info + """ + try: + # Handle both verification_code and qr_code_data parameters + if qr_code_data and not verification_code: + # Extract verification code from QR code URL + if "verify.tra.go.tz/" in qr_code_data: + verification_code = qr_code_data.split("verify.tra.go.tz/")[-1] + + else: + return { + "success": False, + "message": "Invalid QR code data format", + "qr_code_data": qr_code_data, + } + + # Also handle case where verification_code itself contains the full URL + if verification_code and "verify.tra.go.tz/" in verification_code: + verification_code = verification_code.split("verify.tra.go.tz/")[-1] + + if not verification_code: + return {"success": False, "message": "No verification code provided"} + + # Try TRA verification + receipt_data = {} + verification_success = False + + try: + verification_result = fetch_tra_verification(verification_code) + if "error" not in verification_result: + receipt_data = extract_receipt_data(verification_result["verification_data"]) + verification_success = True + + except Exception as e: + frappe.logger().error(f"TRA verification failed: {str(e)}") + + # Always create TRA TAX Inv document + tra_result = create_tra_tax_inv_document_safe( + verification_code, receipt_data, {"success": verification_success} + ) + + if tra_result.get("success"): + return { + "success": True, + "verification_code": verification_code, + "message": tra_result.get("message", "TRA TAX Inv created successfully"), + "doc_name": tra_result.get("doc_name", ""), + "company_name": tra_result.get("company_name", ""), + "receipt_number": tra_result.get("receipt_number", ""), + "total": tra_result.get("grand_total", 0), + } + else: + return { + "success": False, + "verification_code": verification_code, + "message": f"Failed to create document: {tra_result.get('message', 'Unknown error')}", + } + + except Exception as e: + return { + "success": False, + "verification_code": verification_code, + "message": f"Critical error: {str(e)}", + } def fetch_tra_verification(verification_code): - """ - Fetch HTML content from TRA verification by submitting the form and handling time selection - - Args: - verification_code (str): The receipt verification code (e.g., "3D89A530626_094801") - - Returns: - dict: Contains HTML content, parsed data, and response info - """ - - # Extract time from verification code (format: XXXXXXX_HHMMSS) - if "_" in verification_code: - time_part = verification_code.split("_")[1] - if len(time_part) == 6: - hour = time_part[:2] - minute = time_part[2:4] - second = time_part[4:6] - receipt_time = f"{hour}:{minute}:{second}" - else: - return {"error": "Invalid time format in verification code"} - else: - return {"error": "Verification code does not contain time information"} - - # Set up the session with proper headers - session = requests.Session() - - # Headers to mimic a real browser request - headers = { - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", - "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", - "Accept-Language": "en-US,en;q=0.5", - "Accept-Encoding": "gzip, deflate", - "Connection": "keep-alive", - "Upgrade-Insecure-Requests": "1", - } - - session.headers.update(headers) - - try: - form_url = "https://verify.tra.go.tz/Home/Index" - form_response = session.get(form_url, timeout=30) - form_response.raise_for_status() - - form_soup = BeautifulSoup(form_response.text, "html.parser") - token_input = form_soup.find("input", {"name": "__RequestVerificationToken"}) - - if not token_input: - return {"error": "Could not find verification token in form"} - - verification_token = token_input.get("value") - - form_data = { - "__RequestVerificationToken": verification_token, - "RctVcode": verification_code, - } - - session.headers.update( - { - "Content-Type": "application/x-www-form-urlencoded", - "Referer": form_url, - "Origin": "https://verify.tra.go.tz", - } - ) - - response = session.post( - form_url, data=form_data, timeout=30, allow_redirects=True - ) - response.raise_for_status() - - if "Please provide your Receipt time" in response.text: - - verification_url = ( - f"https://verify.tra.go.tz/Verify/Verified?Secret={receipt_time}" - ) - - session.headers.update({"Referer": response.url}) - - final_response = session.get(verification_url, timeout=30) - final_response.raise_for_status() - - response = final_response - - # Parse the HTML content - soup = BeautifulSoup(response.text, "html.parser") - - # Extract useful information - result = { - "status_code": response.status_code, - "url": response.url, - "html_content": response.text, - "title": soup.title.string if soup.title else None, - "headers": dict(response.headers), - "cookies": dict(response.cookies), - "verification_code_used": verification_code, - "receipt_time_used": receipt_time, - "form_token_used": verification_token, - } - - # Try to extract specific verification data - verification_data = extract_verification_data(soup) - result["verification_data"] = verification_data - - # Also store the HTML content for direct parsing - result["verification_data"]["html_content"] = response.text - - return result - - except requests.exceptions.RequestException as e: - return {"error": f"Request failed: {str(e)}", "status_code": None} + """ + Fetch HTML content from TRA verification by submitting the form and handling time selection + + Args: + verification_code (str): The receipt verification code (e.g., "3D89A530626_094801") + + Returns: + dict: Contains HTML content, parsed data, and response info + """ + + # Extract time from verification code (format: XXXXXXX_HHMMSS) + if "_" in verification_code: + time_part = verification_code.split("_")[1] + if len(time_part) == 6: + hour = time_part[:2] + minute = time_part[2:4] + second = time_part[4:6] + receipt_time = f"{hour}:{minute}:{second}" + else: + return {"error": "Invalid time format in verification code"} + else: + return {"error": "Verification code does not contain time information"} + + # Set up the session with proper headers + session = requests.Session() + + # Headers to mimic a real browser request + headers = { + "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36", + "Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "Accept-Language": "en-US,en;q=0.5", + "Accept-Encoding": "gzip, deflate", + "Connection": "keep-alive", + "Upgrade-Insecure-Requests": "1", + } + + session.headers.update(headers) + + try: + form_url = "https://verify.tra.go.tz/Home/Index" + form_response = session.get(form_url, timeout=30) + form_response.raise_for_status() + + form_soup = BeautifulSoup(form_response.text, "html.parser") + token_input = form_soup.find("input", {"name": "__RequestVerificationToken"}) + + if not token_input: + return {"error": "Could not find verification token in form"} + + verification_token = token_input.get("value") + + form_data = { + "__RequestVerificationToken": verification_token, + "RctVcode": verification_code, + } + + session.headers.update( + { + "Content-Type": "application/x-www-form-urlencoded", + "Referer": form_url, + "Origin": "https://verify.tra.go.tz", + } + ) + + response = session.post(form_url, data=form_data, timeout=30, allow_redirects=True) + response.raise_for_status() + + if "Please provide your Receipt time" in response.text: + verification_url = f"https://verify.tra.go.tz/Verify/Verified?Secret={receipt_time}" + + session.headers.update({"Referer": response.url}) + + final_response = session.get(verification_url, timeout=30) + final_response.raise_for_status() + + response = final_response + + # Parse the HTML content + soup = BeautifulSoup(response.text, "html.parser") + + # Extract useful information + result = { + "status_code": response.status_code, + "url": response.url, + "html_content": response.text, + "title": soup.title.string if soup.title else None, + "headers": dict(response.headers), + "cookies": dict(response.cookies), + "verification_code_used": verification_code, + "receipt_time_used": receipt_time, + "form_token_used": verification_token, + } + + # Try to extract specific verification data + verification_data = extract_verification_data(soup) + result["verification_data"] = verification_data + + # Also store the HTML content for direct parsing + result["verification_data"]["html_content"] = response.text + + return result + + except requests.exceptions.RequestException as e: + return {"error": f"Request failed: {str(e)}", "status_code": None} def extract_verification_data(soup): - """ - Extract specific verification information from the HTML - - Args: - soup: BeautifulSoup object of the HTML - - Returns: - dict: Extracted verification data - """ - data = {} - - # Try to find verification status - status_elements = soup.find_all( - ["div", "span", "p"], - class_=lambda x: x - and any( - word in x.lower() for word in ["status", "verified", "valid", "invalid"] - ), - ) - if status_elements: - data["status_elements"] = [ - elem.get_text(strip=True) for elem in status_elements - ] - - # Look for tables (common in verification systems) - tables = soup.find_all("table") - if tables: - data["tables"] = [] - for table in tables: - table_data = [] - rows = table.find_all("tr") - for row in rows: - cells = row.find_all(["td", "th"]) - row_data = [cell.get_text(strip=True) for cell in cells] - if row_data: # Only add non-empty rows - table_data.append(row_data) - if table_data: - data["tables"].append(table_data) - - # Look for any form inputs (might contain hidden verification data) - inputs = soup.find_all("input") - if inputs: - data["form_inputs"] = [] - for inp in inputs: - input_data = { - "name": inp.get("name"), - "value": inp.get("value"), - "type": inp.get("type"), - } - data["form_inputs"].append(input_data) - - # Extract all text content for analysis - data["all_text"] = soup.get_text(separator=" ", strip=True) - - return data + """ + Extract specific verification information from the HTML + + Args: + soup: BeautifulSoup object of the HTML + + Returns: + dict: Extracted verification data + """ + data = {} + + # Try to find verification status + status_elements = soup.find_all( + ["div", "span", "p"], + class_=lambda x: x and any(word in x.lower() for word in ["status", "verified", "valid", "invalid"]), + ) + if status_elements: + data["status_elements"] = [elem.get_text(strip=True) for elem in status_elements] + + # Look for tables (common in verification systems) + tables = soup.find_all("table") + if tables: + data["tables"] = [] + for table in tables: + table_data = [] + rows = table.find_all("tr") + for row in rows: + cells = row.find_all(["td", "th"]) + row_data = [cell.get_text(strip=True) for cell in cells] + if row_data: # Only add non-empty rows + table_data.append(row_data) + if table_data: + data["tables"].append(table_data) + + # Look for any form inputs (might contain hidden verification data) + inputs = soup.find_all("input") + if inputs: + data["form_inputs"] = [] + for inp in inputs: + input_data = { + "name": inp.get("name"), + "value": inp.get("value"), + "type": inp.get("type"), + } + data["form_inputs"].append(input_data) + + # Extract all text content for analysis + data["all_text"] = soup.get_text(separator=" ", strip=True) + + return data def extract_receipt_data(verification_data): - """ - Extract structured receipt data from verification data - - Args: - verification_data (dict): Raw verification data from TRA - - Returns: - dict: Structured receipt data - """ - receipt_data = { - "items": [], - "totals": {}, - "taxes": [], - "receipt_info": {}, - "company_info": {}, - "customer_info": {}, - } - - if not verification_data: - return receipt_data - - # If we have HTML content, parse it directly - html_content = verification_data.get("html_content", "") - if html_content: - return extract_receipt_from_html(html_content) - - # Fallback to table-based extraction for backward compatibility - if "tables" not in verification_data: - return receipt_data - - # Process tables to extract receipt information - for table in verification_data["tables"]: - # Look for items table (usually has Description, Qty, Amount columns) - if len(table) > 1 and len(table[0]) >= 3: - header_row = [cell.lower() for cell in table[0]] - if any( - word in " ".join(header_row) - for word in ["description", "qty", "amount", "quantity"] - ): - # This looks like an items table - for row in table[1:]: # Skip header - if len(row) >= 3 and any(cell.strip() for cell in row): - item = { - "description": row[0].strip() if len(row) > 0 else "", - "quantity": row[1].strip() if len(row) > 1 else "", - "amount": row[2].strip() if len(row) > 2 else "", - } - if item["description"]: # Only add if has description - receipt_data["items"].append(item) - - # Look for totals/summary information - for row in table: - if len(row) >= 2: - key = row[0].strip().lower() - value = row[1].strip() if len(row) > 1 else "" - - # Map common receipt fields - if "total" in key and "excl" in key: - receipt_data["totals"]["subtotal"] = value - elif "total" in key and "incl" in key: - receipt_data["totals"]["grand_total"] = value - elif "tax" in key and "total" in key: - receipt_data["totals"]["total_tax"] = value - elif "vat" in key: - receipt_data["taxes"].append({"type": "VAT", "amount": value}) - elif "receipt" in key and "no" in key: - receipt_data["receipt_info"]["receipt_number"] = value - elif "date" in key: - receipt_data["receipt_info"]["date"] = value - elif "time" in key: - receipt_data["receipt_info"]["time"] = value - elif "tin" in key: - receipt_data["company_info"]["tin"] = value - elif "vrn" in key: - receipt_data["company_info"]["vrn"] = value - - return receipt_data + """ + Extract structured receipt data from verification data + + Args: + verification_data (dict): Raw verification data from TRA + + Returns: + dict: Structured receipt data + """ + receipt_data = { + "items": [], + "totals": {}, + "taxes": [], + "receipt_info": {}, + "company_info": {}, + "customer_info": {}, + } + + if not verification_data: + return receipt_data + + # If we have HTML content, parse it directly + html_content = verification_data.get("html_content", "") + if html_content: + return extract_receipt_from_html(html_content) + + # Fallback to table-based extraction for backward compatibility + if "tables" not in verification_data: + return receipt_data + + # Process tables to extract receipt information + for table in verification_data["tables"]: + # Look for items table (usually has Description, Qty, Amount columns) + if len(table) > 1 and len(table[0]) >= 3: + header_row = [cell.lower() for cell in table[0]] + if any(word in " ".join(header_row) for word in ["description", "qty", "amount", "quantity"]): + # This looks like an items table + for row in table[1:]: # Skip header + if len(row) >= 3 and any(cell.strip() for cell in row): + item = { + "description": row[0].strip() if len(row) > 0 else "", + "quantity": row[1].strip() if len(row) > 1 else "", + "amount": row[2].strip() if len(row) > 2 else "", + } + if item["description"]: # Only add if has description + receipt_data["items"].append(item) + + # Look for totals/summary information + for row in table: + if len(row) >= 2: + key = row[0].strip().lower() + value = row[1].strip() if len(row) > 1 else "" + + # Map common receipt fields + if "total" in key and "excl" in key: + receipt_data["totals"]["subtotal"] = value + elif "total" in key and "incl" in key: + receipt_data["totals"]["grand_total"] = value + elif "tax" in key and "total" in key: + receipt_data["totals"]["total_tax"] = value + elif "vat" in key: + receipt_data["taxes"].append({"type": "VAT", "amount": value}) + elif "receipt" in key and "no" in key: + receipt_data["receipt_info"]["receipt_number"] = value + elif "date" in key: + receipt_data["receipt_info"]["date"] = value + elif "time" in key: + receipt_data["receipt_info"]["time"] = value + elif "tin" in key: + receipt_data["company_info"]["tin"] = value + elif "vrn" in key: + receipt_data["company_info"]["vrn"] = value + + return receipt_data def extract_receipt_from_html(html_content): - """ - Extract receipt data directly from HTML content using BeautifulSoup + """ + Extract receipt data directly from HTML content using BeautifulSoup - Args: - html_content (str): Raw HTML content from TRA verification + Args: + html_content (str): Raw HTML content from TRA verification - Returns: - dict: Structured receipt data - """ - receipt_data = { - "items": [], - "totals": {}, - "taxes": [], - "receipt_info": {}, - "company_info": {}, - "customer_info": {}, - "verification_info": {}, - } + Returns: + dict: Structured receipt data + """ + receipt_data = { + "items": [], + "totals": {}, + "taxes": [], + "receipt_info": {}, + "company_info": {}, + "customer_info": {}, + "verification_info": {}, + } - try: - soup = BeautifulSoup(html_content, "html.parser") + try: + soup = BeautifulSoup(html_content, "html.parser") - extract_company_info(soup, receipt_data) - extract_customer_info(soup, receipt_data) - extract_receipt_info(soup, receipt_data) - extract_items(soup, receipt_data) - extract_totals_and_taxes(soup, receipt_data) - extract_verification_info(soup, receipt_data) + extract_company_info(soup, receipt_data) + extract_customer_info(soup, receipt_data) + extract_receipt_info(soup, receipt_data) + extract_items(soup, receipt_data) + extract_totals_and_taxes(soup, receipt_data) + extract_verification_info(soup, receipt_data) - except Exception as e: - receipt_data["parsing_error"] = str(e) + except Exception as e: + receipt_data["parsing_error"] = str(e) - return receipt_data + return receipt_data def extract_company_info(soup, receipt_data): - """Extract company information from the receipt HTML""" - try: - # Look for company name in the header - company_header = soup.find("h4") - if company_header and company_header.find("b"): - receipt_data["company_info"]["name"] = company_header.find("b").get_text( - strip=True - ) - - # Extract company details from the invoice-info section - invoice_info = soup.find("div", class_="invoice-info") - if invoice_info: - text_content = invoice_info.get_text() - - # Extract TIN - if "TIN:" in text_content: - tin_match = text_content.split("TIN:")[1].split("\n")[0].strip() - receipt_data["company_info"]["tin"] = tin_match - - # Extract VRN - if "VRN:" in text_content: - vrn_match = text_content.split("VRN:")[1].split("\n")[0].strip() - receipt_data["company_info"]["vrn"] = vrn_match - - # Extract Serial Number - if "SERIAL NO:" in text_content: - serial_match = ( - text_content.split("SERIAL NO:")[1].split("\n")[0].strip() - ) - receipt_data["company_info"]["serial_number"] = serial_match - - # Extract UIN - if "UIN:" in text_content: - uin_match = text_content.split("UIN:")[1].split("\n")[0].strip() - receipt_data["company_info"]["uin"] = uin_match - - # Extract Tax Office - if "TAX OFFICE:" in text_content: - tax_office_match = ( - text_content.split("TAX OFFICE:")[1].split("\n")[0].strip() - ) - receipt_data["company_info"]["tax_office"] = tax_office_match - - # Extract Mobile - if "MOBILE:" in text_content: - mobile_match = text_content.split("MOBILE:")[1].split("\n")[0].strip() - receipt_data["company_info"]["mobile"] = mobile_match - - # Extract Address (P.O.BOX) - if "P.O.BOX" in text_content: - address_match = text_content.split("P.O.BOX")[1].split("\n")[0].strip() - receipt_data["company_info"]["address"] = f"P.O.BOX{address_match}" - - except Exception as e: - frappe.logger().error(f"Error extracting company info: {str(e)}") + """Extract company information from the receipt HTML""" + try: + # Look for company name in the header + company_header = soup.find("h4") + if company_header and company_header.find("b"): + receipt_data["company_info"]["name"] = company_header.find("b").get_text(strip=True) + + # Extract company details from the invoice-info section + invoice_info = soup.find("div", class_="invoice-info") + if invoice_info: + text_content = invoice_info.get_text() + + # Extract TIN + if "TIN:" in text_content: + tin_match = text_content.split("TIN:")[1].split("\n")[0].strip() + receipt_data["company_info"]["tin"] = tin_match + + # Extract VRN + if "VRN:" in text_content: + vrn_match = text_content.split("VRN:")[1].split("\n")[0].strip() + receipt_data["company_info"]["vrn"] = vrn_match + + # Extract Serial Number + if "SERIAL NO:" in text_content: + serial_match = text_content.split("SERIAL NO:")[1].split("\n")[0].strip() + receipt_data["company_info"]["serial_number"] = serial_match + + # Extract UIN + if "UIN:" in text_content: + uin_match = text_content.split("UIN:")[1].split("\n")[0].strip() + receipt_data["company_info"]["uin"] = uin_match + + # Extract Tax Office + if "TAX OFFICE:" in text_content: + tax_office_match = text_content.split("TAX OFFICE:")[1].split("\n")[0].strip() + receipt_data["company_info"]["tax_office"] = tax_office_match + + # Extract Mobile + if "MOBILE:" in text_content: + mobile_match = text_content.split("MOBILE:")[1].split("\n")[0].strip() + receipt_data["company_info"]["mobile"] = mobile_match + + # Extract Address (P.O.BOX) + if "P.O.BOX" in text_content: + address_match = text_content.split("P.O.BOX")[1].split("\n")[0].strip() + receipt_data["company_info"]["address"] = f"P.O.BOX{address_match}" + + except Exception as e: + frappe.logger().error(f"Error extracting company info: {str(e)}") def extract_customer_info(soup, receipt_data): - """Extract customer information from the receipt HTML""" - try: - # Find all invoice-header divs and look for customer information - invoice_headers = soup.find_all("div", class_="invoice-header") - - for header in invoice_headers: - text_content = header.get_text() - - # Extract Customer Name - if "CUSTOMER NAME:" in text_content: - customer_name = ( - text_content.split("CUSTOMER NAME:")[1].split("\n")[0].strip() - ) - receipt_data["customer_info"]["name"] = customer_name - - # Extract Customer ID Type - if "CUSTOMER ID TYPE:" in text_content: - id_type = ( - text_content.split("CUSTOMER ID TYPE:")[1].split("\n")[0].strip() - ) - receipt_data["customer_info"]["id_type"] = id_type - - # Extract Customer ID - if "CUSTOMER ID:" in text_content: - customer_id = ( - text_content.split("CUSTOMER ID:")[1].split("\n")[0].strip() - ) - receipt_data["customer_info"]["id"] = customer_id - - # Extract Customer Mobile - if "CUSTOMER MOBILE:" in text_content: - mobile = ( - text_content.split("CUSTOMER MOBILE:")[1].split("\n")[0].strip() - ) - receipt_data["customer_info"]["mobile"] = mobile - - except Exception as e: - frappe.logger().error(f"Error extracting customer info: {str(e)}") + """Extract customer information from the receipt HTML""" + try: + # Find all invoice-header divs and look for customer information + invoice_headers = soup.find_all("div", class_="invoice-header") + + for header in invoice_headers: + text_content = header.get_text() + + # Extract Customer Name + if "CUSTOMER NAME:" in text_content: + customer_name = text_content.split("CUSTOMER NAME:")[1].split("\n")[0].strip() + receipt_data["customer_info"]["name"] = customer_name + + # Extract Customer ID Type + if "CUSTOMER ID TYPE:" in text_content: + id_type = text_content.split("CUSTOMER ID TYPE:")[1].split("\n")[0].strip() + receipt_data["customer_info"]["id_type"] = id_type + + # Extract Customer ID + if "CUSTOMER ID:" in text_content: + customer_id = text_content.split("CUSTOMER ID:")[1].split("\n")[0].strip() + receipt_data["customer_info"]["id"] = customer_id + + # Extract Customer Mobile + if "CUSTOMER MOBILE:" in text_content: + mobile = text_content.split("CUSTOMER MOBILE:")[1].split("\n")[0].strip() + receipt_data["customer_info"]["mobile"] = mobile + + except Exception as e: + frappe.logger().error(f"Error extracting customer info: {str(e)}") def extract_receipt_info(soup, receipt_data): - """Extract receipt information from the receipt HTML""" - try: - # Find all invoice-header divs and look for receipt information - invoice_headers = soup.find_all("div", class_="invoice-header") - - for header in invoice_headers: - text_content = header.get_text() - - # Extract Receipt Number - if "RECEIPT NO:" in text_content: - receipt_no = text_content.split("RECEIPT NO:")[1].split("\n")[0].strip() - receipt_data["receipt_info"]["receipt_number"] = receipt_no - - # Extract Z Number - if "Z NUMBER:" in text_content: - z_number = text_content.split("Z NUMBER:")[1].split("\n")[0].strip() - receipt_data["receipt_info"]["z_number"] = z_number - - # Extract Receipt Date - if "RECEIPT DATE:" in text_content: - receipt_date = ( - text_content.split("RECEIPT DATE:")[1].split("\n")[0].strip() - ) - receipt_data["receipt_info"]["date"] = receipt_date - - # Extract Receipt Time - if "RECEIPT TIME:" in text_content: - receipt_time = ( - text_content.split("RECEIPT TIME:")[1].split("\n")[0].strip() - ) - receipt_data["receipt_info"]["time"] = receipt_time - - except Exception as e: - frappe.logger().error(f"Error extracting receipt info: {str(e)}") + """Extract receipt information from the receipt HTML""" + try: + # Find all invoice-header divs and look for receipt information + invoice_headers = soup.find_all("div", class_="invoice-header") + + for header in invoice_headers: + text_content = header.get_text() + + # Extract Receipt Number + if "RECEIPT NO:" in text_content: + receipt_no = text_content.split("RECEIPT NO:")[1].split("\n")[0].strip() + receipt_data["receipt_info"]["receipt_number"] = receipt_no + + # Extract Z Number + if "Z NUMBER:" in text_content: + z_number = text_content.split("Z NUMBER:")[1].split("\n")[0].strip() + receipt_data["receipt_info"]["z_number"] = z_number + + # Extract Receipt Date + if "RECEIPT DATE:" in text_content: + receipt_date = text_content.split("RECEIPT DATE:")[1].split("\n")[0].strip() + receipt_data["receipt_info"]["date"] = receipt_date + + # Extract Receipt Time + if "RECEIPT TIME:" in text_content: + receipt_time = text_content.split("RECEIPT TIME:")[1].split("\n")[0].strip() + receipt_data["receipt_info"]["time"] = receipt_time + + except Exception as e: + frappe.logger().error(f"Error extracting receipt info: {str(e)}") def extract_items(soup, receipt_data): - """Extract purchased items from the receipt HTML""" - try: - # Find the items table - items_table = soup.find("table", class_="table-striped") - if items_table: - tbody = items_table.find("tbody") - if tbody: - rows = tbody.find_all("tr") - for row in rows: - cells = row.find_all("td") - if len(cells) >= 3: - item = { - "description": cells[0].get_text(strip=True), - "quantity": cells[1].get_text(strip=True), - "amount": cells[2].get_text(strip=True), - } - if item["description"]: # Only add if has description - receipt_data["items"].append(item) - - except Exception as e: - frappe.logger().error(f"Error extracting items: {str(e)}") + """Extract purchased items from the receipt HTML""" + try: + # Find the items table + items_table = soup.find("table", class_="table-striped") + if items_table: + tbody = items_table.find("tbody") + if tbody: + rows = tbody.find_all("tr") + for row in rows: + cells = row.find_all("td") + if len(cells) >= 3: + item = { + "description": cells[0].get_text(strip=True), + "quantity": cells[1].get_text(strip=True), + "amount": cells[2].get_text(strip=True), + } + if item["description"]: # Only add if has description + receipt_data["items"].append(item) + + except Exception as e: + frappe.logger().error(f"Error extracting items: {str(e)}") def extract_totals_and_taxes(soup, receipt_data): - """Extract totals and tax information from the receipt HTML""" - try: - # Find the totals table (the one without table-striped class) - tables = soup.find_all("table", class_="table") - - for table in tables: - if "table-striped" not in table.get("class", []): - tbody = table.find("tbody") - if tbody: - rows = tbody.find_all("tr") - for row in rows: - cells = row.find_all(["th", "td"]) - if len(cells) >= 2: - label = cells[0].get_text(strip=True).upper() - value = cells[1].get_text(strip=True) - - # Map the totals - if "TOTAL EXCL OF TAX" in label: - receipt_data["totals"]["subtotal"] = value - elif "TOTAL INCL OF TAX" in label: - receipt_data["totals"]["grand_total"] = value - elif "TOTAL TAX" in label: - receipt_data["totals"]["total_tax"] = value - elif "TAX RATE" in label: - # Extract tax rate and amount - tax_info = { - "label": label, - "amount": value, - } - # Extract rate percentage if available - if "(" in label and "%" in label: - rate_part = label.split("(")[1].split(")")[0] - tax_info["rate"] = rate_part - receipt_data["taxes"].append(tax_info) - - except Exception as e: - frappe.logger().error(f"Error extracting totals and taxes: {str(e)}") + """Extract totals and tax information from the receipt HTML""" + try: + # Find the totals table (the one without table-striped class) + tables = soup.find_all("table", class_="table") + + for table in tables: + if "table-striped" not in table.get("class", []): + tbody = table.find("tbody") + if tbody: + rows = tbody.find_all("tr") + for row in rows: + cells = row.find_all(["th", "td"]) + if len(cells) >= 2: + label = cells[0].get_text(strip=True).upper() + value = cells[1].get_text(strip=True) + + # Map the totals + if "TOTAL EXCL OF TAX" in label: + receipt_data["totals"]["subtotal"] = value + elif "TOTAL INCL OF TAX" in label: + receipt_data["totals"]["grand_total"] = value + elif "TOTAL TAX" in label: + receipt_data["totals"]["total_tax"] = value + elif "TAX RATE" in label: + # Extract tax rate and amount + tax_info = { + "label": label, + "amount": value, + } + # Extract rate percentage if available + if "(" in label and "%" in label: + rate_part = label.split("(")[1].split(")")[0] + tax_info["rate"] = rate_part + receipt_data["taxes"].append(tax_info) + + except Exception as e: + frappe.logger().error(f"Error extracting totals and taxes: {str(e)}") def extract_verification_info(soup, receipt_data): - """Extract verification information from the receipt HTML""" - try: - # Look for verification code - verification_headers = soup.find_all("h4") - for header in verification_headers: - text = header.get_text(strip=True) - if "RECEIPT VERIFICATION CODE" in text: - # The next h4 should contain the actual code - next_h4 = header.find_next("h4") - if next_h4: - verification_code = next_h4.get_text(strip=True) - receipt_data["verification_info"]["code"] = verification_code - - # Look for QR code image - qr_img = soup.find("img", {"id": "barcode"}) - if qr_img: - qr_src = qr_img.get("src", "") - qr_title = qr_img.get("title", "") - receipt_data["verification_info"]["qr_code_url"] = qr_src - receipt_data["verification_info"]["qr_code_data"] = qr_title - - # Extract verification URL from QR code data parameter - if "data=" in qr_src: - verification_url = qr_src.split("data=")[1].split("&")[0] - receipt_data["verification_info"]["verification_url"] = verification_url - - except Exception as e: - frappe.logger().error(f"Error extracting verification info: {str(e)}") + """Extract verification information from the receipt HTML""" + try: + # Look for verification code + verification_headers = soup.find_all("h4") + for header in verification_headers: + text = header.get_text(strip=True) + if "RECEIPT VERIFICATION CODE" in text: + # The next h4 should contain the actual code + next_h4 = header.find_next("h4") + if next_h4: + verification_code = next_h4.get_text(strip=True) + receipt_data["verification_info"]["code"] = verification_code + + # Look for QR code image + qr_img = soup.find("img", {"id": "barcode"}) + if qr_img: + qr_src = qr_img.get("src", "") + qr_title = qr_img.get("title", "") + receipt_data["verification_info"]["qr_code_url"] = qr_src + receipt_data["verification_info"]["qr_code_data"] = qr_title + + # Extract verification URL from QR code data parameter + if "data=" in qr_src: + verification_url = qr_src.split("data=")[1].split("&")[0] + receipt_data["verification_info"]["verification_url"] = verification_url + + except Exception as e: + frappe.logger().error(f"Error extracting verification info: {str(e)}") def create_tra_tax_inv_document(verification_code, receipt_data, verification_result): - """ - Create a TRA Tax Inv document from successful verification data - - Args: - verification_code (str): The verification code used - receipt_data (dict): Extracted receipt data - verification_result (dict): Full verification result - - Returns: - dict: Result of document creation - """ - try: - # Check if document already exists - existing = frappe.db.exists( - "TRA TAX Inv", {"verification_code": verification_code} - ) - if existing: - frappe.logger().info( - f"TRA TAX Inv already exists for verification code: {verification_code}" - ) - return { - "success": False, - "message": f"TRA TAX Inv already exists for verification code: {verification_code}", - "existing_doc": existing, - } - - # Create new TRA TAX Inv document - doc = frappe.new_doc("TRA TAX Inv") - doc.verification_code = verification_code - doc.type = "Purchase" # Default to Purchase, can be changed later - doc.verification_status = "Verified" - doc.verification_url = str(verification_result.get("url", "")) - - # Populate basic information - company_info = receipt_data.get("company_info", {}) - doc.company_name = company_info.get("name", "") - - receipt_info = receipt_data.get("receipt_info", {}) - doc.receipt_number = receipt_info.get("receipt_number", "") - - # Populate customer information - customer_info = receipt_data.get("customer_info", {}) - doc.customer_name = customer_info.get("name", "") - doc.customer_id_type = customer_info.get("id_type", "") - doc.customer_id = customer_info.get("id", "") - doc.customer_mobile = customer_info.get("mobile", "") - - # Populate totals - totals = receipt_data.get("totals", {}) - if totals.get("subtotal"): - try: - subtotal_str = str(totals.get("subtotal", "0")).replace(",", "") - doc.subtotal = float(subtotal_str) - except: - pass - - if totals.get("total_tax"): - try: - total_tax_str = str(totals.get("total_tax", "0")).replace(",", "") - doc.total_tax = float(total_tax_str) - except: - pass - - if totals.get("grand_total"): - try: - grand_total_str = str(totals.get("grand_total", "0")).replace(",", "") - doc.grand_total = float(grand_total_str) - except: - pass - - # Populate items - items = receipt_data.get("items", []) - for item in items: - item_row = doc.append("items", {}) - item_row.description = item.get("description", "") - item_row.quantity = item.get("quantity", "") - if item.get("amount"): - try: - amount_str = str(item.get("amount", "0")).replace(",", "") - item_row.amount = float(amount_str) - except: - item_row.amount = 0 - - # Save the document - doc.insert() - - return { - "success": True, - "message": "TRA TAX Inv created successfully", - "doc_name": doc.name, - "verification_status": doc.verification_status, - } - - except Exception as e: - return {"success": False, "message": f"Error creating TRA TAX Inv: {str(e)}"} - - -def create_tra_tax_inv_document_safe( - verification_code, receipt_data, verification_result -): - """ - Safely create a TRA TAX Inv document with error handling - - Args: - verification_code (str): The verification code used - receipt_data (dict): Extracted receipt data (may be empty) - verification_result (dict): Full verification result - - Returns: - dict: Result of document creation - """ - try: - # Check if document already exists - existing = frappe.db.exists( - "TRA TAX Inv", {"verification_code": verification_code} - ) - if existing: - frappe.logger().info( - f"TRA TAX Inv already exists for verification code: {verification_code}" - ) - return { - "success": False, - "message": f"Receipt already exists for verification code: {verification_code}", - "existing_doc": existing, - "doc_name": existing, - } - - # Create new TRA TAX Inv document - doc = frappe.new_doc("TRA TAX Inv") - doc.verification_code = verification_code - doc.type = "Sales" # Default to Sales - - # Set verification status based on verification success - if verification_result.get("success"): - doc.verification_status = "Verified" - else: - doc.verification_status = "Failed" - - # Populate basic information if available - try: - company_info = receipt_data.get("company_info", {}) - if company_info.get("name"): - doc.company_name = company_info.get("name", "") - except: - pass - - try: - receipt_info = receipt_data.get("receipt_info", {}) - if receipt_info.get("receipt_number"): - doc.receipt_number = receipt_info.get("receipt_number", "") - except: - pass - - # Populate customer information if available - try: - customer_info = receipt_data.get("customer_info", {}) - if customer_info.get("name"): - doc.customer_name = customer_info.get("name", "") - if customer_info.get("id_type"): - doc.customer_id_type = customer_info.get("id_type", "") - if customer_info.get("id"): - doc.customer_id = customer_info.get("id", "") - if customer_info.get("mobile"): - doc.customer_mobile = customer_info.get("mobile", "") - except: - pass - - # Populate totals if available - try: - totals = receipt_data.get("totals", {}) - if totals.get("subtotal"): - try: - subtotal_str = str(totals.get("subtotal", "0")).replace(",", "") - doc.subtotal = float(subtotal_str) - except: - pass - - if totals.get("total_tax"): - try: - total_tax_str = str(totals.get("total_tax", "0")).replace(",", "") - doc.total_tax = float(total_tax_str) - except: - pass - - if totals.get("grand_total"): - try: - grand_total_str = str(totals.get("grand_total", "0")).replace( - ",", "" - ) - doc.grand_total = float(grand_total_str) - except: - pass - except: - pass - - # Populate items if available - try: - items = receipt_data.get("items", []) - for item in items: - try: - item_row = doc.append("items", {}) - item_row.description = item.get("description", "") - item_row.quantity = item.get("quantity", "") - if item.get("amount"): - try: - amount_str = str(item.get("amount", "0")).replace(",", "") - item_row.amount = float(amount_str) - except: - item_row.amount = 0 - except: - continue - except: - pass - - # Save the document - doc.insert() - - # Prepare success message with details - message_parts = [f"TRA TAX Inv created: {doc.name}"] - if doc.company_name: - message_parts.append(f"Company: {doc.company_name}") - if doc.receipt_number: - message_parts.append(f"Receipt: {doc.receipt_number}") - if doc.grand_total: - message_parts.append(f"Total: {doc.grand_total}") - - return { - "success": True, - "message": " | ".join(message_parts), - "doc_name": doc.name, - "verification_status": doc.verification_status, - "company_name": doc.company_name or "", - "receipt_number": doc.receipt_number or "", - "grand_total": doc.grand_total or 0, - "items_count": len(doc.items) if doc.items else 0, - } - - except Exception as e: - - return { - "success": False, - "message": f"Error creating TRA TAX Inv: {str(e)}", - "verification_code": verification_code, - } + """ + Create a TRA Tax Inv document from successful verification data + + Args: + verification_code (str): The verification code used + receipt_data (dict): Extracted receipt data + verification_result (dict): Full verification result + + Returns: + dict: Result of document creation + """ + try: + # Check if document already exists + existing = frappe.db.exists("TRA TAX Inv", {"verification_code": verification_code}) + if existing: + frappe.logger().info(f"TRA TAX Inv already exists for verification code: {verification_code}") + return { + "success": False, + "message": f"TRA TAX Inv already exists for verification code: {verification_code}", + "existing_doc": existing, + } + + # Create new TRA TAX Inv document + doc = frappe.new_doc("TRA TAX Inv") + doc.verification_code = verification_code + doc.type = "Purchase" # Default to Purchase, can be changed later + doc.verification_status = "Verified" + doc.verification_url = str(verification_result.get("url", "")) + + # Populate basic information + company_info = receipt_data.get("company_info", {}) + doc.company_name = company_info.get("name", "") + + receipt_info = receipt_data.get("receipt_info", {}) + doc.receipt_number = receipt_info.get("receipt_number", "") + + # Populate customer information + customer_info = receipt_data.get("customer_info", {}) + doc.customer_name = customer_info.get("name", "") + doc.customer_id_type = customer_info.get("id_type", "") + doc.customer_id = customer_info.get("id", "") + doc.customer_mobile = customer_info.get("mobile", "") + + # Populate totals + totals = receipt_data.get("totals", {}) + if totals.get("subtotal"): + try: + subtotal_str = str(totals.get("subtotal", "0")).replace(",", "") + doc.subtotal = float(subtotal_str) + except Exception: + pass + + if totals.get("total_tax"): + try: + total_tax_str = str(totals.get("total_tax", "0")).replace(",", "") + doc.total_tax = float(total_tax_str) + except Exception: + pass + + if totals.get("grand_total"): + try: + grand_total_str = str(totals.get("grand_total", "0")).replace(",", "") + doc.grand_total = float(grand_total_str) + except Exception: + pass + + # Populate items + items = receipt_data.get("items", []) + for item in items: + item_row = doc.append("items", {}) + item_row.description = item.get("description", "") + item_row.quantity = item.get("quantity", "") + if item.get("amount"): + try: + amount_str = str(item.get("amount", "0")).replace(",", "") + item_row.amount = float(amount_str) + except Exception: + item_row.amount = 0 + + # Save the document + doc.insert() + + return { + "success": True, + "message": "TRA TAX Inv created successfully", + "doc_name": doc.name, + "verification_status": doc.verification_status, + } + + except Exception as e: + return {"success": False, "message": f"Error creating TRA TAX Inv: {str(e)}"} + + +def create_tra_tax_inv_document_safe(verification_code, receipt_data, verification_result): + """ + Safely create a TRA TAX Inv document with error handling + + Args: + verification_code (str): The verification code used + receipt_data (dict): Extracted receipt data (may be empty) + verification_result (dict): Full verification result + + Returns: + dict: Result of document creation + """ + try: + # Check if document already exists + existing = frappe.db.exists("TRA TAX Inv", {"verification_code": verification_code}) + if existing: + frappe.logger().info(f"TRA TAX Inv already exists for verification code: {verification_code}") + return { + "success": False, + "message": f"Receipt already exists for verification code: {verification_code}", + "existing_doc": existing, + "doc_name": existing, + } + + # Create new TRA TAX Inv document + doc = frappe.new_doc("TRA TAX Inv") + doc.verification_code = verification_code + doc.type = "Sales" # Default to Sales + + # Set verification status based on verification success + if verification_result.get("success"): + doc.verification_status = "Verified" + else: + doc.verification_status = "Failed" + + # Populate basic information if available + try: + company_info = receipt_data.get("company_info", {}) + if company_info.get("name"): + doc.company_name = company_info.get("name", "") + except Exception: + pass + + try: + receipt_info = receipt_data.get("receipt_info", {}) + if receipt_info.get("receipt_number"): + doc.receipt_number = receipt_info.get("receipt_number", "") + except Exception: + pass + + # Populate customer information if available + try: + customer_info = receipt_data.get("customer_info", {}) + if customer_info.get("name"): + doc.customer_name = customer_info.get("name", "") + if customer_info.get("id_type"): + doc.customer_id_type = customer_info.get("id_type", "") + if customer_info.get("id"): + doc.customer_id = customer_info.get("id", "") + if customer_info.get("mobile"): + doc.customer_mobile = customer_info.get("mobile", "") + except Exception: + pass + + # Populate totals if available + try: + totals = receipt_data.get("totals", {}) + if totals.get("subtotal"): + try: + subtotal_str = str(totals.get("subtotal", "0")).replace(",", "") + doc.subtotal = float(subtotal_str) + except Exception: + pass + + if totals.get("total_tax"): + try: + total_tax_str = str(totals.get("total_tax", "0")).replace(",", "") + doc.total_tax = float(total_tax_str) + except Exception: + pass + + if totals.get("grand_total"): + try: + grand_total_str = str(totals.get("grand_total", "0")).replace(",", "") + doc.grand_total = float(grand_total_str) + except Exception: + pass + except Exception: + pass + + # Populate items if available + try: + items = receipt_data.get("items", []) + for item in items: + try: + item_row = doc.append("items", {}) + item_row.description = item.get("description", "") + item_row.quantity = item.get("quantity", "") + if item.get("amount"): + try: + amount_str = str(item.get("amount", "0")).replace(",", "") + item_row.amount = float(amount_str) + except Exception: + item_row.amount = 0 + except Exception: + continue + except Exception: + pass + + # Save the document + doc.insert() + + # Prepare success message with details + message_parts = [f"TRA TAX Inv created: {doc.name}"] + if doc.company_name: + message_parts.append(f"Company: {doc.company_name}") + if doc.receipt_number: + message_parts.append(f"Receipt: {doc.receipt_number}") + if doc.grand_total: + message_parts.append(f"Total: {doc.grand_total}") + + return { + "success": True, + "message": " | ".join(message_parts), + "doc_name": doc.name, + "verification_status": doc.verification_status, + "company_name": doc.company_name or "", + "receipt_number": doc.receipt_number or "", + "grand_total": doc.grand_total or 0, + "items_count": len(doc.items) if doc.items else 0, + } + + except Exception as e: + return { + "success": False, + "message": f"Error creating TRA TAX Inv: {str(e)}", + "verification_code": verification_code, + } @frappe.whitelist() def create_invoice_from_tra_tax_inv(tra_tax_inv_name, invoice_type): - """ - Create Purchase Invoice or Sales Invoice from TRA Tax Inv - - Args: - tra_tax_inv_name (str): Name of the TRA Tax Inv document - invoice_type (str): Either "Purchase Invoice" or "Sales Invoice" - - Returns: - dict: Result of invoice creation with success status and details - """ - try: - # Get the TRA Tax Inv document - tra_doc = frappe.get_doc("TRA TAX Inv", tra_tax_inv_name) - - # Check if invoice already exists - if tra_doc.reference_docname and tra_doc.reference_doctype: - return { - "success": False, - "message": f"Invoice already created: {tra_doc.reference_doctype} - {tra_doc.reference_docname}", - } - - # Validate required data - validation_result = validate_tra_tax_inv_for_invoice(tra_doc, invoice_type) - if not validation_result["success"]: - return validation_result - - # Create the invoice - if invoice_type == "Purchase Invoice": - invoice_doc = create_purchase_invoice_from_tra(tra_doc) - elif invoice_type == "Sales Invoice": - invoice_doc = create_sales_invoice_from_tra(tra_doc) - else: - return { - "success": False, - "message": f"Invalid invoice type: {invoice_type}. Must be 'Purchase Invoice' or 'Sales Invoice'", - } - - # Update TRA Tax Inv with reference - tra_doc.reference_doctype = invoice_type - tra_doc.reference_docname = invoice_doc.name - tra_doc.save() - - return { - "success": True, - "message": f"{invoice_type} created successfully", - "invoice_name": invoice_doc.name, - "invoice_type": invoice_type, - } - - except Exception as e: - frappe.logger().error(f"Error creating invoice from TRA Tax Inv: {str(e)}") - return {"success": False, "message": f"Error creating invoice: {str(e)}"} + """ + Create Purchase Invoice or Sales Invoice from TRA Tax Inv + + Args: + tra_tax_inv_name (str): Name of the TRA Tax Inv document + invoice_type (str): Either "Purchase Invoice" or "Sales Invoice" + + Returns: + dict: Result of invoice creation with success status and details + """ + try: + # Get the TRA Tax Inv document + tra_doc = frappe.get_doc("TRA TAX Inv", tra_tax_inv_name) + + # Check if invoice already exists + if tra_doc.reference_docname and tra_doc.reference_doctype: + return { + "success": False, + "message": f"Invoice already created: {tra_doc.reference_doctype} - {tra_doc.reference_docname}", + } + + # Validate required data + validation_result = validate_tra_tax_inv_for_invoice(tra_doc, invoice_type) + if not validation_result["success"]: + return validation_result + + # Create the invoice + if invoice_type == "Purchase Invoice": + invoice_doc = create_purchase_invoice_from_tra(tra_doc) + elif invoice_type == "Sales Invoice": + invoice_doc = create_sales_invoice_from_tra(tra_doc) + else: + return { + "success": False, + "message": f"Invalid invoice type: {invoice_type}. Must be 'Purchase Invoice' or 'Sales Invoice'", + } + + # Update TRA Tax Inv with reference + tra_doc.reference_doctype = invoice_type + tra_doc.reference_docname = invoice_doc.name + tra_doc.save() + + return { + "success": True, + "message": f"{invoice_type} created successfully", + "invoice_name": invoice_doc.name, + "invoice_type": invoice_type, + } + + except Exception as e: + frappe.logger().error(f"Error creating invoice from TRA Tax Inv: {str(e)}") + return {"success": False, "message": f"Error creating invoice: {str(e)}"} def validate_tra_tax_inv_for_invoice(tra_doc, invoice_type): - """ - Validate TRA Tax Inv data before creating invoice - - Args: - tra_doc: TRA Tax Inv document - invoice_type (str): Either "Purchase Invoice" or "Sales Invoice" - - Returns: - dict: Validation result with success status and details - """ - try: - missing_items = [] - missing_party = None - - # Check if we have items - if not tra_doc.items or len(tra_doc.items) == 0: - return { - "success": False, - "message": "No items found in TRA Tax Inv. Cannot create invoice without items.", - } - - # Validate items exist in Item master - for item in tra_doc.items: - if not item.description: - continue - - # Check if mapped_item_code is provided and valid - if hasattr(item, "mapped_item_code") and item.mapped_item_code: - if not frappe.db.exists("Item", item.mapped_item_code): - missing_items.append( - f"{item.description} (mapped to: {item.mapped_item_code})" - ) - continue - - # Fallback: Try to find item by description - item_exists = frappe.db.exists("Item", {"item_name": item.description}) - if not item_exists: - # Also try exact match on item_code - item_exists = frappe.db.exists("Item", item.description) - - if not item_exists: - missing_items.append(item.description) - - # Validate party (Customer/Supplier) exists - if invoice_type == "Purchase Invoice": - # For Purchase Invoice, we auto-create suppliers, so just check if customer_name exists - if not tra_doc.customer_name: - missing_party = "Supplier: No customer name found in TRA Tax Inv" - # Note: We don't validate supplier existence since we auto-create them - - elif invoice_type == "Sales Invoice": - # For Sales Invoice, we auto-create customers, so just check if company_name exists - if not tra_doc.company_name: - missing_party = "Customer: No company name found in TRA Tax Inv" - # Note: We don't validate customer existence since we auto-create them - - # Prepare validation result - if missing_items or missing_party: - error_messages = [] - - if missing_party: - error_messages.append(f"Missing {missing_party}") - - if missing_items: - error_messages.append(f"Missing Items: {', '.join(missing_items[:5])}") - if len(missing_items) > 5: - error_messages.append( - f"... and {len(missing_items) - 5} more items" - ) - - return { - "success": False, - "message": "Please create the following master records first:\n" - + "\n".join(error_messages), - "missing_items": missing_items, - "missing_party": missing_party, - } - - return {"success": True, "message": "Validation passed"} - - except Exception as e: - frappe.logger().error(f"Error validating TRA Tax Inv: {str(e)}") - return {"success": False, "message": f"Validation error: {str(e)}"} + """ + Validate TRA Tax Inv data before creating invoice + + Args: + tra_doc: TRA Tax Inv document + invoice_type (str): Either "Purchase Invoice" or "Sales Invoice" + + Returns: + dict: Validation result with success status and details + """ + try: + missing_items = [] + missing_party = None + + # Check if we have items + if not tra_doc.items or len(tra_doc.items) == 0: + return { + "success": False, + "message": "No items found in TRA Tax Inv. Cannot create invoice without items.", + } + + # Validate items exist in Item master + for item in tra_doc.items: + if not item.description: + continue + + # Check if mapped_item_code is provided and valid + if hasattr(item, "mapped_item_code") and item.mapped_item_code: + if not frappe.db.exists("Item", item.mapped_item_code): + missing_items.append(f"{item.description} (mapped to: {item.mapped_item_code})") + continue + + # Fallback: Try to find item by description + item_exists = frappe.db.exists("Item", {"item_name": item.description}) + if not item_exists: + # Also try exact match on item_code + item_exists = frappe.db.exists("Item", item.description) + + if not item_exists: + missing_items.append(item.description) + + # Validate party (Customer/Supplier) exists + if invoice_type == "Purchase Invoice": + # For Purchase Invoice, we auto-create suppliers, so just check if customer_name exists + if not tra_doc.customer_name: + missing_party = "Supplier: No customer name found in TRA Tax Inv" + # Note: We don't validate supplier existence since we auto-create them + + elif invoice_type == "Sales Invoice": + # For Sales Invoice, we auto-create customers, so just check if company_name exists + if not tra_doc.company_name: + missing_party = "Customer: No company name found in TRA Tax Inv" + # Note: We don't validate customer existence since we auto-create them + + # Prepare validation result + if missing_items or missing_party: + error_messages = [] + + if missing_party: + error_messages.append(f"Missing {missing_party}") + + if missing_items: + error_messages.append(f"Missing Items: {', '.join(missing_items[:5])}") + if len(missing_items) > 5: + error_messages.append(f"... and {len(missing_items) - 5} more items") + + return { + "success": False, + "message": "Please create the following master records first:\n" + "\n".join(error_messages), + "missing_items": missing_items, + "missing_party": missing_party, + } + + return {"success": True, "message": "Validation passed"} + + except Exception as e: + frappe.logger().error(f"Error validating TRA Tax Inv: {str(e)}") + return {"success": False, "message": f"Validation error: {str(e)}"} def create_purchase_invoice_from_tra(tra_doc): - """ - Create Purchase Invoice from TRA Tax Inv - - Args: - tra_doc: TRA Tax Inv document - - Returns: - Purchase Invoice document - """ - # Create new Purchase Invoice - pi_doc = frappe.new_doc("Purchase Invoice") - - # Set basic information - # For Purchase Invoice, customer_name from TRA receipt is our supplier - pi_doc.supplier = get_or_create_supplier(tra_doc.customer_name) - pi_doc.posting_date = frappe.utils.today() - pi_doc.due_date = frappe.utils.today() - - # Set reference information - pi_doc.bill_no = tra_doc.receipt_number or tra_doc.verification_code - pi_doc.bill_date = frappe.utils.today() - - # Add items - for tra_item in tra_doc.items: - if not tra_item.description: - continue - - item_code = get_or_suggest_item(tra_item) - if item_code: - pi_item = pi_doc.append("items", {}) - pi_item.item_code = item_code - pi_item.item_name = tra_item.description - # Use TRA Tax Inv description as item description in the invoice - pi_item.description = tra_item.description - try: - pi_item.qty = float(tra_item.quantity) if tra_item.quantity else 1 - except (ValueError, TypeError): - pi_item.qty = 1 - pi_item.rate = float(tra_item.amount) if tra_item.amount else 0 - pi_item.amount = pi_item.qty * pi_item.rate - - # Set totals if available - if tra_doc.grand_total: - pi_doc.total = tra_doc.grand_total - pi_doc.grand_total = tra_doc.grand_total - - # Save and submit - pi_doc.insert() - - return pi_doc + """ + Create Purchase Invoice from TRA Tax Inv + + Args: + tra_doc: TRA Tax Inv document + + Returns: + Purchase Invoice document + """ + # Create new Purchase Invoice + pi_doc = frappe.new_doc("Purchase Invoice") + + # Set basic information + # For Purchase Invoice, customer_name from TRA receipt is our supplier + pi_doc.supplier = get_or_create_supplier(tra_doc.customer_name) + pi_doc.posting_date = frappe.utils.today() + pi_doc.due_date = frappe.utils.today() + + # Set reference information + pi_doc.bill_no = tra_doc.receipt_number or tra_doc.verification_code + pi_doc.bill_date = frappe.utils.today() + + # Add items + for tra_item in tra_doc.items: + if not tra_item.description: + continue + + item_code = get_or_suggest_item(tra_item) + if item_code: + pi_item = pi_doc.append("items", {}) + pi_item.item_code = item_code + pi_item.item_name = tra_item.description + # Use TRA Tax Inv description as item description in the invoice + pi_item.description = tra_item.description + try: + pi_item.qty = float(tra_item.quantity) if tra_item.quantity else 1 + except (ValueError, TypeError): + pi_item.qty = 1 + pi_item.rate = float(tra_item.amount) if tra_item.amount else 0 + pi_item.amount = pi_item.qty * pi_item.rate + + # Set totals if available + if tra_doc.grand_total: + pi_doc.total = tra_doc.grand_total + pi_doc.grand_total = tra_doc.grand_total + + # Save and submit + pi_doc.insert() + + return pi_doc def create_sales_invoice_from_tra(tra_doc): - """ - Create Sales Invoice from TRA Tax Inv - - Args: - tra_doc: TRA Tax Inv document - - Returns: - Sales Invoice document - """ - # Create new Sales Invoice - si_doc = frappe.new_doc("Sales Invoice") - - # Set basic information - # For Sales Invoice, company_name from TRA receipt is our customer - si_doc.customer = get_or_create_customer(tra_doc.company_name) - si_doc.posting_date = frappe.utils.today() - si_doc.due_date = frappe.utils.today() - - # Add items - for tra_item in tra_doc.items: - if not tra_item.description: - continue - - item_code = get_or_suggest_item(tra_item) - if item_code: - si_item = si_doc.append("items", {}) - si_item.item_code = item_code - si_item.item_name = tra_item.description - # Use TRA Tax Inv description as item description in the invoice - si_item.description = tra_item.description - try: - si_item.qty = float(tra_item.quantity) if tra_item.quantity else 1 - except (ValueError, TypeError): - si_item.qty = 1 - si_item.rate = float(tra_item.amount) if tra_item.amount else 0 - si_item.amount = si_item.qty * si_item.rate - - # Set totals if available - if tra_doc.grand_total: - si_doc.total = tra_doc.grand_total - si_doc.grand_total = tra_doc.grand_total - - # Save and submit - si_doc.insert() - - return si_doc + """ + Create Sales Invoice from TRA Tax Inv + + Args: + tra_doc: TRA Tax Inv document + + Returns: + Sales Invoice document + """ + # Create new Sales Invoice + si_doc = frappe.new_doc("Sales Invoice") + + # Set basic information + # For Sales Invoice, company_name from TRA receipt is our customer + si_doc.customer = get_or_create_customer(tra_doc.company_name) + si_doc.posting_date = frappe.utils.today() + si_doc.due_date = frappe.utils.today() + + # Add items + for tra_item in tra_doc.items: + if not tra_item.description: + continue + + item_code = get_or_suggest_item(tra_item) + if item_code: + si_item = si_doc.append("items", {}) + si_item.item_code = item_code + si_item.item_name = tra_item.description + # Use TRA Tax Inv description as item description in the invoice + si_item.description = tra_item.description + try: + si_item.qty = float(tra_item.quantity) if tra_item.quantity else 1 + except (ValueError, TypeError): + si_item.qty = 1 + si_item.rate = float(tra_item.amount) if tra_item.amount else 0 + si_item.amount = si_item.qty * si_item.rate + + # Set totals if available + if tra_doc.grand_total: + si_doc.total = tra_doc.grand_total + si_doc.grand_total = tra_doc.grand_total + + # Save and submit + si_doc.insert() + + return si_doc def get_or_suggest_item(tra_item): - """ - Get existing item code based on mapped_item_code or description - - Args: - tra_item: TRA Tax Inv Item object with mapped_item_code and description - - Returns: - str: Item code if found, otherwise the description itself - """ - # First priority: Use mapped_item_code if provided - if hasattr(tra_item, "mapped_item_code") and tra_item.mapped_item_code: - if frappe.db.exists("Item", tra_item.mapped_item_code): - return tra_item.mapped_item_code - else: - # Log warning if mapped item doesn't exist - frappe.logger().warning( - f"Mapped item code '{tra_item.mapped_item_code}' not found for item '{tra_item.description}'" - ) - - # Fallback: Auto-match based on description - if not tra_item.description: - return None - - # Try to find existing item by name - item_code = frappe.db.get_value("Item", {"item_name": tra_item.description}, "name") - if item_code: - return item_code - - # Try exact match on item code - if frappe.db.exists("Item", tra_item.description): - return tra_item.description - - # If not found, return the description as item code (validation will catch this) - return tra_item.description + """ + Get existing item code based on mapped_item_code or description + + Args: + tra_item: TRA Tax Inv Item object with mapped_item_code and description + + Returns: + str: Item code if found, otherwise the description itself + """ + # First priority: Use mapped_item_code if provided + if hasattr(tra_item, "mapped_item_code") and tra_item.mapped_item_code: + if frappe.db.exists("Item", tra_item.mapped_item_code): + return tra_item.mapped_item_code + else: + # Log warning if mapped item doesn't exist + frappe.logger().warning( + f"Mapped item code '{tra_item.mapped_item_code}' not found for item '{tra_item.description}'" + ) + + # Fallback: Auto-match based on description + if not tra_item.description: + return None + + # Try to find existing item by name + item_code = frappe.db.get_value("Item", {"item_name": tra_item.description}, "name") + if item_code: + return item_code + + # Try exact match on item code + if frappe.db.exists("Item", tra_item.description): + return tra_item.description + + # If not found, return the description as item code (validation will catch this) + return tra_item.description def get_or_create_supplier(supplier_name): - """ - Get existing supplier or create new supplier if not found - - Args: - supplier_name (str): Supplier name from TRA Tax Inv - - Returns: - str: Supplier code (existing or newly created) - """ - if not supplier_name: - return None - - # Try to find existing supplier by name - supplier_code = frappe.db.get_value( - "Supplier", {"supplier_name": supplier_name}, "name" - ) - if supplier_code: - return supplier_code - - # Try exact match on supplier code - if frappe.db.exists("Supplier", supplier_name): - return supplier_name - - # If not found, create new supplier - try: - supplier_doc = frappe.new_doc("Supplier") - supplier_doc.supplier_name = supplier_name - supplier_doc.supplier_group = ( - frappe.db.get_single_value("Buying Settings", "supplier_group") - or "All Supplier Groups" - ) - supplier_doc.supplier_type = "Company" - supplier_doc.insert() - - frappe.logger().info(f"Auto-created supplier: {supplier_name}") - return supplier_doc.name - - except Exception as e: - frappe.logger().error(f"Failed to create supplier '{supplier_name}': {str(e)}") - # Return the name anyway, let validation handle the error - return supplier_name + """ + Get existing supplier or create new supplier if not found + + Args: + supplier_name (str): Supplier name from TRA Tax Inv + + Returns: + str: Supplier code (existing or newly created) + """ + if not supplier_name: + return None + + # Try to find existing supplier by name + supplier_code = frappe.db.get_value("Supplier", {"supplier_name": supplier_name}, "name") + if supplier_code: + return supplier_code + + # Try exact match on supplier code + if frappe.db.exists("Supplier", supplier_name): + return supplier_name + + # If not found, create new supplier + try: + supplier_doc = frappe.new_doc("Supplier") + supplier_doc.supplier_name = supplier_name + supplier_doc.supplier_group = ( + frappe.db.get_single_value("Buying Settings", "supplier_group") or "All Supplier Groups" + ) + supplier_doc.supplier_type = "Company" + supplier_doc.insert() + + frappe.logger().info(f"Auto-created supplier: {supplier_name}") + return supplier_doc.name + + except Exception as e: + frappe.logger().error(f"Failed to create supplier '{supplier_name}': {str(e)}") + # Return the name anyway, let validation handle the error + return supplier_name def get_or_suggest_supplier(supplier_name): - """ - Get existing supplier or suggest supplier code based on name (legacy function) + """ + Get existing supplier or suggest supplier code based on name (legacy function) - Args: - supplier_name (str): Supplier name from TRA Tax Inv + Args: + supplier_name (str): Supplier name from TRA Tax Inv - Returns: - str: Supplier code if found, otherwise the name itself - """ - if not supplier_name: - return None + Returns: + str: Supplier code if found, otherwise the name itself + """ + if not supplier_name: + return None - # Try to find existing supplier by name - supplier_code = frappe.db.get_value( - "Supplier", {"supplier_name": supplier_name}, "name" - ) - if supplier_code: - return supplier_code + # Try to find existing supplier by name + supplier_code = frappe.db.get_value("Supplier", {"supplier_name": supplier_name}, "name") + if supplier_code: + return supplier_code - # Try exact match on supplier code - if frappe.db.exists("Supplier", supplier_name): - return supplier_name + # Try exact match on supplier code + if frappe.db.exists("Supplier", supplier_name): + return supplier_name - # If not found, return the name as supplier code (validation will catch this) - return supplier_name + # If not found, return the name as supplier code (validation will catch this) + return supplier_name def get_or_create_customer(customer_name): - """ - Get existing customer or create new customer if not found - - Args: - customer_name (str): Customer name from TRA Tax Inv - - Returns: - str: Customer code (existing or newly created) - """ - if not customer_name: - return None - - # Try to find existing customer by name - customer_code = frappe.db.get_value( - "Customer", {"customer_name": customer_name}, "name" - ) - if customer_code: - return customer_code - - # Try exact match on customer code - if frappe.db.exists("Customer", customer_name): - return customer_name - - # If not found, create new customer - try: - customer_doc = frappe.new_doc("Customer") - customer_doc.customer_name = customer_name - customer_doc.customer_group = ( - frappe.db.get_single_value("Selling Settings", "customer_group") - or "All Customer Groups" - ) - customer_doc.customer_type = "Company" - customer_doc.insert() - - frappe.logger().info(f"Auto-created customer: {customer_name}") - return customer_doc.name - - except Exception as e: - frappe.logger().error(f"Failed to create customer '{customer_name}': {str(e)}") - # Return the name anyway, let validation handle the error - return customer_name + """ + Get existing customer or create new customer if not found + + Args: + customer_name (str): Customer name from TRA Tax Inv + + Returns: + str: Customer code (existing or newly created) + """ + if not customer_name: + return None + + # Try to find existing customer by name + customer_code = frappe.db.get_value("Customer", {"customer_name": customer_name}, "name") + if customer_code: + return customer_code + + # Try exact match on customer code + if frappe.db.exists("Customer", customer_name): + return customer_name + + # If not found, create new customer + try: + customer_doc = frappe.new_doc("Customer") + customer_doc.customer_name = customer_name + customer_doc.customer_group = ( + frappe.db.get_single_value("Selling Settings", "customer_group") or "All Customer Groups" + ) + customer_doc.customer_type = "Company" + customer_doc.insert() + + frappe.logger().info(f"Auto-created customer: {customer_name}") + return customer_doc.name + + except Exception as e: + frappe.logger().error(f"Failed to create customer '{customer_name}': {str(e)}") + # Return the name anyway, let validation handle the error + return customer_name def get_or_suggest_customer(customer_name): - """ - Get existing customer or suggest customer code based on name (legacy function) - - Args: - customer_name (str): Customer name from TRA Tax Inv - - Returns: - str: Customer code if found, otherwise the name itself - """ - if not customer_name: - return None - - # Try to find existing customer by name - customer_code = frappe.db.get_value( - "Customer", {"customer_name": customer_name}, "name" - ) - if customer_code: - return customer_code - - # Try exact match on customer code - if frappe.db.exists("Customer", customer_name): - return customer_name - - # If not found, return the name as customer code (validation will catch this) - return customer_name + """ + Get existing customer or suggest customer code based on name (legacy function) + + Args: + customer_name (str): Customer name from TRA Tax Inv + + Returns: + str: Customer code if found, otherwise the name itself + """ + if not customer_name: + return None + + # Try to find existing customer by name + customer_code = frappe.db.get_value("Customer", {"customer_name": customer_name}, "name") + if customer_code: + return customer_code + + # Try exact match on customer code + if frappe.db.exists("Customer", customer_name): + return customer_name + + # If not found, return the name as customer code (validation will catch this) + return customer_name diff --git a/csf_tz/csf_tz/doctype/tra_tax_inv_item/tra_tax_inv_item.py b/csf_tz/csf_tz/doctype/tra_tax_inv_item/tra_tax_inv_item.py index 993ad2ca..15d977a4 100644 --- a/csf_tz/csf_tz/doctype/tra_tax_inv_item/tra_tax_inv_item.py +++ b/csf_tz/csf_tz/doctype/tra_tax_inv_item/tra_tax_inv_item.py @@ -3,5 +3,6 @@ from frappe.model.document import Document + class TRATAXInvItem(Document): - pass + pass diff --git a/csf_tz/csf_tz/doctype/tz_district/test_tz_district.py b/csf_tz/csf_tz/doctype/tz_district/test_tz_district.py index 9f30068d..53b6dafb 100644 --- a/csf_tz/csf_tz/doctype/tz_district/test_tz_district.py +++ b/csf_tz/csf_tz/doctype/tz_district/test_tz_district.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestTZDistrict(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/tz_district/tz_district.py b/csf_tz/csf_tz/doctype/tz_district/tz_district.py index f69f3b29..f2d31c5a 100644 --- a/csf_tz/csf_tz/doctype/tz_district/tz_district.py +++ b/csf_tz/csf_tz/doctype/tz_district/tz_district.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TZDistrict(Document): pass diff --git a/csf_tz/csf_tz/doctype/tz_insurance_company_detail/tz_insurance_company_detail.py b/csf_tz/csf_tz/doctype/tz_insurance_company_detail/tz_insurance_company_detail.py index 1edb3767..9709b0c1 100644 --- a/csf_tz/csf_tz/doctype/tz_insurance_company_detail/tz_insurance_company_detail.py +++ b/csf_tz/csf_tz/doctype/tz_insurance_company_detail/tz_insurance_company_detail.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TZInsuranceCompanyDetail(Document): pass diff --git a/csf_tz/csf_tz/doctype/tz_insurance_cover_note/test_tz_insurance_cover_note.py b/csf_tz/csf_tz/doctype/tz_insurance_cover_note/test_tz_insurance_cover_note.py index 8c7f40e3..0470531e 100644 --- a/csf_tz/csf_tz/doctype/tz_insurance_cover_note/test_tz_insurance_cover_note.py +++ b/csf_tz/csf_tz/doctype/tz_insurance_cover_note/test_tz_insurance_cover_note.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestTZInsuranceCoverNote(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/tz_insurance_cover_note/tz_insurance_cover_note.py b/csf_tz/csf_tz/doctype/tz_insurance_cover_note/tz_insurance_cover_note.py index 64837099..282ca656 100644 --- a/csf_tz/csf_tz/doctype/tz_insurance_cover_note/tz_insurance_cover_note.py +++ b/csf_tz/csf_tz/doctype/tz_insurance_cover_note/tz_insurance_cover_note.py @@ -1,23 +1,26 @@ # Copyright (c) 2022, Aakvatech and contributors # For license information, please see license.txt -import frappe -import requests import json from datetime import datetime -from frappe.utils import cint -from frappe.utils import getdate, now_datetime, nowdate from time import sleep + +import frappe +import requests from frappe.model.document import Document -from csf_tz.vehicle_authority import get_vehicle_like_records, send_authority_notification +from frappe.utils import cint, getdate, now_datetime, nowdate + from csf_tz.csf_tz.doctype.vehicle_fine_record.vehicle_fine_record import ( - normalize_number_plate, is_valid_number_plate, + normalize_number_plate, ) +from csf_tz.vehicle_authority import get_vehicle_like_records, send_authority_notification + class TZInsuranceCoverNote(Document): pass + @frappe.whitelist() def update_covernote_docs(): """Create or Update covernote document after getting necessary details from tira @@ -42,86 +45,111 @@ def update_covernote_docs(): frappe.logger().info(f"[CoverNote] Processed covernote updates for {processed} vehicles") return {"message": f"Processed covernote updates for {processed} vehicles"} + def fetch_and_update_covernote(plate_number): """ Fetch and update covernote for a specific vehicle plate. """ req = get_covernote_details(plate_number) try: - if not req or not req.get('data'): + if not req or not req.get("data"): return - for record in req.get('data'): - if not frappe.db.exists('TZ Insurance Cover Note', record['coverNoteNumber']): - doc = frappe.new_doc('TZ Insurance Cover Note') + for record in req.get("data"): + if not frappe.db.exists("TZ Insurance Cover Note", record["coverNoteNumber"]): + doc = frappe.new_doc("TZ Insurance Cover Note") else: - doc = frappe.get_doc('TZ Insurance Cover Note', record['coverNoteNumber']) - + doc = frappe.get_doc("TZ Insurance Cover Note", record["coverNoteNumber"]) + for key, value in record.items(): - if key.lower() == 'motor': + if key.lower() == "motor": row = {} doc.insurance_motors = [] for motor_child_key, motor_child_value in value.items(): motor_new_value = None - if motor_child_value and motor_child_key.lower() in ['createddate', 'updateddate']: + if motor_child_value and motor_child_key.lower() in ["createddate", "updateddate"]: unix_timestamp_int = cint(motor_child_value) - motor_new_value = datetime.utcfromtimestamp((unix_timestamp_int/1000)).strftime('%Y-%m-%d %H:%M:%S') + motor_new_value = datetime.utcfromtimestamp(unix_timestamp_int / 1000).strftime( + "%Y-%m-%d %H:%M:%S" + ) else: motor_new_value = motor_child_value - + row[motor_child_key.lower()] = motor_new_value - - doc.append('insurance_motors', row) - - if key.lower() == 'company': + + doc.append("insurance_motors", row) + + if key.lower() == "company": row = {} doc.insurance_provider = [] for company_child_key, company_child_value in value.items(): company_new_value = None - if company_child_value and company_child_key.lower() in ['createddate', 'updateddate', 'incorporationdate', 'initialregistrationdate', 'businesscommencementdate']: + if company_child_value and company_child_key.lower() in [ + "createddate", + "updateddate", + "incorporationdate", + "initialregistrationdate", + "businesscommencementdate", + ]: unix_timestamp_int = cint(company_child_value) - company_new_value = datetime.utcfromtimestamp((unix_timestamp_int/1000)).strftime('%Y-%m-%d %H:%M:%S') - - elif company_child_key.lower() == 'shareholders': + company_new_value = datetime.utcfromtimestamp(unix_timestamp_int / 1000).strftime( + "%Y-%m-%d %H:%M:%S" + ) + + elif company_child_key.lower() == "shareholders": company_new_value = json.dumps(company_child_value) - + else: company_new_value = company_child_value - + row[company_child_key.lower()] = company_new_value - - doc.append('insurance_provider', row) - - if key.lower() == 'policyholders': + + doc.append("insurance_provider", row) + + if key.lower() == "policyholders": doc.policy_holders = [] - for i, row in enumerate(value): + for _i, row in enumerate(value): new_row = {} for policy_child_key, policy_child_value in row.items(): policy_new_value = None - if policy_child_value and policy_child_key.lower() in ['createddate', 'updateddate', 'policyholderbirthdate']: + if policy_child_value and policy_child_key.lower() in [ + "createddate", + "updateddate", + "policyholderbirthdate", + ]: unix_timestamp_int = cint(policy_child_value) - policy_new_value = datetime.utcfromtimestamp((unix_timestamp_int/1000)).strftime('%Y-%m-%d %H:%M:%S') - + policy_new_value = datetime.utcfromtimestamp( + unix_timestamp_int / 1000 + ).strftime("%Y-%m-%d %H:%M:%S") + else: policy_new_value = policy_child_value - + new_row[policy_child_key.lower()] = policy_new_value - - doc.append('policy_holders', new_row) - - if key.lower() not in ['covernotestartdate', 'covernoteenddate', 'company', 'motor', 'policyholders']: + + doc.append("policy_holders", new_row) + + if key.lower() not in [ + "covernotestartdate", + "covernoteenddate", + "company", + "motor", + "policyholders", + ]: doc.update({key.lower(): value}) - - if key.lower() in ['covernotestartdate', 'covernoteenddate']: + + if key.lower() in ["covernotestartdate", "covernoteenddate"]: unix_timestamp_int = cint(value) - date_value = datetime.utcfromtimestamp((unix_timestamp_int/1000.0)).strftime('%Y-%m-%d %H:%M:%S') + date_value = datetime.utcfromtimestamp(unix_timestamp_int / 1000.0).strftime( + "%Y-%m-%d %H:%M:%S" + ) doc.update({key.lower(): date_value}) - + doc.vehicle = plate_number doc.save(ignore_permissions=True) - + frappe.db.commit() - + except Exception as e: frappe.log_error(frappe.get_traceback(), str(e)) @@ -132,7 +160,13 @@ def notify_tira_covernote_expiry(): for row in frappe.get_all( "TZ Insurance Cover Note", - fields=["name", "vehicle", "covernotenumber", "covernoteenddate", "authority_last_expiry_notification_key"], + fields=[ + "name", + "vehicle", + "covernotenumber", + "covernoteenddate", + "authority_last_expiry_notification_key", + ], limit_page_length=0, ): if not row.covernoteenddate: @@ -176,6 +210,7 @@ def notify_tira_covernote_expiry(): frappe.db.commit() + def get_covernote_details(regnumber): """Fetch motor insurance details from tira @@ -183,14 +218,8 @@ def get_covernote_details(regnumber): """ url = "https://tiramis.tira.go.tz/covernote/api/public/portal/verify" - payload = json.dumps({ - "paramType": 2, - "searchParam": regnumber - }) - headers = { - 'Accept': 'application/json', - 'Content-Type': 'application/json' - } + payload = json.dumps({"paramType": 2, "searchParam": regnumber}) + headers = {"Accept": "application/json", "Content-Type": "application/json"} max_retries = 3 response = None @@ -207,7 +236,9 @@ def get_covernote_details(regnumber): except (requests.exceptions.Timeout, requests.exceptions.ConnectionError): if attempt < max_retries - 1: continue - frappe.logger().warning(f"[CoverNote] Connection timeout for {regnumber} after {max_retries} retries") + frappe.logger().warning( + f"[CoverNote] Connection timeout for {regnumber} after {max_retries} retries" + ) return None except requests.exceptions.HTTPError: @@ -215,12 +246,14 @@ def get_covernote_details(regnumber): if status in (408, 429) or status >= 500: if attempt < max_retries - 1: continue - frappe.logger().warning(f"[CoverNote] HTTP {status} for {regnumber} after {max_retries} retries") + frappe.logger().warning( + f"[CoverNote] HTTP {status} for {regnumber} after {max_retries} retries" + ) return None else: frappe.log_error( title="Tiramis API Error", - message=f"HTTP {status} for {regnumber}: {response.text[:500] if response is not None else ''}" + message=f"HTTP {status} for {regnumber}: {response.text[:500] if response is not None else ''}", ) return None @@ -240,6 +273,6 @@ def get_covernote_details(regnumber): except Exception: frappe.log_error( title="Tiramis API: Invalid JSON", - message=f"Non-JSON response for {regnumber}: {response.text[:500]}" + message=f"Non-JSON response for {regnumber}: {response.text[:500]}", ) return None diff --git a/csf_tz/csf_tz/doctype/tz_insurance_policy_holder_detail/tz_insurance_policy_holder_detail.py b/csf_tz/csf_tz/doctype/tz_insurance_policy_holder_detail/tz_insurance_policy_holder_detail.py index b6f0d5c8..bff68d4c 100644 --- a/csf_tz/csf_tz/doctype/tz_insurance_policy_holder_detail/tz_insurance_policy_holder_detail.py +++ b/csf_tz/csf_tz/doctype/tz_insurance_policy_holder_detail/tz_insurance_policy_holder_detail.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TZInsurancePolicyHolderDetail(Document): pass diff --git a/csf_tz/csf_tz/doctype/tz_insurance_vehicle_detail/tz_insurance_vehicle_detail.py b/csf_tz/csf_tz/doctype/tz_insurance_vehicle_detail/tz_insurance_vehicle_detail.py index b4d83cbe..495ed3e5 100644 --- a/csf_tz/csf_tz/doctype/tz_insurance_vehicle_detail/tz_insurance_vehicle_detail.py +++ b/csf_tz/csf_tz/doctype/tz_insurance_vehicle_detail/tz_insurance_vehicle_detail.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TZInsuranceVehicleDetail(Document): pass diff --git a/csf_tz/csf_tz/doctype/tz_region/test_tz_region.py b/csf_tz/csf_tz/doctype/tz_region/test_tz_region.py index 141eef1b..753448ed 100644 --- a/csf_tz/csf_tz/doctype/tz_region/test_tz_region.py +++ b/csf_tz/csf_tz/doctype/tz_region/test_tz_region.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestTZRegion(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/tz_region/tz_region.py b/csf_tz/csf_tz/doctype/tz_region/tz_region.py index 9c11a914..e393481c 100644 --- a/csf_tz/csf_tz/doctype/tz_region/tz_region.py +++ b/csf_tz/csf_tz/doctype/tz_region/tz_region.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TZRegion(Document): pass diff --git a/csf_tz/csf_tz/doctype/tz_village/test_tz_village.py b/csf_tz/csf_tz/doctype/tz_village/test_tz_village.py index db2f18c5..25cafc9a 100644 --- a/csf_tz/csf_tz/doctype/tz_village/test_tz_village.py +++ b/csf_tz/csf_tz/doctype/tz_village/test_tz_village.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestTZVillage(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/tz_village/tz_village.py b/csf_tz/csf_tz/doctype/tz_village/tz_village.py index c5f4c454..c7fef1a8 100644 --- a/csf_tz/csf_tz/doctype/tz_village/tz_village.py +++ b/csf_tz/csf_tz/doctype/tz_village/tz_village.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TZVillage(Document): pass diff --git a/csf_tz/csf_tz/doctype/tz_ward/test_tz_ward.py b/csf_tz/csf_tz/doctype/tz_ward/test_tz_ward.py index 5a7f0048..bd85de1f 100644 --- a/csf_tz/csf_tz/doctype/tz_ward/test_tz_ward.py +++ b/csf_tz/csf_tz/doctype/tz_ward/test_tz_ward.py @@ -4,5 +4,6 @@ # import frappe import unittest + class TestTZWard(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/tz_ward/tz_ward.py b/csf_tz/csf_tz/doctype/tz_ward/tz_ward.py index d6d10e56..ac3ade05 100644 --- a/csf_tz/csf_tz/doctype/tz_ward/tz_ward.py +++ b/csf_tz/csf_tz/doctype/tz_ward/tz_ward.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class TZWard(Document): pass diff --git a/csf_tz/csf_tz/doctype/vehicle_fine_record/test_vehicle_fine_record.py b/csf_tz/csf_tz/doctype/vehicle_fine_record/test_vehicle_fine_record.py index cf306b13..3bc8896d 100644 --- a/csf_tz/csf_tz/doctype/vehicle_fine_record/test_vehicle_fine_record.py +++ b/csf_tz/csf_tz/doctype/vehicle_fine_record/test_vehicle_fine_record.py @@ -1,10 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and Contributors # See license.txt -from __future__ import unicode_literals # import frappe import unittest + class TestVehicleFineRecord(unittest.TestCase): pass diff --git a/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py b/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py index 60e3d737..b3781bfd 100644 --- a/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py +++ b/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py @@ -1,402 +1,389 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import base64 -from frappe.model.document import Document -import frappe -from frappe.utils import flt import hashlib import json -import requests -from csf_tz.custom_api import print_out -from csf_tz.vehicle_authority import ( - get_vehicle_docname_by_plate, - get_vehicle_like_records, - is_authority_notification_event_enabled, - send_authority_notification, -) import re from time import sleep -from frappe.utils import now_datetime + +import frappe +import requests from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes +from frappe.model.document import Document +from frappe.utils import flt, now_datetime +from csf_tz.vehicle_authority import ( + get_vehicle_docname_by_plate, + get_vehicle_like_records, + is_authority_notification_event_enabled, + send_authority_notification, +) TPF_SECRET = "irtismutDkjQBbZKEUn8hw7WqKdxld01E6HIY" class VehicleFineRecord(Document): - def validate(self): - """ - Resolve the ERPNext Vehicle document linked to this fine's plate number. - - Searches all known plate-like fields (license_plate, number_plate, etc.) - across the Vehicle doctype. If found, sets vehicle_doc; otherwise clears it. - """ - try: - if self.vehicle: - vehicle_name = get_vehicle_docname_by_plate(self.vehicle) - self.vehicle_doc = vehicle_name or None - except Exception: - frappe.log_error( - title="Error in VehicleFineRecord.validate", - message=frappe.get_traceback(), - ) + def validate(self): + """ + Resolve the ERPNext Vehicle document linked to this fine's plate number. + + Searches all known plate-like fields (license_plate, number_plate, etc.) + across the Vehicle doctype. If found, sets vehicle_doc; otherwise clears it. + """ + try: + if self.vehicle: + vehicle_name = get_vehicle_docname_by_plate(self.vehicle) + self.vehicle_doc = vehicle_name or None + except Exception: + frappe.log_error( + title="Error in VehicleFineRecord.validate", + message=frappe.get_traceback(), + ) def normalize_number_plate(number_plate): - """ - Strip non-alphanumeric characters and uppercase the result. - Returns None if the input is empty or normalises to an empty string. - Truncates to 7 characters (Tanzanian plate length). - """ - if not number_plate: - return None - normalized = re.sub(r"[^A-Za-z0-9]", "", str(number_plate)).upper() - normalized = normalized[:7] if len(normalized) >= 7 else normalized - return normalized or None + """ + Strip non-alphanumeric characters and uppercase the result. + Returns None if the input is empty or normalises to an empty string. + Truncates to 7 characters (Tanzanian plate length). + """ + if not number_plate: + return None + normalized = re.sub(r"[^A-Za-z0-9]", "", str(number_plate)).upper() + normalized = normalized[:7] if len(normalized) >= 7 else normalized + return normalized or None def is_valid_number_plate(number_plate): - """ - Validate the plate follows the Tanzanian format: 1-3 letters, 3 digits, 1-3 letters. - Example: T123ABC, TZ999A - """ - if not number_plate or len(number_plate) != 7: - return False - return bool(re.match(r"^[A-Z]{1,3}[0-9]{3}[A-Z]{1,3}$", number_plate)) + """ + Validate the plate follows the Tanzanian format: 1-3 letters, 3 digits, 1-3 letters. + Example: T123ABC, TZ999A + """ + if not number_plate or len(number_plate) != 7: + return False + return bool(re.match(r"^[A-Z]{1,3}[0-9]{3}[A-Z]{1,3}$", number_plate)) def decode_tpf_response(result): - if not result.get("payload"): - return result - - payload = str(result.get("payload")).strip() - try: - maybe_payload = base64.b64decode(payload, validate=True).decode() - if re.match(r"^[A-Za-z0-9+/]+=*$", maybe_payload.strip()): - payload = maybe_payload.strip() - except Exception: - pass - - key = TPF_SECRET[:32].ljust(32, "\0").encode() - iv = hashlib.sha256(TPF_SECRET.encode()).hexdigest()[:16].encode() - decryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor() - padded = decryptor.update(base64.b64decode(payload)) + decryptor.finalize() - unpadder = padding.PKCS7(128).unpadder() - plain = unpadder.update(padded) + unpadder.finalize() - return json.loads(plain.decode("utf-8")) + if not result.get("payload"): + return result + + payload = str(result.get("payload")).strip() + try: + maybe_payload = base64.b64decode(payload, validate=True).decode() + if re.match(r"^[A-Za-z0-9+/]+=*$", maybe_payload.strip()): + payload = maybe_payload.strip() + except Exception: + pass + + key = TPF_SECRET[:32].ljust(32, "\0").encode() + iv = hashlib.sha256(TPF_SECRET.encode()).hexdigest()[:16].encode() + decryptor = Cipher(algorithms.AES(key), modes.CBC(iv)).decryptor() + padded = decryptor.update(base64.b64decode(payload)) + decryptor.finalize() + unpadder = padding.PKCS7(128).unpadder() + plain = unpadder.update(padded) + unpadder.finalize() + return json.loads(plain.decode("utf-8")) def check_fine_all_vehicles(batch_size=20): - """ - Discover every vehicle-like record across all installed doctypes - (ERPNext Vehicle, Fleet MS Truck, Fleet MS Trailers, and any future - doctype that has a plate-like field), normalise the registration number, - and process each unique, valid plate inline in the scheduler run. - """ - seen_plates = set() - processed = 0 - - for vehicle in get_vehicle_like_records(): - plate = normalize_number_plate(vehicle.plate_number) - if not plate or not is_valid_number_plate(plate): - continue - if plate in seen_plates: - continue - seen_plates.add(plate) - get_fine(number_plate=plate) - processed += 1 - - frappe.logger().info( - f"Processed fine checks for {processed} unique vehicle-like records" - ) - return {"message": f"Processed fine checks for {processed} unique vehicle-like records"} + """ + Discover every vehicle-like record across all installed doctypes + (ERPNext Vehicle, Fleet MS Truck, Fleet MS Trailers, and any future + doctype that has a plate-like field), normalise the registration number, + and process each unique, valid plate inline in the scheduler run. + """ + seen_plates = set() + processed = 0 + + for vehicle in get_vehicle_like_records(): + plate = normalize_number_plate(vehicle.plate_number) + if not plate or not is_valid_number_plate(plate): + continue + if plate in seen_plates: + continue + seen_plates.add(plate) + get_fine(number_plate=plate) + processed += 1 + + frappe.logger().info(f"Processed fine checks for {processed} unique vehicle-like records") + return {"message": f"Processed fine checks for {processed} unique vehicle-like records"} def sync_vehicle_fines(number_plate): - number_plate = normalize_number_plate(number_plate) - - if not number_plate: - return { - "status": "invalid", - "message": "Missing number plate", - "fine_list": [], - } - - if not is_valid_number_plate(number_plate): - return { - "status": "invalid", - "message": f"Skipping invalid plate: {number_plate}", - "fine_list": [], - } - - url = "https://tms.tpf.go.tz/api/OffenceCheck" - headers = { - "Content-Type": "application/json", - "Accept": "*/*", - "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", - "Origin": "https://tms.tpf.go.tz", - "Referer": "https://tms.tpf.go.tz/", - "Connection": "keep-alive", - } - payload = {"vehicle": number_plate} - - max_retries = 3 - response = None - - for attempt in range(max_retries): - try: - if attempt > 0: - sleep(5 * attempt) - - response = requests.post(url, json=payload, headers=headers, timeout=30) - if response.status_code == 429: - return { - "status": "rate_limited", - "message": f"TPF rate limited {number_plate}", - "fine_list": [], - } - response.raise_for_status() - break - - except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as exc: - if attempt < max_retries - 1: - continue - frappe.logger().warning( - f"[VehicleFine] Connection timeout for {number_plate} " - f"after {max_retries} retries" - ) - return { - "status": "retryable_error", - "message": str(exc), - "fine_list": [], - } - - except requests.exceptions.HTTPError: - status = response.status_code if response is not None else 0 - if status in (408,) or status >= 500: - if attempt < max_retries - 1: - continue - frappe.logger().warning( - f"[VehicleFine] HTTP {status} for {number_plate} " - f"after {max_retries} retries" - ) - return { - "status": "retryable_error", - "message": f"HTTP {status}", - "fine_list": [], - } - - frappe.log_error( - title="TPF API Error", - message=( - f"HTTP {status} for {number_plate}: " - f"{response.text[:500] if response is not None else ''}" - ), - ) - return { - "status": "error", - "message": f"HTTP {status}", - "fine_list": [], - } - - except requests.exceptions.RequestException as exc: - frappe.log_error(title="TPF API Error", message=str(exc)) - return { - "status": "error", - "message": str(exc), - "fine_list": [], - } - - if response is None: - return { - "status": "retryable_error", - "message": "No response from TPF", - "fine_list": [], - } - - try: - result = response.json() - result = decode_tpf_response(result) - except Exception: - frappe.log_error( - title="TPF API: Invalid JSON", - message=( - f"Non-JSON response for {number_plate}: " - f"{response.text[:500]}" - ), - ) - return { - "status": "error", - "message": "Invalid JSON response", - "fine_list": [], - } - - data = result.get("pending_transactions", []) - fine_list = [] - - if data: - fine_list = [fine.get("reference") for fine in data if fine.get("reference")] - if not fine_list: - return {"status": "success", "message": "No fine references", "fine_list": fine_list} - - stale_filters = { - "vehicle": number_plate, - "status": ["!=", "PAID"], - "reference": ["not in", fine_list], - } - for record in frappe.get_all( - "Vehicle Fine Record", filters=stale_filters, pluck="name" - ): - old_status = frappe.db.get_value("Vehicle Fine Record", record, "status") - frappe.db.set_value("Vehicle Fine Record", record, "status", "PAID") - _notify_vehicle_fine_status_change(record, number_plate, old_status, "PAID") - - existing_refs = frappe.get_all( - "Vehicle Fine Record", - filters={"vehicle": number_plate, "reference": ["in", fine_list]}, - pluck="reference", - ) - for fine in data: - fine_ref = fine.get("reference") - if not fine_ref or fine_ref in existing_refs: - continue - charge = fine.get("charge") or fine.get("amount") - penalty = fine.get("penalty") - try: - doc = frappe.get_doc( - { - "doctype": "Vehicle Fine Record", - "vehicle": number_plate, - "reference": fine_ref, - "status": fine.get("status") or "PENDING", - "licence": fine.get("licence"), - "location": fine.get("location"), - "officer": fine.get("officer"), - "charge": charge, - "penalty": penalty, - "total": fine.get("total") or (flt(charge) + flt(penalty)), - "offence": fine.get("offence"), - "issued_date": fine.get("issued_date") or fine.get("date"), - } - ) - doc.insert(ignore_permissions=True) - _notify_vehicle_fine_new(doc) - except frappe.exceptions.DuplicateEntryError: - pass - except Exception: - frappe.log_error( - title=f"Error creating fine record for {number_plate}", - message=frappe.get_traceback(), - ) - else: - for record in frappe.get_all( - "Vehicle Fine Record", - filters={"vehicle": number_plate, "status": ["!=", "PAID"]}, - pluck="name", - ): - old_status = frappe.db.get_value("Vehicle Fine Record", record, "status") - frappe.db.set_value("Vehicle Fine Record", record, "status", "PAID") - _notify_vehicle_fine_status_change(record, number_plate, old_status, "PAID") - - frappe.db.commit() - return { - "status": "success", - "message": f"Processed fine sync for {number_plate}", - "fine_list": fine_list, - } + number_plate = normalize_number_plate(number_plate) + + if not number_plate: + return { + "status": "invalid", + "message": "Missing number plate", + "fine_list": [], + } + + if not is_valid_number_plate(number_plate): + return { + "status": "invalid", + "message": f"Skipping invalid plate: {number_plate}", + "fine_list": [], + } + + url = "https://tms.tpf.go.tz/api/OffenceCheck" + headers = { + "Content-Type": "application/json", + "Accept": "*/*", + "User-Agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36", + "Origin": "https://tms.tpf.go.tz", + "Referer": "https://tms.tpf.go.tz/", + "Connection": "keep-alive", + } + payload = {"vehicle": number_plate} + + max_retries = 3 + response = None + + for attempt in range(max_retries): + try: + if attempt > 0: + sleep(5 * attempt) + + response = requests.post(url, json=payload, headers=headers, timeout=30) + if response.status_code == 429: + return { + "status": "rate_limited", + "message": f"TPF rate limited {number_plate}", + "fine_list": [], + } + response.raise_for_status() + break + + except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as exc: + if attempt < max_retries - 1: + continue + frappe.logger().warning( + f"[VehicleFine] Connection timeout for {number_plate} after {max_retries} retries" + ) + return { + "status": "retryable_error", + "message": str(exc), + "fine_list": [], + } + + except requests.exceptions.HTTPError: + status = response.status_code if response is not None else 0 + if status in (408,) or status >= 500: + if attempt < max_retries - 1: + continue + frappe.logger().warning( + f"[VehicleFine] HTTP {status} for {number_plate} after {max_retries} retries" + ) + return { + "status": "retryable_error", + "message": f"HTTP {status}", + "fine_list": [], + } + + frappe.log_error( + title="TPF API Error", + message=( + f"HTTP {status} for {number_plate}: {response.text[:500] if response is not None else ''}" + ), + ) + return { + "status": "error", + "message": f"HTTP {status}", + "fine_list": [], + } + + except requests.exceptions.RequestException as exc: + frappe.log_error(title="TPF API Error", message=str(exc)) + return { + "status": "error", + "message": str(exc), + "fine_list": [], + } + + if response is None: + return { + "status": "retryable_error", + "message": "No response from TPF", + "fine_list": [], + } + + try: + result = response.json() + result = decode_tpf_response(result) + except Exception: + frappe.log_error( + title="TPF API: Invalid JSON", + message=(f"Non-JSON response for {number_plate}: {response.text[:500]}"), + ) + return { + "status": "error", + "message": "Invalid JSON response", + "fine_list": [], + } + + data = result.get("pending_transactions", []) + fine_list = [] + + if data: + fine_list = [fine.get("reference") for fine in data if fine.get("reference")] + if not fine_list: + return {"status": "success", "message": "No fine references", "fine_list": fine_list} + + stale_filters = { + "vehicle": number_plate, + "status": ["!=", "PAID"], + "reference": ["not in", fine_list], + } + for record in frappe.get_all("Vehicle Fine Record", filters=stale_filters, pluck="name"): + old_status = frappe.db.get_value("Vehicle Fine Record", record, "status") + frappe.db.set_value("Vehicle Fine Record", record, "status", "PAID") + _notify_vehicle_fine_status_change(record, number_plate, old_status, "PAID") + + existing_refs = frappe.get_all( + "Vehicle Fine Record", + filters={"vehicle": number_plate, "reference": ["in", fine_list]}, + pluck="reference", + ) + for fine in data: + fine_ref = fine.get("reference") + if not fine_ref or fine_ref in existing_refs: + continue + charge = fine.get("charge") or fine.get("amount") + penalty = fine.get("penalty") + try: + doc = frappe.get_doc( + { + "doctype": "Vehicle Fine Record", + "vehicle": number_plate, + "reference": fine_ref, + "status": fine.get("status") or "PENDING", + "licence": fine.get("licence"), + "location": fine.get("location"), + "officer": fine.get("officer"), + "charge": charge, + "penalty": penalty, + "total": fine.get("total") or (flt(charge) + flt(penalty)), + "offence": fine.get("offence"), + "issued_date": fine.get("issued_date") or fine.get("date"), + } + ) + doc.insert(ignore_permissions=True) + _notify_vehicle_fine_new(doc) + except frappe.exceptions.DuplicateEntryError: + pass + except Exception: + frappe.log_error( + title=f"Error creating fine record for {number_plate}", + message=frappe.get_traceback(), + ) + else: + for record in frappe.get_all( + "Vehicle Fine Record", + filters={"vehicle": number_plate, "status": ["!=", "PAID"]}, + pluck="name", + ): + old_status = frappe.db.get_value("Vehicle Fine Record", record, "status") + frappe.db.set_value("Vehicle Fine Record", record, "status", "PAID") + _notify_vehicle_fine_status_change(record, number_plate, old_status, "PAID") + + frappe.db.commit() + return { + "status": "success", + "message": f"Processed fine sync for {number_plate}", + "fine_list": fine_list, + } @frappe.whitelist() def get_fine(number_plate): - """ - Query the TPF API for pending fines on the given number plate. + """ + Query the TPF API for pending fines on the given number plate. - Behaviour: - - If the API returns pending transactions: - * Create a new Vehicle Fine Record for each reference not yet in ERPNext. - * Mark any existing PENDING records whose reference is no longer in the - API response as PAID (they have been settled). - - If the API returns no pending transactions: - * Mark all PENDING records for this plate as PAID. + Behaviour: + - If the API returns pending transactions: + * Create a new Vehicle Fine Record for each reference not yet in ERPNext. + * Mark any existing PENDING records whose reference is no longer in the + API response as PAID (they have been settled). + - If the API returns no pending transactions: + * Mark all PENDING records for this plate as PAID. - Returns a list of fine reference strings that are currently pending - according to the TPF API, or [] on any error. - """ - result = sync_vehicle_fines(number_plate) - return result.get("fine_list", []) + Returns a list of fine reference strings that are currently pending + according to the TPF API, or [] on any error. + """ + result = sync_vehicle_fines(number_plate) + return result.get("fine_list", []) def _notify_vehicle_fine_new(doc): - if not is_authority_notification_event_enabled("Vehicle Fine", "new"): - return - - subject = f"Vehicle Fine Alert: {doc.vehicle or doc.reference}" - message = ( - f"Vehicle {doc.vehicle or '-'} has a new traffic fine " - f"({doc.reference or '-'}) with status {doc.status or '-'} " - f"and total {doc.total or 0}." - ) - result = send_authority_notification("Vehicle Fine", subject, message) - if result.get("sent"): - frappe.db.set_value( - "Vehicle Fine Record", - doc.name, - { - "authority_notified_on_new": now_datetime(), - "authority_last_notified_status": doc.status or "", - }, - update_modified=False, - ) + if not is_authority_notification_event_enabled("Vehicle Fine", "new"): + return + + subject = f"Vehicle Fine Alert: {doc.vehicle or doc.reference}" + message = ( + f"Vehicle {doc.vehicle or '-'} has a new traffic fine " + f"({doc.reference or '-'}) with status {doc.status or '-'} " + f"and total {doc.total or 0}." + ) + result = send_authority_notification("Vehicle Fine", subject, message) + if result.get("sent"): + frappe.db.set_value( + "Vehicle Fine Record", + doc.name, + { + "authority_notified_on_new": now_datetime(), + "authority_last_notified_status": doc.status or "", + }, + update_modified=False, + ) def _notify_vehicle_fine_status_change(docname, vehicle, old_status, new_status): - if old_status == new_status: - return - if not is_authority_notification_event_enabled("Vehicle Fine", "status_change"): - return - - subject = f"Vehicle Fine Status Changed: {vehicle or docname}" - message = ( - f"Vehicle {vehicle or '-'} traffic fine status changed " - f"from {old_status or '-'} to {new_status or '-'}." - ) - result = send_authority_notification("Vehicle Fine", subject, message) - if result.get("sent"): - frappe.db.set_value( - "Vehicle Fine Record", - docname, - "authority_last_notified_status", - new_status or "", - update_modified=False, - ) + if old_status == new_status: + return + if not is_authority_notification_event_enabled("Vehicle Fine", "status_change"): + return + + subject = f"Vehicle Fine Status Changed: {vehicle or docname}" + message = ( + f"Vehicle {vehicle or '-'} traffic fine status changed " + f"from {old_status or '-'} to {new_status or '-'}." + ) + result = send_authority_notification("Vehicle Fine", subject, message) + if result.get("sent"): + frappe.db.set_value( + "Vehicle Fine Record", + docname, + "authority_last_notified_status", + new_status or "", + update_modified=False, + ) def send_pending_vehicle_fine_notifications(): - for row in frappe.get_all( - "Vehicle Fine Record", - fields=[ - "name", - "vehicle", - "reference", - "status", - "total", - "authority_notified_on_new", - "authority_last_notified_status", - ], - limit_page_length=0, - ): - doc = frappe._dict(row) - - if not doc.authority_notified_on_new: - _notify_vehicle_fine_new(doc) - - last_status = doc.authority_last_notified_status or "" - current_status = doc.status or "" - if last_status and current_status and last_status != current_status: - _notify_vehicle_fine_status_change(doc.name, doc.vehicle, last_status, current_status) - - frappe.db.commit() + for row in frappe.get_all( + "Vehicle Fine Record", + fields=[ + "name", + "vehicle", + "reference", + "status", + "total", + "authority_notified_on_new", + "authority_last_notified_status", + ], + limit_page_length=0, + ): + doc = frappe._dict(row) + + if not doc.authority_notified_on_new: + _notify_vehicle_fine_new(doc) + + last_status = doc.authority_last_notified_status or "" + current_status = doc.status or "" + if last_status and current_status and last_status != current_status: + _notify_vehicle_fine_status_change(doc.name, doc.vehicle, last_status, current_status) + + frappe.db.commit() diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py index e1e1c01d..8768caca 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py @@ -4,228 +4,227 @@ import frappe from csf_tz.csf_tz.doctype.vehicle_fine_record.vehicle_fine_record import ( - is_valid_number_plate, - normalize_number_plate, - sync_vehicle_fines, + is_valid_number_plate, + normalize_number_plate, + sync_vehicle_fines, ) from csf_tz.csf_tz.doctype.vehicle_sync_task import queue from csf_tz.vehicle_authority import get_vehicle_like_records - TASK_DOCTYPE = "Vehicle Sync Task" RATE_LIMIT_CACHE_KEY = "vehicle_sync_task:tpf_calls" def _get_current_plates(): - plates = {} - for record in get_vehicle_like_records(): - plate = normalize_number_plate(record.plate_number) - if not plate or not is_valid_number_plate(plate): - continue - plates[plate] = plate - return sorted(plates) + plates = {} + for record in get_vehicle_like_records(): + plate = normalize_number_plate(record.plate_number) + if not plate or not is_valid_number_plate(plate): + continue + plates[plate] = plate + return sorted(plates) def _acquire_rate_limit_slot(): - now_ts = time.time() - cache = frappe.cache() - raw = cache.get_value(RATE_LIMIT_CACHE_KEY) + now_ts = time.time() + cache = frappe.cache() + raw = cache.get_value(RATE_LIMIT_CACHE_KEY) - try: - timestamps = json.loads(raw) if raw else [] - except Exception: - timestamps = [] + try: + timestamps = json.loads(raw) if raw else [] + except Exception: + timestamps = [] - timestamps = [ts for ts in timestamps if now_ts - float(ts) < 60] - if len(timestamps) >= queue.MAX_CALLS_PER_MINUTE: - cache.set_value(RATE_LIMIT_CACHE_KEY, json.dumps(timestamps), expires_in_sec=60) - return False + timestamps = [ts for ts in timestamps if now_ts - float(ts) < 60] + if len(timestamps) >= queue.MAX_CALLS_PER_MINUTE: + cache.set_value(RATE_LIMIT_CACHE_KEY, json.dumps(timestamps), expires_in_sec=60) + return False - timestamps.append(now_ts) - cache.set_value(RATE_LIMIT_CACHE_KEY, json.dumps(timestamps), expires_in_sec=60) - return True + timestamps.append(now_ts) + cache.set_value(RATE_LIMIT_CACHE_KEY, json.dumps(timestamps), expires_in_sec=60) + return True def _backoff_seconds(attempts): - exponent = max(attempts - 1, 0) - return queue.BASE_BACKOFF * (2 ** exponent) + exponent = max(attempts - 1, 0) + return queue.BASE_BACKOFF * (2**exponent) @frappe.whitelist() def run_vehicle_batch(): - started_at = time.monotonic() - processed = 0 - errors = 0 - - queue.reset_stuck_tasks(TASK_DOCTYPE, timeout_minutes=10) - tasks = queue.claim_batch(TASK_DOCTYPE, limit=queue.BATCH_SIZE) - - if not tasks: - return {"status": "no_tasks", "message": "No pending vehicle sync tasks"} - - for task in tasks: - if (time.monotonic() - started_at) >= queue.TIME_BUDGET_SEC: - break - - if not _acquire_rate_limit_slot(): - queue.schedule_next( - TASK_DOCTYPE, - task, - 60, - "TPF per-minute limit reached for this site", - ) - continue - - result = sync_vehicle_fines(task["vehicle_no"]) - status = result.get("status") - - if status == "success": - queue.mark_done(TASK_DOCTYPE, task) - processed += 1 - continue - - if status in {"rate_limited", "retryable_error"}: - attempts, _ = queue.bump_attempts(TASK_DOCTYPE, task) - queue.schedule_next( - TASK_DOCTYPE, - task, - _backoff_seconds(attempts), - result.get("message") or status, - ) - errors += 1 - continue - - queue.mark_failed( - TASK_DOCTYPE, - task, - result.get("message") or "Unhandled sync error", - ) - errors += 1 - - frappe.db.commit() - return { - "status": "completed", - "processed": processed, - "errors": errors, - "claimed": len(tasks), - } + started_at = time.monotonic() + processed = 0 + errors = 0 + + queue.reset_stuck_tasks(TASK_DOCTYPE, timeout_minutes=10) + tasks = queue.claim_batch(TASK_DOCTYPE, limit=queue.BATCH_SIZE) + + if not tasks: + return {"status": "no_tasks", "message": "No pending vehicle sync tasks"} + + for task in tasks: + if (time.monotonic() - started_at) >= queue.TIME_BUDGET_SEC: + break + + if not _acquire_rate_limit_slot(): + queue.schedule_next( + TASK_DOCTYPE, + task, + 60, + "TPF per-minute limit reached for this site", + ) + continue + + result = sync_vehicle_fines(task["vehicle_no"]) + status = result.get("status") + + if status == "success": + queue.mark_done(TASK_DOCTYPE, task) + processed += 1 + continue + + if status in {"rate_limited", "retryable_error"}: + attempts, _ = queue.bump_attempts(TASK_DOCTYPE, task) + queue.schedule_next( + TASK_DOCTYPE, + task, + _backoff_seconds(attempts), + result.get("message") or status, + ) + errors += 1 + continue + + queue.mark_failed( + TASK_DOCTYPE, + task, + result.get("message") or "Unhandled sync error", + ) + errors += 1 + + frappe.db.commit() + return { + "status": "completed", + "processed": processed, + "errors": errors, + "claimed": len(tasks), + } @frappe.whitelist() def create_sync_task(vehicle_no, priority=0, immediate=False): - try: - vehicle_no = normalize_number_plate(vehicle_no) - if not vehicle_no or not is_valid_number_plate(vehicle_no): - return None - - existing = frappe.db.get_value( - TASK_DOCTYPE, - { - "vehicle_no": vehicle_no, - "status": ["in", ["Pending", "Processing"]], - "is_deleted": ["!=", 1], - }, - "name", - ) - - if existing: - if immediate or priority > 5: - frappe.db.set_value( - TASK_DOCTYPE, - existing, - { - "priority": max(priority, 5), - "next_run_at": frappe.utils.now_datetime(), - }, - ) - return existing - - deleted_task = frappe.db.get_value( - TASK_DOCTYPE, - {"vehicle_no": vehicle_no, "is_deleted": 1}, - "name", - ) - - if deleted_task: - frappe.db.set_value( - TASK_DOCTYPE, - deleted_task, - { - "is_deleted": 0, - "status": "Pending", - "priority": priority, - "attempts": 0, - "backoff_exp": 0, - "next_run_at": frappe.utils.now_datetime() if immediate else None, - "claimed_by": "", - "claimed_at": None, - "last_error": "", - }, - ) - return deleted_task - - task = frappe.new_doc(TASK_DOCTYPE) - task.vehicle_no = vehicle_no - task.status = "Pending" - task.priority = priority - task.attempts = 0 - task.backoff_exp = 0 - task.is_deleted = 0 - task.next_run_at = frappe.utils.now_datetime() if immediate else None - task.insert(ignore_permissions=True) - return task.name - - except Exception as exc: - frappe.log_error( - title="Sync Task Creation Failed", - message=f"Error creating sync task for vehicle {vehicle_no}: {str(exc)}", - ) - return None + try: + vehicle_no = normalize_number_plate(vehicle_no) + if not vehicle_no or not is_valid_number_plate(vehicle_no): + return None + + existing = frappe.db.get_value( + TASK_DOCTYPE, + { + "vehicle_no": vehicle_no, + "status": ["in", ["Pending", "Processing"]], + "is_deleted": ["!=", 1], + }, + "name", + ) + + if existing: + if immediate or priority > 5: + frappe.db.set_value( + TASK_DOCTYPE, + existing, + { + "priority": max(priority, 5), + "next_run_at": frappe.utils.now_datetime(), + }, + ) + return existing + + deleted_task = frappe.db.get_value( + TASK_DOCTYPE, + {"vehicle_no": vehicle_no, "is_deleted": 1}, + "name", + ) + + if deleted_task: + frappe.db.set_value( + TASK_DOCTYPE, + deleted_task, + { + "is_deleted": 0, + "status": "Pending", + "priority": priority, + "attempts": 0, + "backoff_exp": 0, + "next_run_at": frappe.utils.now_datetime() if immediate else None, + "claimed_by": "", + "claimed_at": None, + "last_error": "", + }, + ) + return deleted_task + + task = frappe.new_doc(TASK_DOCTYPE) + task.vehicle_no = vehicle_no + task.status = "Pending" + task.priority = priority + task.attempts = 0 + task.backoff_exp = 0 + task.is_deleted = 0 + task.next_run_at = frappe.utils.now_datetime() if immediate else None + task.insert(ignore_permissions=True) + return task.name + + except Exception as exc: + frappe.log_error( + title="Sync Task Creation Failed", + message=f"Error creating sync task for vehicle {vehicle_no}: {str(exc)}", + ) + return None @frappe.whitelist() def seed_vehicle_sync_queue(): - try: - current_plates = set(_get_current_plates()) - all_tasks = frappe.get_all( - TASK_DOCTYPE, - fields=["vehicle_no", "name", "is_deleted"], - ) - existing_tasks_map = {task.vehicle_no: task for task in all_tasks} - active_plates = {task.vehicle_no for task in all_tasks if not task.is_deleted} - - created = skipped = invalid = reactivated = deleted_marked = 0 - - for number_plate in current_plates: - if number_plate not in existing_tasks_map: - create_sync_task(number_plate, priority=0) - created += 1 - elif existing_tasks_map[number_plate].is_deleted: - create_sync_task(number_plate, priority=0) - reactivated += 1 - else: - skipped += 1 - - for plate in active_plates: - if plate not in current_plates: - task_name = existing_tasks_map[plate].name - frappe.db.set_value(TASK_DOCTYPE, task_name, "is_deleted", 1) - deleted_marked += 1 - - frappe.db.commit() - return { - "status": "success", - "created": created, - "skipped": skipped, - "invalid": invalid, - "reactivated": reactivated, - "deleted_marked": deleted_marked, - "total_vehicles": len(current_plates), - "total_valid_plates": len(current_plates), - } - except Exception as exc: - frappe.log_error( - title="Seed Vehicle Sync Queue Failed", - message=f"Critical error in seed_vehicle_sync_queue: {str(exc)}", - ) - return {"status": "error", "message": str(exc)} + try: + current_plates = set(_get_current_plates()) + all_tasks = frappe.get_all( + TASK_DOCTYPE, + fields=["vehicle_no", "name", "is_deleted"], + ) + existing_tasks_map = {task.vehicle_no: task for task in all_tasks} + active_plates = {task.vehicle_no for task in all_tasks if not task.is_deleted} + + created = skipped = invalid = reactivated = deleted_marked = 0 + + for number_plate in current_plates: + if number_plate not in existing_tasks_map: + create_sync_task(number_plate, priority=0) + created += 1 + elif existing_tasks_map[number_plate].is_deleted: + create_sync_task(number_plate, priority=0) + reactivated += 1 + else: + skipped += 1 + + for plate in active_plates: + if plate not in current_plates: + task_name = existing_tasks_map[plate].name + frappe.db.set_value(TASK_DOCTYPE, task_name, "is_deleted", 1) + deleted_marked += 1 + + frappe.db.commit() + return { + "status": "success", + "created": created, + "skipped": skipped, + "invalid": invalid, + "reactivated": reactivated, + "deleted_marked": deleted_marked, + "total_vehicles": len(current_plates), + "total_valid_plates": len(current_plates), + } + except Exception as exc: + frappe.log_error( + title="Seed Vehicle Sync Queue Failed", + message=f"Critical error in seed_vehicle_sync_queue: {str(exc)}", + ) + return {"status": "error", "message": str(exc)} diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py index 30c7c680..07afd09b 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py @@ -1,4 +1,5 @@ import secrets + import frappe # ------------ CONFIGURATION ------------ @@ -9,158 +10,175 @@ BACKOFF_JITTER = 0.2 SUCCESS_INTERVAL_SECONDS = 60 * 60 * 2 MAX_CALLS_PER_MINUTE = 1 -WORKER_ID = frappe.local.site + # ------------ INTERNAL HELPERS ------------ def _now(): - return frappe.utils.now_datetime() + return frappe.utils.now_datetime() + def _jitter(seconds): - # Generate cryptographically secure random jitter for backoff timing - # Range: -BACKOFF_JITTER to +BACKOFF_JITTER - random_factor = (secrets.randbelow(10000) / 10000.0) * 2 - 1 # -1 to 1 - jitter_factor = 1 + (random_factor * BACKOFF_JITTER) - return int(seconds * jitter_factor) + # Generate cryptographically secure random jitter for backoff timing + # Range: -BACKOFF_JITTER to +BACKOFF_JITTER + random_factor = (secrets.randbelow(10000) / 10000.0) * 2 - 1 # -1 to 1 + jitter_factor = 1 + (random_factor * BACKOFF_JITTER) + return int(seconds * jitter_factor) + # ------------ CORE QUEUE OPERATIONS ------------ def claim_batch(doctype, limit=BATCH_SIZE): - try: - now = _now() - Task = frappe.qb.DocType(doctype) - - rows = ( - frappe.qb.from_(Task) - .select(Task.name) - .where( - (Task.status == "Pending") & - ((Task.next_run_at.isnull()) | (Task.next_run_at <= now)) & - ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) # ← IGNORE DELETED TASKS - ) - .orderby(Task.priority, order=frappe.qb.terms.Order.desc) - .orderby(Task.name) - .limit(limit) - ).run(as_dict=True) - - if not rows: - return [] - - claimed = [] - for row in rows: - frappe.db.set_value(doctype, row["name"], { - "status": "Processing", - "claimed_by": WORKER_ID, - "claimed_at": now, - "last_run_at": now, - }) - data = frappe.db.get_value(doctype, row["name"], ["name", "vehicle_no"], as_dict=True) - claimed.append(data) - return claimed - except Exception as e: - frappe.log_error( - title="Queue Claim Batch Failed", - message=f"Error claiming batch from {doctype}: {str(e)}" - ) - return [] + try: + now = _now() + Task = frappe.qb.DocType(doctype) + + rows = ( + frappe.qb.from_(Task) + .select(Task.name) + .where( + (Task.status == "Pending") + & ((Task.next_run_at.isnull()) | (Task.next_run_at <= now)) + & ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) # ← IGNORE DELETED TASKS + ) + .orderby(Task.priority, order=frappe.qb.terms.Order.desc) + .orderby(Task.name) + .limit(limit) + ).run(as_dict=True) + + if not rows: + return [] + + claimed = [] + for row in rows: + frappe.db.set_value( + doctype, + row["name"], + { + "status": "Processing", + "claimed_by": frappe.local.site, + "claimed_at": now, + "last_run_at": now, + }, + ) + data = frappe.db.get_value(doctype, row["name"], ["name", "vehicle_no"], as_dict=True) + claimed.append(data) + return claimed + except Exception as e: + frappe.log_error( + title="Queue Claim Batch Failed", message=f"Error claiming batch from {doctype}: {str(e)}" + ) + return [] + def mark_done(doctype, task): - try: - frappe.db.set_value(doctype, task["name"], { - "status": "Pending", - "attempts": 0, - "backoff_exp": 0, - "last_run_at": _now(), - "claimed_by": "", - "claimed_at": None, - "next_run_at": frappe.utils.add_to_date(_now(), seconds=SUCCESS_INTERVAL_SECONDS), - "last_error": "" - }) - except Exception as e: - frappe.log_error( - title="Queue Mark Done Failed", - message=f"Error marking task {task.get('name')} as done in {doctype}: {str(e)}" - ) + try: + frappe.db.set_value( + doctype, + task["name"], + { + "status": "Pending", + "attempts": 0, + "backoff_exp": 0, + "last_run_at": _now(), + "claimed_by": "", + "claimed_at": None, + "next_run_at": frappe.utils.add_to_date(_now(), seconds=SUCCESS_INTERVAL_SECONDS), + "last_error": "", + }, + ) + except Exception as e: + frappe.log_error( + title="Queue Mark Done Failed", + message=f"Error marking task {task.get('name')} as done in {doctype}: {str(e)}", + ) + def mark_failed(doctype, task, err_msg): - try: - frappe.db.set_value(doctype, task["name"], { - "status": "Failed", - "last_error": err_msg[:1000], - "last_run_at": _now(), - "claimed_by": "", - "claimed_at": None, - "next_run_at": None, - }) - except Exception as e: - frappe.log_error( - title="Queue Mark Failed Error", - message=f"Error marking task {task.get('name')} as failed in {doctype}: {str(e)}" - ) + try: + frappe.db.set_value( + doctype, + task["name"], + { + "status": "Failed", + "last_error": err_msg[:1000], + "last_run_at": _now(), + "claimed_by": "", + "claimed_at": None, + "next_run_at": None, + }, + ) + except Exception as e: + frappe.log_error( + title="Queue Mark Failed Error", + message=f"Error marking task {task.get('name')} as failed in {doctype}: {str(e)}", + ) + def bump_attempts(doctype, task): - try: - current = frappe.db.get_value( - doctype, task["name"], ["attempts", "backoff_exp"], as_dict=True - ) - attempts = (current.attempts or 0) + 1 - backoff_exp = min((current.backoff_exp or 0) + 1, 6) - frappe.db.set_value(doctype, task["name"], { - "attempts": attempts, - "backoff_exp": backoff_exp, - "last_run_at": _now() - }) - return attempts, backoff_exp - except Exception as e: - frappe.log_error( - title="Queue Bump Attempts Failed", - message=f"Error bumping attempts for task {task.get('name')} in {doctype}: {str(e)}" - ) - return 1, 1 # Return default values + try: + current = frappe.db.get_value(doctype, task["name"], ["attempts", "backoff_exp"], as_dict=True) + attempts = (current.attempts or 0) + 1 + backoff_exp = min((current.backoff_exp or 0) + 1, 6) + frappe.db.set_value( + doctype, task["name"], {"attempts": attempts, "backoff_exp": backoff_exp, "last_run_at": _now()} + ) + return attempts, backoff_exp + except Exception as e: + frappe.log_error( + title="Queue Bump Attempts Failed", + message=f"Error bumping attempts for task {task.get('name')} in {doctype}: {str(e)}", + ) + return 1, 1 # Return default values + def schedule_next(doctype, task, backoff_seconds, error_msg=""): - try: - attempts, _ = bump_attempts(doctype, task) - if attempts >= MAX_ATTEMPTS: - mark_failed(doctype, task, error_msg or "Max attempts exceeded") - return - next_run = frappe.utils.add_to_date(_now(), seconds=_jitter(backoff_seconds)) - frappe.db.set_value(doctype, task["name"], { - "status": "Pending", - "claimed_by": "", - "claimed_at": None, - "next_run_at": next_run, - "last_error": error_msg[:500] if error_msg else "", - }) - except Exception as e: - frappe.log_error( - title="Queue Schedule Next Failed", - message=f"Error scheduling next run for task {task.get('name')} in {doctype}: {str(e)}" - ) + try: + attempts, _ = bump_attempts(doctype, task) + if attempts >= MAX_ATTEMPTS: + mark_failed(doctype, task, error_msg or "Max attempts exceeded") + return + next_run = frappe.utils.add_to_date(_now(), seconds=_jitter(backoff_seconds)) + frappe.db.set_value( + doctype, + task["name"], + { + "status": "Pending", + "claimed_by": "", + "claimed_at": None, + "next_run_at": next_run, + "last_error": error_msg[:500] if error_msg else "", + }, + ) + except Exception as e: + frappe.log_error( + title="Queue Schedule Next Failed", + message=f"Error scheduling next run for task {task.get('name')} in {doctype}: {str(e)}", + ) + def reset_stuck_tasks(doctype, timeout_minutes=10): - try: - timeout_time = frappe.utils.add_to_date(_now(), minutes=-timeout_minutes) - Task = frappe.qb.DocType(doctype) - stuck = ( - frappe.qb.from_(Task) - .select(Task.name) - .where( - (Task.status == "Processing") & - (Task.claimed_at < timeout_time) & - ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) # ← IGNORE DELETED TASKS - ) - ).run(as_dict=True) - - for row in stuck: - frappe.db.set_value(doctype, row["name"], { - "status": "Pending", - "claimed_by": "", - "claimed_at": None, - "next_run_at": _now() - }) - return len(stuck) - except Exception as e: - frappe.log_error( - title="Queue Reset Stuck Tasks Failed", - message=f"Error resetting stuck tasks in {doctype}: {str(e)}" - ) - return 0 + try: + timeout_time = frappe.utils.add_to_date(_now(), minutes=-timeout_minutes) + Task = frappe.qb.DocType(doctype) + stuck = ( + frappe.qb.from_(Task) + .select(Task.name) + .where( + (Task.status == "Processing") + & (Task.claimed_at < timeout_time) + & ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) # ← IGNORE DELETED TASKS + ) + ).run(as_dict=True) + + for row in stuck: + frappe.db.set_value( + doctype, + row["name"], + {"status": "Pending", "claimed_by": "", "claimed_at": None, "next_run_at": _now()}, + ) + return len(stuck) + except Exception as e: + frappe.log_error( + title="Queue Reset Stuck Tasks Failed", + message=f"Error resetting stuck tasks in {doctype}: {str(e)}", + ) + return 0 diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/vehicle_sync_task.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/vehicle_sync_task.py index 1d495f70..5989cc9d 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/vehicle_sync_task.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/vehicle_sync_task.py @@ -6,8 +6,9 @@ from frappe.query_builder import Interval from frappe.query_builder.functions import Now + class VehicleSyncTask(Document): - @staticmethod - def clear_old_logs(days=7): - table = frappe.qb.DocType("Vehicle Sync Task") - frappe.db.delete(table, filters=(table.creation < (Now() - Interval(days=days)))) \ No newline at end of file + @staticmethod + def clear_old_logs(days=7): + table = frappe.qb.DocType("Vehicle Sync Task") + frappe.db.delete(table, filters=(table.creation < (Now() - Interval(days=days)))) diff --git a/csf_tz/csf_tz/employee_contact_qr.js b/csf_tz/csf_tz/employee_contact_qr.js index e5567c4e..22848971 100644 --- a/csf_tz/csf_tz/employee_contact_qr.js +++ b/csf_tz/csf_tz/employee_contact_qr.js @@ -24,4 +24,4 @@ frappe.ui.form.on('Employee', { } }); } -}); \ No newline at end of file +}); diff --git a/csf_tz/csf_tz/fees.js b/csf_tz/csf_tz/fees.js index 96e5a84b..7b9435d5 100644 --- a/csf_tz/csf_tz/fees.js +++ b/csf_tz/csf_tz/fees.js @@ -23,4 +23,4 @@ frappe.ui.form.on('Fees', { }; }); }, -}); \ No newline at end of file +}); diff --git a/csf_tz/csf_tz/journal_entry.js b/csf_tz/csf_tz/journal_entry.js index f60e8c36..450fa139 100644 --- a/csf_tz/csf_tz/journal_entry.js +++ b/csf_tz/csf_tz/journal_entry.js @@ -3,4 +3,3 @@ // Budget check for Journal Entry is now handled server-side via doc_events hooks // See apps/csf_tz/csf_tz/hooks.py and apps/csf_tz/csf_tz/budget_check.py - diff --git a/csf_tz/csf_tz/landed_cost_voucher.js b/csf_tz/csf_tz/landed_cost_voucher.js index 605c5aef..72d65d98 100644 --- a/csf_tz/csf_tz/landed_cost_voucher.js +++ b/csf_tz/csf_tz/landed_cost_voucher.js @@ -31,4 +31,3 @@ frappe.ui.form.on("Landed Cost Voucher", { }); } }); - diff --git a/csf_tz/csf_tz/page/jobcards/jobcards.js b/csf_tz/csf_tz/page/jobcards/jobcards.js index 4f1c5724..f980aa27 100644 --- a/csf_tz/csf_tz/page/jobcards/jobcards.js +++ b/csf_tz/csf_tz/page/jobcards/jobcards.js @@ -8,7 +8,7 @@ frappe.pages['jobcards'].on_page_load = function (wrapper) { this.page.$JobCards = new frappe.JobCards.job_cards(this.page); - $("head").append(""); + $("head").append(""); $("head").append(""); $("head").append(""); -} \ No newline at end of file +} diff --git a/csf_tz/csf_tz/page/jobcards/jobcards.py b/csf_tz/csf_tz/page/jobcards/jobcards.py index 0fab3584..2827e3a4 100644 --- a/csf_tz/csf_tz/page/jobcards/jobcards.py +++ b/csf_tz/csf_tz/page/jobcards/jobcards.py @@ -1,53 +1,46 @@ -from __future__ import unicode_literals -import frappe -from frappe import _ import json -from csf_tz import console + +import frappe @frappe.whitelist() def get_job_cards(): - data = frappe.get_list( - "Job Card", - filters={ - "status": ["in", ["Open", "Work In Progress", "Material Transferred", "On Hold", "Submitted"]], - "docstatus": 0, - }, - fields=["*"], - limit_page_length=0, - order_by='name' - ) - for card in data: - card["operation"] = frappe.get_doc("Operation", card.operation) - card["work_order_image"] = frappe.get_value( - "Work Order", card.work_order, "image") - card["time_logs"] = frappe.get_all("Job Card Time Log", filters={ - "parent": card.name}, fields=["*"]) - return data + data = frappe.get_list( + "Job Card", + filters={ + "status": ["in", ["Open", "Work In Progress", "Material Transferred", "On Hold", "Submitted"]], + "docstatus": 0, + }, + fields=["*"], + limit_page_length=0, + order_by="name", + ) + for card in data: + card["operation"] = frappe.get_doc("Operation", card.operation) + card["work_order_image"] = frappe.get_value("Work Order", card.work_order, "image") + card["time_logs"] = frappe.get_all("Job Card Time Log", filters={"parent": card.name}, fields=["*"]) + return data @frappe.whitelist() def get_employees(company): - data = frappe.get_list( - "Employee", - filters={ - "status": "Active", - "company": company - }, - fields=["name", "employee_name"], - limit_page_length=0, - order_by='name' - ) + data = frappe.get_list( + "Employee", + filters={"status": "Active", "company": company}, + fields=["name", "employee_name"], + limit_page_length=0, + order_by="name", + ) - return data + return data @frappe.whitelist() def save_doc(doc, action="Save"): - doc = json.loads(doc) - cur_doc = frappe.get_doc("Job Card", doc.get("name")) - cur_doc.update(doc) - cur_doc.save() - if action == "Submit": - cur_doc.submit() - return cur_doc + doc = json.loads(doc) + cur_doc = frappe.get_doc("Job Card", doc.get("name")) + cur_doc.update(doc) + cur_doc.save() + if action == "Submit": + cur_doc.submit() + return cur_doc diff --git a/csf_tz/csf_tz/page/scan_qrcode/scan_qrcode.py b/csf_tz/csf_tz/page/scan_qrcode/scan_qrcode.py index 9c3af47c..ce81bcd0 100644 --- a/csf_tz/csf_tz/page/scan_qrcode/scan_qrcode.py +++ b/csf_tz/csf_tz/page/scan_qrcode/scan_qrcode.py @@ -1,18 +1,17 @@ import frappe -import json @frappe.whitelist() def add_biometric_log(data): - """Add biometric log""" - log = frappe.new_doc("CSF TZ Biometric Log") - log.user_id = data - log.uid = data - log.timestamp = frappe.utils.now_datetime() - log.insert(ignore_permissions=True) - frappe.msgprint("Biometric log added successfully", alert=True) + """Add biometric log""" + log = frappe.new_doc("CSF TZ Biometric Log") + log.user_id = data + log.uid = data + log.timestamp = frappe.utils.now_datetime() + log.insert(ignore_permissions=True) + frappe.msgprint("Biometric log added successfully", alert=True) - return log + return log - # if not data: - # frappe.throw(str(frappe.form_dict)) + # if not data: + # frappe.throw(str(frappe.form_dict)) diff --git a/csf_tz/csf_tz/payment_entry.js b/csf_tz/csf_tz/payment_entry.js index 77baa5a9..fe180134 100644 --- a/csf_tz/csf_tz/payment_entry.js +++ b/csf_tz/csf_tz/payment_entry.js @@ -39,7 +39,7 @@ frappe.ui.form.on("Payment Entry", { // Feature is disabled, do not proceed with get_outstanding_documents return; } - + // Feature is enabled, proceed with existing functionality const today = frappe.datetime.get_today(); const filters = { @@ -62,7 +62,7 @@ frappe.ui.form.on("Payment Entry", { // Feature is disabled, do not proceed return; } - + // Continue with normal functionality if (typeof frappe.route_history[frappe.route_history.length - 2] != "undefined") { if (frappe.route_history[frappe.route_history.length - 2][1] in ["Sales Invoice", "Employee Advance", "Purchase Invoice"]) { @@ -198,7 +198,7 @@ frappe.ui.form.on("Payment Entry", { }, { fieldtype: "Column Break" }, { fieldtype: "Float", label: __("Less Than Amount"), fieldname: "outstanding_amt_less_than" }, - { + { fieldtype: "Check", label: __("Allocate Payment Amount"), fieldname: "allocate_payment_amount", @@ -210,7 +210,7 @@ frappe.ui.form.on("Payment Entry", { fields, function (filters) { frm.clear_table("references"); - + if (!frm.doc.party) { frappe.throw(__("Please select a Party first")); return; @@ -226,7 +226,7 @@ frappe.ui.form.on("Payment Entry", { } frappe.flags.allocate_payment_amount = filters.allocate_payment_amount; - + var args = { "posting_date": frm.doc.posting_date, "company": frm.doc.company, @@ -261,15 +261,15 @@ frappe.ui.form.on("Payment Entry", { c.posting_date = d.posting_date; c.total_amount = d.invoice_amount; c.outstanding_amount = d.outstanding_amount; - + // Add to total outstanding total_positive_outstanding += flt(d.outstanding_amount); - + var party_account_currency = frm.doc.payment_type == "Receive" ? frm.doc.paid_from_account_currency : frm.doc.paid_to_account_currency; - + var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; - + if (party_account_currency != company_currency) { c.exchange_rate = d.exchange_rate; } else { @@ -285,9 +285,9 @@ frappe.ui.form.on("Payment Entry", { frm.set_value("received_amount", total_positive_outstanding); } } - + frm.refresh_fields(); - + const paid_amount = frm.doc.payment_type == "Receive" ? frm.doc.paid_amount : frm.doc.received_amount; if (paid_amount && frappe.flags.allocate_payment_amount) { frm.events.allocate_party_amount_against_ref_docs(frm, paid_amount, true); diff --git a/csf_tz/csf_tz/program_enrollment.js b/csf_tz/csf_tz/program_enrollment.js index a9fc5657..caa0883d 100644 --- a/csf_tz/csf_tz/program_enrollment.js +++ b/csf_tz/csf_tz/program_enrollment.js @@ -25,7 +25,7 @@ frappe.ui.form.on("Program Enrollment", { student_category: function () { frappe.ui.form.trigger("program"); }, - + validate: function (frm) { if (( !frm.doc.fees || !frm.doc.fees.length) && frm.doc.student_category) { frm.trigger("program"); diff --git a/csf_tz/csf_tz/program_enrollment_tool.js b/csf_tz/csf_tz/program_enrollment_tool.js index 7fffcb70..f7da49d7 100644 --- a/csf_tz/csf_tz/program_enrollment_tool.js +++ b/csf_tz/csf_tz/program_enrollment_tool.js @@ -35,4 +35,4 @@ frappe.ui.form.on("Program Enrollment Tool", { } }, -}) \ No newline at end of file +}) diff --git a/csf_tz/csf_tz/property_setter.js b/csf_tz/csf_tz/property_setter.js index 405d86a9..e0f2c1e2 100644 --- a/csf_tz/csf_tz/property_setter.js +++ b/csf_tz/csf_tz/property_setter.js @@ -44,4 +44,4 @@ frappe.listview_settings['Property Setter'] = { a.remove(); }); } -}; \ No newline at end of file +}; diff --git a/csf_tz/csf_tz/purchase_invoice.js b/csf_tz/csf_tz/purchase_invoice.js index 15fbd09c..758f095f 100644 --- a/csf_tz/csf_tz/purchase_invoice.js +++ b/csf_tz/csf_tz/purchase_invoice.js @@ -21,9 +21,9 @@ frappe.ui.form.on("Purchase Invoice", { frm.trigger("tax_category"); } } - }); + }); } - }, 1000); + }, 1000); }, setup: function(frm) { frm.set_query("taxes_and_charges", function() { @@ -43,10 +43,10 @@ frappe.ui.form.on("Purchase Invoice", { }); frm.dimensions = dimensions; // console.log(frm.dimensions); - + } } - }); + }); // const dimensions_fields = $("div.frappe-control[data-fieldname='expense_type']") // console.log(dimensions_fields); }, diff --git a/csf_tz/csf_tz/purchase_receipt.js b/csf_tz/csf_tz/purchase_receipt.js index 7320a13b..7ce848b4 100644 --- a/csf_tz/csf_tz/purchase_receipt.js +++ b/csf_tz/csf_tz/purchase_receipt.js @@ -14,8 +14,8 @@ frappe.ui.form.on("Purchase Receipt", { frm.trigger("tax_category"); } } - }); + }); } - }, 1000); + }, 1000); }, -}); \ No newline at end of file +}); diff --git a/csf_tz/csf_tz/quotation.js b/csf_tz/csf_tz/quotation.js index fc59f90e..4015b65c 100644 --- a/csf_tz/csf_tz/quotation.js +++ b/csf_tz/csf_tz/quotation.js @@ -39,4 +39,4 @@ frappe.ui.form.on("Quotation", { } }, 1000); }, -}); \ No newline at end of file +}); diff --git a/csf_tz/csf_tz/report/accounts_receivable_multi_currency/accounts_receivable_multi_currency.js b/csf_tz/csf_tz/report/accounts_receivable_multi_currency/accounts_receivable_multi_currency.js index 78fbf256..d672026a 100644 --- a/csf_tz/csf_tz/report/accounts_receivable_multi_currency/accounts_receivable_multi_currency.js +++ b/csf_tz/csf_tz/report/accounts_receivable_multi_currency/accounts_receivable_multi_currency.js @@ -200,4 +200,3 @@ frappe.query_reports["Accounts Receivable Multi Currency"] = { } erpnext.utils.add_dimensions('Accounts Receivable', 9); - diff --git a/csf_tz/csf_tz/report/accounts_receivable_multi_currency/accounts_receivable_multi_currency.py b/csf_tz/csf_tz/report/accounts_receivable_multi_currency/accounts_receivable_multi_currency.py index 7935c7c4..8b79cf7f 100644 --- a/csf_tz/csf_tz/report/accounts_receivable_multi_currency/accounts_receivable_multi_currency.py +++ b/csf_tz/csf_tz/report/accounts_receivable_multi_currency/accounts_receivable_multi_currency.py @@ -1,13 +1,16 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe, erpnext -from frappe import _, scrub -from frappe.utils import getdate, nowdate, flt, cint, formatdate, cstr, now, time_diff_in_seconds from collections import OrderedDict + +import frappe +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + get_dimension_with_children, +) from erpnext.accounts.utils import get_currency_precision -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children +from frappe import _, scrub +from frappe.utils import cint, cstr, flt, getdate, nowdate # This report gives a summary of all Outstanding Invoices considering the following @@ -24,6 +27,7 @@ # 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party # 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" + def execute(filters=None): args = { "party_type": "Customer", @@ -31,18 +35,19 @@ def execute(filters=None): } return ReceivablePayableReport(filters).run(args) -class ReceivablePayableReport(object): + +class ReceivablePayableReport: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.filters.report_date = getdate(self.filters.report_date or nowdate()) - self.age_as_on = getdate(nowdate()) \ - if self.filters.report_date > getdate(nowdate()) \ - else self.filters.report_date + self.age_as_on = ( + getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date + ) def run(self, args): self.filters.update(args) self.set_defaults() - self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + self.party_naming_by = frappe.db.get_single_value(*args.get("naming_by")) self.get_columns() self.get_data() self.get_chart_data() @@ -50,8 +55,10 @@ def run(self, args): def set_defaults(self): if not self.filters.get("company"): - self.filters.company = frappe.db.get_single_value('Global Defaults', 'default_company') - self.company_currency = frappe.get_cached_value('Company', self.filters.get("company"), "default_currency") + self.filters.company = frappe.db.get_single_value("Global Defaults", "default_company") + self.company_currency = frappe.get_cached_value( + "Company", self.filters.get("company"), "default_currency" + ) self.currency_precision = get_currency_precision() or 2 self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" self.party_type = self.filters.party_type @@ -59,8 +66,8 @@ def set_defaults(self): self.invoices = set() self.skip_total_row = 0 - if self.filters.get('group_by_party'): - self.previous_party='' + if self.filters.get("group_by_party"): + self.previous_party = "" self.total_row_map = {} self.skip_total_row = 1 @@ -68,7 +75,7 @@ def get_data(self): self.get_gl_entries() self.get_sales_invoices_or_customers_based_on_sales_person() self.voucher_balance = OrderedDict() - self.init_voucher_balance() # invoiced, paid, credit_note, outstanding + self.init_voucher_balance() # invoiced, paid, credit_note, outstanding # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -93,60 +100,69 @@ def init_voucher_balance(self): for gle in self.gl_entries: # get the balance object for voucher_type key = (gle.voucher_type, gle.voucher_no, gle.party) - if not key in self.voucher_balance: + if key not in self.voucher_balance: self.voucher_balance[key] = frappe._dict( - voucher_type = gle.voucher_type, - voucher_no = gle.voucher_no, - party = gle.party, - posting_date = gle.posting_date, - remarks = gle.remarks, - account_currency = gle.account_currency, - invoiced = 0.0, - paid = 0.0, - credit_note = 0.0, - outstanding = 0.0 + voucher_type=gle.voucher_type, + voucher_no=gle.voucher_no, + party=gle.party, + posting_date=gle.posting_date, + remarks=gle.remarks, + account_currency=gle.account_currency, + invoiced=0.0, + paid=0.0, + credit_note=0.0, + outstanding=0.0, ) self.get_invoices(gle) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.init_subtotal_row(gle.party) - if self.filters.get('group_by_party'): - self.init_subtotal_row('Total') + if self.filters.get("group_by_party"): + self.init_subtotal_row("Total") def get_invoices(self, gle): - if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): if self.filters.get("sales_person"): - if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \ - or gle.party in self.sales_person_records.get("Customer", []): - self.invoices.add(gle.voucher_no) + if gle.voucher_no in self.sales_person_records.get( + "Sales Invoice", [] + ) or gle.party in self.sales_person_records.get("Customer", []): + self.invoices.add(gle.voucher_no) else: self.invoices.add(gle.voucher_no) def init_subtotal_row(self, party): if not self.total_row_map.get(party): - self.total_row_map.setdefault(party, { - 'party': party, - 'bold': 1 - }) + self.total_row_map.setdefault(party, {"party": party, "bold": 1}) for field in self.get_currency_fields(): self.total_row_map[party][field] = 0.0 def get_currency_fields(self): - return ['invoiced', 'foreign_amount', 'paid', 'credit_note', 'outstanding', 'range1', - 'range2', 'range3', 'range4', 'range5'] + return [ + "invoiced", + "foreign_amount", + "paid", + "credit_note", + "outstanding", + "range1", + "range2", + "range3", + "range4", + "range5", + ] def update_voucher_balance(self, gle): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance row = self.get_voucher_balance(gle) - if not row: return + if not row: + return # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) if gle_balance > 0: - if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: + if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher: # debit against sales / purchase invoice row.paid -= gle_balance else: @@ -161,9 +177,11 @@ def update_voucher_balance(self, gle): # advance / unlinked payment or other adjustment row.paid -= gle_balance if row.account_currency != self.company_currency: - if row.voucher_type in ["Sales Invoice","Purchase Invoice"]: - row.foreign_currency, row.foreign_amount = frappe.get_value(row.voucher_type, row.voucher_no, ["currency","rounded_total"]) or "" - elif row.voucher_type in ["Payment Entry","Journal Entry"]: + if row.voucher_type in ["Sales Invoice", "Purchase Invoice"]: + row.foreign_currency, row.foreign_amount = ( + frappe.get_value(row.voucher_type, row.voucher_no, ["currency", "rounded_total"]) or "" + ) + elif row.voucher_type in ["Payment Entry", "Journal Entry"]: row.foreign_amount = gle.credit_in_account_currency or gle.debit_in_account_currency row.foreign_currency = row.account_currency @@ -179,14 +197,16 @@ def append_subtotal_row(self, party): if sub_total_row: self.data.append(sub_total_row) self.data.append({}) - self.update_sub_total_row(sub_total_row, 'Total') + self.update_sub_total_row(sub_total_row, "Total") def get_voucher_balance(self, gle): if self.filters.get("sales_person"): against_voucher = gle.against_voucher or gle.voucher_no - if not (gle.party in self.sales_person_records.get("Customer", []) or \ - against_voucher in self.sales_person_records.get("Sales Invoice", [])): - return + if not ( + gle.party in self.sales_person_records.get("Customer", []) + or against_voucher in self.sales_person_records.get("Sales Invoice", []) + ): + return voucher_balance = None if gle.against_voucher: @@ -196,7 +216,7 @@ def get_voucher_balance(self, gle): # If payment is made against credit note # and credit note is made against a Sales Invoice # then consider the payment against original sales invoice. - if gle.against_voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.against_voucher_type in ("Sales Invoice", "Purchase Invoice"): if gle.against_voucher in self.return_entries: return_against = self.return_entries.get(gle.against_voucher) if return_against: @@ -213,11 +233,11 @@ def get_voucher_balance(self, gle): def build_data(self): # set outstanding for all the accumulated balances # as we can use this to filter out invoices without outstanding - for key, row in self.voucher_balance.items(): + for _key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) row.invoice_grand_total = row.invoiced - if abs(row.outstanding) > 1.0/10 ** self.currency_precision: + if abs(row.outstanding) > 1.0 / 10**self.currency_precision: # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -238,10 +258,10 @@ def build_data(self): else: self.append_row(row) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.append_subtotal_row(self.previous_party) if self.data: - self.data.append(self.total_row_map.get('Total')) + self.data.append(self.total_row_map.get("Total")) def append_row(self, row): self.allocate_future_payments(row) @@ -249,7 +269,7 @@ def append_row(self, row): self.set_party_details(row) self.set_ageing(row) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.update_sub_total_row(row, row.party) if self.previous_party and (self.previous_party != row.party): self.append_subtotal_row(self.previous_party) @@ -263,39 +283,47 @@ def set_invoice_details(self, row): invoice_details.pop("due_date", None) row.update(invoice_details) - if row.voucher_type == 'Sales Invoice': + if row.voucher_type == "Sales Invoice": if self.filters.show_delivery_notes: self.set_delivery_notes(row) if self.filters.show_sales_person and row.sales_team: row.sales_person = ", ".join(row.sales_team) - del row['sales_team'] + del row["sales_team"] def set_delivery_notes(self, row): delivery_notes = self.delivery_notes.get(row.voucher_no, []) if delivery_notes: - row.delivery_notes = ', '.join(delivery_notes) + row.delivery_notes = ", ".join(delivery_notes) def build_delivery_note_map(self): if self.invoices and self.filters.show_delivery_notes: self.delivery_notes = frappe._dict() # delivery note link inside sales invoice - si_against_dn = frappe.db.sql(""" + si_against_dn = frappe.db.sql( + """ select parent, delivery_note from `tabSales Invoice Item` - where docstatus=1 and parent in (%s) - """ % (','.join(['%s'] * len(self.invoices))), tuple(self.invoices), as_dict=1) + where docstatus=1 and parent in ({}) + """.format(",".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in si_against_dn: if d.delivery_note: self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note) - dn_against_si = frappe.db.sql(""" + dn_against_si = frappe.db.sql( + """ select distinct parent, against_sales_invoice from `tabDelivery Note Item` - where against_sales_invoice in (%s) - """ % (','.join(['%s'] * len(self.invoices))), tuple(self.invoices) , as_dict=1) + where against_sales_invoice in ({}) + """.format(",".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in dn_against_si: self.delivery_notes.setdefault(d.against_sales_invoice, set()).add(d.parent) @@ -303,40 +331,56 @@ def build_delivery_note_map(self): def get_invoice_details(self): self.invoice_details = frappe._dict() if self.party_type == "Customer": - si_list = frappe.db.sql(""" + si_list = frappe.db.sql( + """ select name, due_date, po_no from `tabSales Invoice` where posting_date <= %s - """,self.filters.report_date, as_dict=1) + """, + self.filters.report_date, + as_dict=1, + ) for d in si_list: self.invoice_details.setdefault(d.name, d) # Get Sales Team if self.filters.show_sales_person: - sales_team = frappe.db.sql(""" + sales_team = frappe.db.sql( + """ select parent, sales_person from `tabSales Team` where parenttype = 'Sales Invoice' - """, as_dict=1) + """, + as_dict=1, + ) for d in sales_team: - self.invoice_details.setdefault(d.parent, {})\ - .setdefault('sales_team', []).append(d.sales_person) + self.invoice_details.setdefault(d.parent, {}).setdefault("sales_team", []).append( + d.sales_person + ) if self.party_type == "Supplier": - for pi in frappe.db.sql(""" + for pi in frappe.db.sql( + """ select name, due_date, bill_no, bill_date from `tabPurchase Invoice` where posting_date <= %s - """, self.filters.report_date, as_dict=1): + """, + self.filters.report_date, + as_dict=1, + ): self.invoice_details.setdefault(pi.name, pi) # Invoices booked via Journal Entries - journal_entries = frappe.db.sql(""" + journal_entries = frappe.db.sql( + """ select name, due_date, bill_no, bill_date from `tabJournal Entry` where posting_date <= %s and voucher_type != "Exchange Rate Revaluation" - """, self.filters.report_date, as_dict=1) + """, + self.filters.report_date, + as_dict=1, + ) for je in journal_entries: if je.bill_no: @@ -354,30 +398,32 @@ def set_party_details(self, row): def allocate_outstanding_based_on_payment_terms(self, row): self.get_payment_terms(row) for term in row.payment_terms: - # update "paid" and "oustanding" for this term if not term.paid: - self.allocate_closing_to_term(row, term, 'paid') + self.allocate_closing_to_term(row, term, "paid") # update "credit_note" and "oustanding" for this term if term.outstanding: - self.allocate_closing_to_term(row, term, 'credit_note') + self.allocate_closing_to_term(row, term, "credit_note") - row.payment_terms = sorted(row.payment_terms, key=lambda x: x['due_date']) + row.payment_terms = sorted(row.payment_terms, key=lambda x: x["due_date"]) def get_payment_terms(self, row): # build payment_terms for row - payment_terms_details = frappe.db.sql(""" + payment_terms_details = frappe.db.sql( + f""" select si.name, si.party_account_currency, si.currency, si.conversion_rate, ps.due_date, ps.payment_amount, ps.description, ps.paid_amount - from `tab{0}` si, `tabPayment Schedule` ps + from `tab{row.voucher_type}` si, `tabPayment Schedule` ps where si.name = ps.parent and si.name = %s order by ps.paid_amount desc, due_date - """.format(row.voucher_type), row.voucher_no, as_dict = 1) - + """, + row.voucher_no, + as_dict=1, + ) original_row = frappe._dict(row) row.payment_terms = [] @@ -391,23 +437,29 @@ def get_payment_terms(self, row): self.append_payment_term(row, d, term) def append_payment_term(self, row, d, term): - if (self.filters.get("customer") or self.filters.get("supplier")) and d.currency == d.party_account_currency: + if ( + self.filters.get("customer") or self.filters.get("supplier") + ) and d.currency == d.party_account_currency: invoiced = d.payment_amount else: invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) - row.payment_terms.append(term.update({ - "due_date": d.due_date, - "invoiced": invoiced, - "invoice_grand_total": row.invoiced, - "payment_term": d.description, - "paid": d.paid_amount, - "credit_note": 0.0, - "outstanding": invoiced - d.paid_amount - })) + row.payment_terms.append( + term.update( + { + "due_date": d.due_date, + "invoiced": invoiced, + "invoice_grand_total": row.invoiced, + "payment_term": d.description, + "paid": d.paid_amount, + "credit_note": 0.0, + "outstanding": invoiced - d.paid_amount, + } + ) + ) if d.paid_amount: - row['paid'] -= d.paid_amount + row["paid"] -= d.paid_amount def allocate_closing_to_term(self, row, term, key): if row[key]: @@ -422,7 +474,7 @@ def allocate_closing_to_term(self, row, term, key): def allocate_extra_payments_or_credits(self, row): # allocate extra payments / credits additional_row = None - for key in ('paid', 'credit_note'): + for key in ("paid", "credit_note"): if row[key] > 0: if not additional_row: additional_row = frappe._dict(row) @@ -430,7 +482,9 @@ def allocate_extra_payments_or_credits(self, row): additional_row[key] = row[key] if additional_row: - additional_row.outstanding = additional_row.invoiced - additional_row.paid - additional_row.credit_note + additional_row.outstanding = ( + additional_row.invoiced - additional_row.paid - additional_row.credit_note + ) self.append_row(additional_row) def get_future_payments(self): @@ -444,7 +498,8 @@ def get_future_payments(self): self.future_payments.setdefault((d.invoice_no, d.party), []).append(d) def get_future_payments_from_payment_entry(self): - return frappe.db.sql(""" + return frappe.db.sql( + """ select ref.reference_name as invoice_no, payment_entry.party, @@ -460,22 +515,29 @@ def get_future_payments_from_payment_entry(self): payment_entry.docstatus < 2 and payment_entry.posting_date > %s and payment_entry.party_type = %s - """, (self.filters.report_date, self.party_type), as_dict=1) + """, + (self.filters.report_date, self.party_type), + as_dict=1, + ) def get_future_payments_from_journal_entry(self): - if self.filters.get('party'): - amount_field = ("jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.party_type == 'Supplier' else "jea.credit_in_account_currency - jea.debit_in_account_currency") + if self.filters.get("party"): + amount_field = ( + "jea.debit_in_account_currency - jea.credit_in_account_currency" + if self.party_type == "Supplier" + else "jea.credit_in_account_currency - jea.debit_in_account_currency" + ) else: - amount_field = ("jea.debit - " if self.party_type == 'Supplier' else "jea.credit") + amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit" - return frappe.db.sql(""" + return frappe.db.sql( + f""" select jea.reference_name as invoice_no, jea.party, jea.party_type, je.posting_date as future_date, - sum({0}) as future_amount, + sum({amount_field}) as future_amount, je.cheque_no as future_ref from `tabJournal Entry` as je inner join `tabJournal Entry Account` as jea @@ -489,7 +551,10 @@ def get_future_payments_from_journal_entry(self): and je.voucher_type != "Exchange Rate Revaluation" group by je.name, jea.reference_name having future_amount > 0 - """.format(amount_field), (self.filters.report_date, self.party_type), as_dict=1) + """, + (self.filters.report_date, self.party_type), + as_dict=1, + ) def allocate_future_payments(self, row): # future payments are captured in additional columns @@ -511,22 +576,21 @@ def allocate_future_payments(self, row): future.future_amount = 0 row.remaining_balance = row.outstanding - row.future_amount - row.setdefault('future_ref', []).append(cstr(future.future_ref) + '/' + cstr(future.future_date)) + row.setdefault("future_ref", []).append( + cstr(future.future_ref) + "/" + cstr(future.future_date) + ) if row.future_ref: - row.future_ref = ', '.join(row.future_ref) + row.future_ref = ", ".join(row.future_ref) def get_return_entries(self): doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" - filters={ - 'is_return': 1, - 'docstatus': 1 - } + filters = {"is_return": 1, "docstatus": 1} party_field = scrub(self.filters.party_type) if self.filters.get(party_field): filters.update({party_field: self.filters.get(party_field)}) self.return_entries = frappe._dict( - frappe.get_all(doctype, filters, ['name', 'return_against'], as_list=1) + frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1) ) def set_ageing(self, row): @@ -554,15 +618,23 @@ def get_ageing_data(self, entry_date, row): index = None if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4): - self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = 30, 60, 90, 120 - - for i, days in enumerate([self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]): + self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = ( + 30, + 60, + 90, + 120, + ) + + for i, days in enumerate( + [self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4] + ): if row.age <= days: index = i break - if index is None: index = 4 - row['range' + str(index+1)] = row.outstanding + if index is None: + index = 4 + row["range" + str(index + 1)] = row.outstanding def get_gl_entries(self): # get all the GL entries filtered by the given filters @@ -583,10 +655,11 @@ def get_gl_entries(self): else: select_fields = "debit, credit" - self.gl_entries = frappe.db.sql(""" + self.gl_entries = frappe.db.sql( + f""" select - name, posting_date, account, party_type, party, voucher_type, voucher_no, debit_in_account_currency, credit_in_account_currency, - against_voucher_type, against_voucher, account_currency, remarks, {0} + name, posting_date, account, party_type, party, voucher_type, voucher_no, debit_in_account_currency, credit_in_account_currency, + against_voucher_type, against_voucher, account_currency, remarks, {select_fields} from `tabGL Entry` where @@ -594,20 +667,25 @@ def get_gl_entries(self): and (against_voucher_type IS NULL OR against_voucher_type != "Exchange Rate Revaluation") and party_type=%s and (party is not null and party != '') - {1} {2} {3}""" - .format(select_fields, date_condition, conditions, order_by), values, as_dict=True) + {date_condition} {conditions} {order_by}""", + values, + as_dict=True, + ) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"]) - records = frappe.db.sql(""" + records = frappe.db.sql( + """ select distinct parent, parenttype from `tabSales Team` steam where parenttype in ('Customer', 'Sales Invoice') and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) - """, (lft, rgt), as_dict=1) + """, + (lft, rgt), + as_dict=1, + ) self.sales_person_records = frappe._dict() for d in records: @@ -620,17 +698,17 @@ def prepare_conditions(self): self.add_common_filters(conditions, values, party_type_field) - if party_type_field=="customer": + if party_type_field == "customer": self.add_customer_filters(conditions, values) - elif party_type_field=="supplier": + elif party_type_field == "supplier": self.add_supplier_filters(conditions, values) self.add_accounting_dimensions_filters(conditions, values) return " and ".join(conditions), values def get_order_by_condition(self): - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): return "order by party, posting_date" else: return "order by posting_date, party" @@ -650,17 +728,21 @@ def add_common_filters(self, conditions, values, party_type_field): # get GL with "receivable" or "payable" account_type account_type = "Receivable" if self.party_type == "Customer" else "Payable" - accounts = [d.name for d in frappe.get_all("Account", - filters={"account_type": account_type, "company": self.filters.company})] - conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) + accounts = [ + d.name + for d in frappe.get_all( + "Account", filters={"account_type": account_type, "company": self.filters.company} + ) + ] + conditions.append("account in ({})".format(",".join(["%s"] * len(accounts)))) values += accounts def add_customer_filters(self, conditions, values): if self.filters.get("customer_group"): - conditions.append(self.get_hierarchical_filters('Customer Group', 'customer_group')) + conditions.append(self.get_hierarchical_filters("Customer Group", "customer_group")) if self.filters.get("territory"): - conditions.append(self.get_hierarchical_filters('Territory', 'territory')) + conditions.append(self.get_hierarchical_filters("Territory", "territory")) if self.filters.get("payment_terms_template"): conditions.append("party in (select name from tabCustomer where payment_terms=%s)") @@ -683,10 +765,9 @@ def add_supplier_filters(self, conditions, values): def get_hierarchical_filters(self, doctype, key): lft, rgt = frappe.db.get_value(doctype, self.filters.get(key), ["lft", "rgt"]) - return """party in (select name from tabCustomer + return f"""party in (select name from tabCustomer where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt} - and name=tabCustomer.{key}))""".format( - doctype=doctype, lft=lft, rgt=rgt, key=key) + and name=tabCustomer.{key}))""" def add_accounting_dimensions_filters(self, conditions, values): accounting_dimensions = get_accounting_dimensions(as_list=False) @@ -694,10 +775,11 @@ def add_accounting_dimensions_filters(self, conditions, values): if accounting_dimensions: for dimension in accounting_dimensions: if self.filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - self.filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - self.filters.get(dimension.fieldname)) - conditions.append("{0} in %s".format(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + self.filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, self.filters.get(dimension.fieldname) + ) + conditions.append(f"{dimension.fieldname} in %s") values.append(tuple(self.filters.get(dimension.fieldname))) def get_gle_balance(self, gle): @@ -706,115 +788,152 @@ def get_gle_balance(self, gle): def get_reverse_balance(self, gle): # get "credit" balance if report type is "debit" and vice versa - return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') + return gle.get("debit" if self.dr_or_cr == "credit" else "credit") def is_invoice(self, gle): - if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): return True def get_party_details(self, party): - if not party in self.party_details: - if self.party_type == 'Customer': - self.party_details[party] = frappe.db.get_value('Customer', party, ['customer_name', - 'territory', 'customer_group', 'customer_primary_contact'], as_dict=True) + if party not in self.party_details: + if self.party_type == "Customer": + self.party_details[party] = frappe.db.get_value( + "Customer", + party, + ["customer_name", "territory", "customer_group", "customer_primary_contact"], + as_dict=True, + ) else: - self.party_details[party] = frappe.db.get_value('Supplier', party, ['supplier_name', - 'supplier_group'], as_dict=True) + self.party_details[party] = frappe.db.get_value( + "Supplier", party, ["supplier_name", "supplier_group"], as_dict=True + ) return self.party_details[party] - def get_columns(self): self.columns = [] - self.add_column('Posting Date', fieldtype='Date') - self.add_column(label=_(self.party_type), fieldname='party', - fieldtype='Link', options=self.party_type, width=180) + self.add_column("Posting Date", fieldtype="Date") + self.add_column( + label=_(self.party_type), fieldname="party", fieldtype="Link", options=self.party_type, width=180 + ) if self.party_naming_by == "Naming Series": - self.add_column(_('{0} Name').format(self.party_type), - fieldname = scrub(self.party_type) + '_name', fieldtype='Data') + self.add_column( + _("{0} Name").format(self.party_type), + fieldname=scrub(self.party_type) + "_name", + fieldtype="Data", + ) - if self.party_type == 'Customer': - self.add_column(_("Customer Contact"), fieldname='customer_primary_contact', - fieldtype='Link', options='Contact') - - self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data') - self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link', - options='voucher_type', width=180) - self.add_column(label='Due Date', fieldtype='Date') + if self.party_type == "Customer": + self.add_column( + _("Customer Contact"), + fieldname="customer_primary_contact", + fieldtype="Link", + options="Contact", + ) + + self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data") + self.add_column( + label=_("Voucher No"), + fieldname="voucher_no", + fieldtype="Dynamic Link", + options="voucher_type", + width=180, + ) + self.add_column(label="Due Date", fieldtype="Date") if self.party_type == "Supplier": - self.add_column(label=_('Bill No'), fieldname='bill_no', fieldtype='Data') - self.add_column(label=_('Bill Date'), fieldname='bill_date', fieldtype='Date') + self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") + self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date") if self.filters.based_on_payment_terms: - self.add_column(label=_('Payment Term'), fieldname='payment_term', fieldtype='Data') - self.add_column(label=_('Invoice Grand Total'), fieldname='invoice_grand_total') + self.add_column(label=_("Payment Term"), fieldname="payment_term", fieldtype="Data") + self.add_column(label=_("Invoice Grand Total"), fieldname="invoice_grand_total") - self.add_column(_('Invoiced Amount'), fieldname='invoiced') - self.add_column(_('Foreign Amount'), fieldname='foreign_amount', fieldtype='Float') - self.add_column(_('Foreign Currency'), fieldname='foreign_currency', fieldtype='Data') - self.add_column(_('Paid Amount'), fieldname='paid') + self.add_column(_("Invoiced Amount"), fieldname="invoiced") + self.add_column(_("Foreign Amount"), fieldname="foreign_amount", fieldtype="Float") + self.add_column(_("Foreign Currency"), fieldname="foreign_currency", fieldtype="Data") + self.add_column(_("Paid Amount"), fieldname="paid") if self.party_type == "Customer": - self.add_column(_('Credit Note'), fieldname='credit_note') + self.add_column(_("Credit Note"), fieldname="credit_note") else: # note: fieldname is still `credit_note` - self.add_column(_('Debit Note'), fieldname='credit_note') - self.add_column(_('Outstanding Amount'), fieldname='outstanding') + self.add_column(_("Debit Note"), fieldname="credit_note") + self.add_column(_("Outstanding Amount"), fieldname="outstanding") self.setup_ageing_columns() - self.add_column(label=_('Currency'), fieldname='currency', fieldtype='Link', options='Currency', width=80) + self.add_column( + label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80 + ) if self.filters.show_future_payments: - self.add_column(label=_('Future Payment Ref'), fieldname='future_ref', fieldtype='Data') - self.add_column(label=_('Future Payment Amount'), fieldname='future_amount') - self.add_column(label=_('Remaining Balance'), fieldname='remaining_balance') + self.add_column(label=_("Future Payment Ref"), fieldname="future_ref", fieldtype="Data") + self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") + self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.filters.party_type == 'Customer': - self.add_column(label=_('Customer LPO'), fieldname='po_no', fieldtype='Data') + if self.filters.party_type == "Customer": + self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data") # comma separated list of linked delivery notes if self.filters.show_delivery_notes: - self.add_column(label=_('Delivery Notes'), fieldname='delivery_notes', fieldtype='Data') - self.add_column(label=_('Territory'), fieldname='territory', fieldtype='Link', - options='Territory') - self.add_column(label=_('Customer Group'), fieldname='customer_group', fieldtype='Link', - options='Customer Group') + self.add_column(label=_("Delivery Notes"), fieldname="delivery_notes", fieldtype="Data") + self.add_column( + label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" + ) + self.add_column( + label=_("Customer Group"), + fieldname="customer_group", + fieldtype="Link", + options="Customer Group", + ) if self.filters.show_sales_person: - self.add_column(label=_('Sales Person'), fieldname='sales_person', fieldtype='Data') + self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") if self.filters.party_type == "Supplier": - self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', - options='Supplier Group') - - self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200) - - def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120): - if not fieldname: fieldname = scrub(label) - if fieldtype=='Currency': options='currency' - if fieldtype=='Date': width = 90 - - self.columns.append(dict( - label=label, - fieldname=fieldname, - fieldtype=fieldtype, - options=options, - width=width - )) + self.add_column( + label=_("Supplier Group"), + fieldname="supplier_group", + fieldtype="Link", + options="Supplier Group", + ) + + self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200) + + def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): + if not fieldname: + fieldname = scrub(label) + if fieldtype == "Currency": + options = "currency" + if fieldtype == "Date": + width = 90 + + self.columns.append( + dict(label=label, fieldname=fieldname, fieldtype=fieldtype, options=options, width=width) + ) def setup_ageing_columns(self): # for charts self.ageing_column_labels = [] - self.add_column(label=_('Age (Days)'), fieldname='age', fieldtype='Int', width=80) - - for i, label in enumerate(["0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), - "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), - "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), - "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): - self.add_column(label=label, fieldname='range' + str(i+1)) - self.ageing_column_labels.append(label) + self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80) + + for i, label in enumerate( + [ + "0-{range1}".format(range1=self.filters["range1"]), + "{range1}-{range2}".format( + range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"] + ), + "{range2}-{range3}".format( + range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"] + ), + "{range3}-{range4}".format( + range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] + ), + "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")), + ] + ): + self.add_column(label=label, fieldname="range" + str(i + 1)) + self.ageing_column_labels.append(label) def get_chart_data(self): rows = [] @@ -823,14 +942,6 @@ def get_chart_data(self): if not cint(row.bold): values = [row.range1, row.range2, row.range3, row.range4, row.range5] precision = cint(frappe.db.get_default("float_precision")) or 2 - rows.append({ - 'values': [flt(val, precision) for val in values] - }) - - self.chart = { - "data": { - 'labels': self.ageing_column_labels, - 'datasets': rows - }, - "type": 'percentage' - } + rows.append({"values": [flt(val, precision) for val in values]}) + + self.chart = {"data": {"labels": self.ageing_column_labels, "datasets": rows}, "type": "percentage"} diff --git a/csf_tz/csf_tz/report/accounts_receivable_summary_multi_currency/accounts_receivable_summary_multi_currency.py b/csf_tz/csf_tz/report/accounts_receivable_summary_multi_currency/accounts_receivable_summary_multi_currency.py index 6d27a226..1bafd3e8 100644 --- a/csf_tz/csf_tz/report/accounts_receivable_summary_multi_currency/accounts_receivable_summary_multi_currency.py +++ b/csf_tz/csf_tz/report/accounts_receivable_summary_multi_currency/accounts_receivable_summary_multi_currency.py @@ -1,49 +1,53 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe +from erpnext.accounts.party import get_partywise_advanced_payment_amount from frappe import _, scrub from frappe.utils import flt -from erpnext.accounts.party import get_partywise_advanced_payment_amount -from csf_tz.csf_tz.report.accounts_receivable_summary_multi_currency.accounts_receivable_utils import ReceivablePayableReport - from six import iteritems -from six.moves import zip + +from csf_tz.csf_tz.report.accounts_receivable_summary_multi_currency.accounts_receivable_utils import ( + ReceivablePayableReport, +) + class AccountsReceivableSummary(ReceivablePayableReport): def run(self, args): - party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + party_naming_by = frappe.db.get_single_value(*args.get("naming_by")) return self.get_columns(party_naming_by, args), self.get_data(party_naming_by, args) def get_columns(self, party_naming_by, args): columns = [_(args.get("party_type")) + ":Link/" + args.get("party_type") + ":200"] if party_naming_by == "Naming Series": - columns += [ args.get("party_type") + " Name::140"] - - credit_debit_label = "Credit Note Amt" if args.get('party_type') == 'Customer' else "Debit Note Amt" - - columns += [{ - "label": _("Advance Amount"), - "fieldname": "advance_amount", - "fieldtype": "Currency", - "options": "currency", - "width": 100 - },{ - "label": _("Total Invoiced Amt"), - "fieldname": "total_invoiced_amt", - "fieldtype": "Currency", - "options": "currency", - "width": 100 - }, - { - "label": _("Total Paid Amt"), - "fieldname": "total_paid_amt", - "fieldtype": "Currency", - "options": "currency", - "width": 100 - }] + columns += [args.get("party_type") + " Name::140"] + + credit_debit_label = "Credit Note Amt" if args.get("party_type") == "Customer" else "Debit Note Amt" + + columns += [ + { + "label": _("Advance Amount"), + "fieldname": "advance_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("Total Invoiced Amt"), + "fieldname": "total_invoiced_amt", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "label": _("Total Paid Amt"), + "fieldname": "total_paid_amt", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + ] columns += [ { @@ -51,90 +55,96 @@ def get_columns(self, party_naming_by, args): "fieldname": scrub(credit_debit_label), "fieldtype": "Currency", "options": "currency", - "width": 140 + "width": 140, }, { "label": _("Total Outstanding Amt"), "fieldname": "total_outstanding_amt", "fieldtype": "Currency", "options": "currency", - "width": 160 + "width": 160, }, { "label": _("0-" + str(self.filters.range1)), "fieldname": scrub("0-" + str(self.filters.range1)), "fieldtype": "Currency", "options": "currency", - "width": 160 + "width": 160, }, { "label": _(str(self.filters.range1) + "-" + str(self.filters.range2)), "fieldname": scrub(str(self.filters.range1) + "-" + str(self.filters.range2)), "fieldtype": "Currency", "options": "currency", - "width": 160 + "width": 160, }, { "label": _(str(self.filters.range2) + "-" + str(self.filters.range3)), "fieldname": scrub(str(self.filters.range2) + "-" + str(self.filters.range3)), "fieldtype": "Currency", "options": "currency", - "width": 160 + "width": 160, }, { "label": _(str(self.filters.range3) + "-" + str(self.filters.range4)), "fieldname": scrub(str(self.filters.range3) + "-" + str(self.filters.range4)), "fieldtype": "Currency", "options": "currency", - "width": 160 + "width": 160, }, { "label": _(str(self.filters.range4) + _("-Above")), "fieldname": scrub(str(self.filters.range4) + _("-Above")), "fieldtype": "Currency", "options": "currency", - "width": 160 - } + "width": 160, + }, ] if args.get("party_type") == "Customer": - columns += [{ - "label": _("Territory"), - "fieldname": "territory", - "fieldtype": "Link", - "options": "Territory", - "width": 80 - }, - { - "label": _("Customer Group"), - "fieldname": "customer_group", - "fieldtype": "Link", - "options": "Customer Group", - "width": 80 - }, - { - "label": _("Sales Person"), - "fieldtype": "Data", - "fieldname": "sales_person", - "width": 120, - }] + columns += [ + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 80, + }, + { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + "width": 80, + }, + { + "label": _("Sales Person"), + "fieldtype": "Data", + "fieldname": "sales_person", + "width": 120, + }, + ] if args.get("party_type") == "Supplier": - columns += [{ - "label": _("Supplier Group"), - "fieldname": "supplier_group", + columns += [ + { + "label": _("Supplier Group"), + "fieldname": "supplier_group", + "fieldtype": "Link", + "options": "Supplier Group", + "width": 80, + } + ] + + columns.append( + { + "fieldname": "currency", + "label": _("Currency"), "fieldtype": "Link", - "options": "Supplier Group", - "width": 80 - }] - - columns.append({ - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "width": 80 - }) + "options": "Currency", + "width": 80, + } + ) return columns @@ -143,8 +153,10 @@ def get_data(self, party_naming_by, args): partywise_total = self.get_partywise_total(party_naming_by, args) - partywise_advance_amount = get_partywise_advanced_payment_amount([args.get("party_type")], - self.filters.get("report_date")) or {} + partywise_advance_amount = ( + get_partywise_advanced_payment_amount([args.get("party_type")], self.filters.get("report_date")) + or {} + ) for party, party_dict in iteritems(partywise_total): row = [party] @@ -158,12 +170,23 @@ def get_data(self, party_naming_by, args): paid_amt = flt(party_dict.paid_amt - partywise_advance_amount.get(party, 0)) row += [ - party_dict.invoiced_amt, paid_amt, party_dict.credit_amt, party_dict.outstanding_amt, - party_dict.range1, party_dict.range2, party_dict.range3, party_dict.range4, party_dict.range5 + party_dict.invoiced_amt, + paid_amt, + party_dict.credit_amt, + party_dict.outstanding_amt, + party_dict.range1, + party_dict.range2, + party_dict.range3, + party_dict.range4, + party_dict.range5, ] if args.get("party_type") == "Customer": - row += [self.get_territory(party), self.get_customer_group(party), ", ".join(set(party_dict.sales_person))] + row += [ + self.get_territory(party), + self.get_customer_group(party), + ", ".join(set(party_dict.sales_person)), + ] if args.get("party_type") == "Supplier": row += [self.get_supplier_group(party)] @@ -175,19 +198,22 @@ def get_data(self, party_naming_by, args): def get_partywise_total(self, party_naming_by, args): party_total = frappe._dict() for d in self.get_voucherwise_data(party_naming_by, args): - party_total.setdefault(d.party, - frappe._dict({ - "invoiced_amt": 0, - "paid_amt": 0, - "credit_amt": 0, - "outstanding_amt": 0, - "range1": 0, - "range2": 0, - "range3": 0, - "range4": 0, - "range5": 0, - "sales_person": [] - }) + party_total.setdefault( + d.party, + frappe._dict( + { + "invoiced_amt": 0, + "paid_amt": 0, + "credit_amt": 0, + "outstanding_amt": 0, + "range1": 0, + "range2": 0, + "range3": 0, + "range4": 0, + "range5": 0, + "sales_person": [], + } + ), ) for k in list(party_total[d.party]): if k not in ["currency", "sales_person"]: @@ -208,7 +234,7 @@ def get_voucherwise_data(self, party_naming_by, args): if party_naming_by == "Naming Series": cols += ["party_name"] - if args.get("party_type") == 'Customer': + if args.get("party_type") == "Customer": cols += ["contact"] cols += ["voucher_type", "voucher_no", "due_date"] @@ -216,9 +242,22 @@ def get_voucherwise_data(self, party_naming_by, args): if args.get("party_type") == "Supplier": cols += ["bill_no", "bill_date"] - cols += ["invoiced_amt", "paid_amt", "credit_amt", - "outstanding_amt", "age", "range1", "range2", "range3", "range4", "range5", "currency", "pdc/lc_date", "pdc/lc_ref", - "pdc/lc_amount"] + cols += [ + "invoiced_amt", + "paid_amt", + "credit_amt", + "outstanding_amt", + "age", + "range1", + "range2", + "range3", + "range4", + "range5", + "currency", + "pdc/lc_date", + "pdc/lc_ref", + "pdc/lc_amount", + ] if args.get("party_type") == "Supplier": cols += ["supplier_group", "remarks"] @@ -231,10 +270,11 @@ def get_voucherwise_data(self, party_naming_by, args): def make_data_dict(cols, data): data_dict = [] for d in data: - data_dict.append(frappe._dict(zip(cols, d))) + data_dict.append(frappe._dict(zip(cols, d, strict=False))) return data_dict + def execute(filters=None): args = { "party_type": "Customer", diff --git a/csf_tz/csf_tz/report/accounts_receivable_summary_multi_currency/accounts_receivable_utils.py b/csf_tz/csf_tz/report/accounts_receivable_summary_multi_currency/accounts_receivable_utils.py index 36d0f895..cb40d9c0 100644 --- a/csf_tz/csf_tz/report/accounts_receivable_summary_multi_currency/accounts_receivable_utils.py +++ b/csf_tz/csf_tz/report/accounts_receivable_summary_multi_currency/accounts_receivable_utils.py @@ -1,21 +1,21 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe from frappe import _, scrub -from frappe.utils import getdate, nowdate, flt, cint, formatdate, cstr +from frappe.utils import cint, cstr, flt, formatdate, getdate, nowdate -class ReceivablePayableReport(object): + +class ReceivablePayableReport: def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.filters.report_date = getdate(self.filters.report_date or nowdate()) - self.age_as_on = getdate(nowdate()) \ - if self.filters.report_date > getdate(nowdate()) \ - else self.filters.report_date + self.age_as_on = ( + getdate(nowdate()) if self.filters.report_date > getdate(nowdate()) else self.filters.report_date + ) def run(self, args): - party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + party_naming_by = frappe.db.get_single_value(*args.get("naming_by")) columns = self.get_columns(party_naming_by, args) data = self.get_data(party_naming_by, args) chart = self.get_chart_data(columns, data) @@ -23,41 +23,39 @@ def run(self, args): def get_columns(self, party_naming_by, args): columns = [] - columns.append({ - "label": _("Posting Date"), - "fieldtype": "Date", - "fieldname": "posting_date", - "width": 90 - }) + columns.append( + {"label": _("Posting Date"), "fieldtype": "Date", "fieldname": "posting_date", "width": 90} + ) columns += [_(args.get("party_type")) + ":Link/" + args.get("party_type") + ":200"] - if args.get("party_type") == 'Customer': - columns.append({ - "label": _("Customer Contact"), - "fieldtype": "Link", - "fieldname": "contact", - "options":"Contact", - "width": 100 - }) + if args.get("party_type") == "Customer": + columns.append( + { + "label": _("Customer Contact"), + "fieldtype": "Link", + "fieldname": "contact", + "options": "Contact", + "width": 100, + } + ) if party_naming_by == "Naming Series": columns += [args.get("party_type") + " Name::110"] - columns.append({ - "label": _("Voucher Type"), - "fieldtype": "Data", - "fieldname": "voucher_type", - "width": 110 - }) - - columns.append({ - "label": _("Voucher No"), - "fieldtype": "Dynamic Link", - "fieldname": "voucher_no", - "width": 110, - "options": "voucher_type", - }) + columns.append( + {"label": _("Voucher Type"), "fieldtype": "Data", "fieldname": "voucher_type", "width": 110} + ) + + columns.append( + { + "label": _("Voucher No"), + "fieldtype": "Dynamic Link", + "fieldname": "voucher_no", + "width": 110, + "options": "voucher_type", + } + ) columns += [_("Due Date") + ":Date:80"] @@ -67,85 +65,92 @@ def get_columns(self, party_naming_by, args): credit_or_debit_note = "Credit Note" if args.get("party_type") == "Customer" else "Debit Note" if self.filters.based_on_payment_terms: - columns.append({ - "label": _("Payment Term"), - "fieldname": "payment_term", - "fieldtype": "Data", - "width": 120 - }) - columns.append({ - "label": _("Invoice Grand Total"), - "fieldname": "invoice_grand_total", - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }) + columns.append( + {"label": _("Payment Term"), "fieldname": "payment_term", "fieldtype": "Data", "width": 120} + ) + columns.append( + { + "label": _("Invoice Grand Total"), + "fieldname": "invoice_grand_total", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) for label in ("Invoiced Amount", "Paid Amount", credit_or_debit_note, "Outstanding Amount"): - columns.append({ - "label": _(label), - "fieldname": frappe.scrub(label), - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }) + columns.append( + { + "label": _(label), + "fieldname": frappe.scrub(label), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) columns += [_("Age (Days)") + ":Int:80"] self.ageing_col_idx_start = len(columns) - if not "range1" in self.filters: + if "range1" not in self.filters: self.filters["range1"] = "30" - if not "range2" in self.filters: + if "range2" not in self.filters: self.filters["range2"] = "60" - if not "range3" in self.filters: + if "range3" not in self.filters: self.filters["range3"] = "90" - if not "range4" in self.filters: + if "range4" not in self.filters: self.filters["range4"] = "120" - for label in ("0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), - "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), - "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), - "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))): - columns.append({ + for label in ( + "0-{range1}".format(range1=self.filters["range1"]), + "{range1}-{range2}".format( + range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"] + ), + "{range2}-{range3}".format( + range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"] + ), + "{range3}-{range4}".format( + range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] + ), + "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")), + ): + columns.append( + { "label": label, - "fieldname":label, + "fieldname": label, "fieldtype": "Currency", "options": "currency", - "width": 120 - }) + "width": 120, + } + ) columns += [ - { - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "width": 100 - }, - { - "fieldname": "pdc/lc_ref", - "label": _("PDC/LC Ref"), - "fieldtype": "Data", - "width": 110 - }, - { - "fieldname": "pdc/lc_amount", - "label": _("PDC/LC Amount"), - "fieldtype": "Currency", - "options": "currency", - "width": 130 - }, - { - "fieldname": "remaining_balance", - "label": _("Remaining Balance"), - "fieldtype": "Currency", - "options": "currency", - "width": 130 - }] - - if args.get('party_type') == 'Customer': + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "width": 100, + }, + {"fieldname": "pdc/lc_ref", "label": _("PDC/LC Ref"), "fieldtype": "Data", "width": 110}, + { + "fieldname": "pdc/lc_amount", + "label": _("PDC/LC Amount"), + "fieldtype": "Currency", + "options": "currency", + "width": 130, + }, + { + "fieldname": "remaining_balance", + "label": _("Remaining Balance"), + "fieldtype": "Currency", + "options": "currency", + "width": 130, + }, + ] + + if args.get("party_type") == "Customer": columns += [ { "label": _("Customer LPO"), @@ -161,7 +166,7 @@ def get_columns(self, party_naming_by, args): "fieldtype": "Data", "fieldname": "sales_person", "width": 120, - } + }, ] if args.get("party_type") == "Supplier": columns += [_("Supplier Group") + ":Link/Supplier Group:80"] @@ -172,15 +177,18 @@ def get_columns(self, party_naming_by, args): def get_data(self, party_naming_by, args): from erpnext.accounts.utils import get_currency_precision + self.currency_precision = get_currency_precision() or 2 self.dr_or_cr = "debit" if args.get("party_type") == "Customer" else "credit" future_vouchers = self.get_entries_after(self.filters.report_date, args.get("party_type")) if not self.filters.get("company"): - self.filters["company"] = frappe.db.get_single_value('Global Defaults', 'default_company') + self.filters["company"] = frappe.db.get_single_value("Global Defaults", "default_company") - self.company_currency = frappe.get_cached_value('Company', self.filters.get("company"), "default_currency") + self.company_currency = frappe.get_cached_value( + "Company", self.filters.get("company"), "default_currency" + ) return_entries = self.get_return_entries(args.get("party_type")) @@ -199,41 +207,60 @@ def get_data(self, party_naming_by, args): for gle in gl_entries_data: if self.is_receivable_or_payable(gle, self.dr_or_cr, future_vouchers, return_entries): outstanding_amount, credit_note_amount, payment_amount = self.get_outstanding_amount( - gle,self.filters.report_date, self.dr_or_cr, return_entries) + gle, self.filters.report_date, self.dr_or_cr, return_entries + ) temp_outstanding_amt = outstanding_amount temp_credit_note_amt = credit_note_amount - if abs(outstanding_amount) > 0.1/10**self.currency_precision: + if abs(outstanding_amount) > 0.1 / 10**self.currency_precision: if self.filters.based_on_payment_terms and self.payment_term_map.get(gle.voucher_no): for d in self.payment_term_map.get(gle.voucher_no): # Allocate payment amount based on payment terms(FIFO order) - payment_amount, d.payment_amount = self.allocate_based_on_fifo(payment_amount, d.payment_term_amount) + payment_amount, d.payment_amount = self.allocate_based_on_fifo( + payment_amount, d.payment_term_amount + ) term_outstanding_amount = d.payment_term_amount - d.payment_amount # Allocate credit note based on payment terms(FIFO order) - credit_note_amount, d.credit_note_amount = self.allocate_based_on_fifo(credit_note_amount, term_outstanding_amount) + credit_note_amount, d.credit_note_amount = self.allocate_based_on_fifo( + credit_note_amount, term_outstanding_amount + ) term_outstanding_amount -= d.credit_note_amount row_outstanding = term_outstanding_amount # Allocate PDC based on payment terms(FIFO order) - d.pdc_details, d.pdc_amount = self.allocate_pdc_amount_in_fifo(gle, row_outstanding) + d.pdc_details, d.pdc_amount = self.allocate_pdc_amount_in_fifo( + gle, row_outstanding + ) if term_outstanding_amount > 0: - row = self.prepare_row(party_naming_by, args, gle, term_outstanding_amount, - d.credit_note_amount, d.due_date, d.payment_amount , d.payment_term_amount, - d.description, d.pdc_amount, d.pdc_details) + row = self.prepare_row( + party_naming_by, + args, + gle, + term_outstanding_amount, + d.credit_note_amount, + d.due_date, + d.payment_amount, + d.payment_term_amount, + d.description, + d.pdc_amount, + d.pdc_details, + ) data.append(row) if credit_note_amount: - row = self.prepare_row_without_payment_terms(party_naming_by, args, gle, temp_outstanding_amt, - temp_credit_note_amt) + row = self.prepare_row_without_payment_terms( + party_naming_by, args, gle, temp_outstanding_amt, temp_credit_note_amt + ) data.append(row) else: - row = self.prepare_row_without_payment_terms(party_naming_by, args, gle, outstanding_amount, - credit_note_amount) + row = self.prepare_row_without_payment_terms( + party_naming_by, args, gle, outstanding_amount, credit_note_amount + ) data.append(row) return data @@ -259,7 +286,9 @@ def allocate_pdc_amount_in_fifo(self, gle, row_outstanding): return pdc_details, pdc_amount - def prepare_row_without_payment_terms(self, party_naming_by, args, gle, outstanding_amount, credit_note_amount): + def prepare_row_without_payment_terms( + self, party_naming_by, args, gle, outstanding_amount, credit_note_amount + ): pdc_list = self.pdc_details.get((gle.voucher_no, gle.party), []) pdc_amount = 0 pdc_details = [] @@ -268,12 +297,18 @@ def prepare_row_without_payment_terms(self, party_naming_by, args, gle, outstand if pdc_amount and d.pdc_ref and d.pdc_date: pdc_details.append(cstr(d.pdc_ref) + "/" + formatdate(d.pdc_date)) - row = self.prepare_row(party_naming_by, args, gle, outstanding_amount, - credit_note_amount, pdc_amount=pdc_amount, pdc_details=pdc_details) + row = self.prepare_row( + party_naming_by, + args, + gle, + outstanding_amount, + credit_note_amount, + pdc_amount=pdc_amount, + pdc_details=pdc_details, + ) return row - @staticmethod def allocate_based_on_fifo(total_amount, row_amount): allocated_amount = 0 @@ -286,15 +321,27 @@ def allocate_based_on_fifo(total_amount, row_amount): return total_amount, allocated_amount - def prepare_row(self, party_naming_by, args, gle, outstanding_amount, credit_note_amount, - due_date=None, paid_amt=None, payment_term_amount=None, payment_term=None, pdc_amount=None, pdc_details=None): + def prepare_row( + self, + party_naming_by, + args, + gle, + outstanding_amount, + credit_note_amount, + due_date=None, + paid_amt=None, + payment_term_amount=None, + payment_term=None, + pdc_amount=None, + pdc_details=None, + ): row = [gle.posting_date, gle.party] # customer / supplier name if party_naming_by == "Naming Series": row += [self.get_party_name(gle.party_type, gle.party)] - if args.get("party_type") == 'Customer': + if args.get("party_type") == "Customer": row += [self.get_customer_contact(gle.party_type, gle.party)] # get due date @@ -308,14 +355,14 @@ def prepare_row(self, party_naming_by, args, gle, outstanding_amount, credit_not if args.get("party_type") == "Supplier": row += [ self.voucher_details.get(gle.voucher_no, {}).get("bill_no", ""), - self.voucher_details.get(gle.voucher_no, {}).get("bill_date", "") + self.voucher_details.get(gle.voucher_no, {}).get("bill_date", ""), ] # invoiced and paid amounts invoiced_amount = gle.get(self.dr_or_cr) if (gle.get(self.dr_or_cr) > 0) else 0 if self.filters.based_on_payment_terms: - row+=[payment_term, invoiced_amount] + row += [payment_term, invoiced_amount] if payment_term_amount: invoiced_amount = payment_term_amount @@ -331,18 +378,26 @@ def prepare_row(self, party_naming_by, args, gle, outstanding_amount, credit_not else: entry_date = gle.posting_date - row += get_ageing_data(cint(self.filters.range1), cint(self.filters.range2), - cint(self.filters.range3), cint(self.filters.range4), self.age_as_on, entry_date, outstanding_amount) + row += get_ageing_data( + cint(self.filters.range1), + cint(self.filters.range2), + cint(self.filters.range3), + cint(self.filters.range4), + self.age_as_on, + entry_date, + outstanding_amount, + ) # issue 6371-Ageing buckets should not have amounts if due date is not reached - if self.filters.ageing_based_on == "Due Date" \ - and getdate(due_date) > getdate(self.filters.report_date): - row[-1]=row[-2]=row[-3]=row[-4]=row[-5]=0 - - if self.filters.ageing_based_on == "Supplier Invoice Date" \ - and getdate(bill_date) > getdate(self.filters.report_date): + if self.filters.ageing_based_on == "Due Date" and getdate(due_date) > getdate( + self.filters.report_date + ): + row[-1] = row[-2] = row[-3] = row[-4] = row[-5] = 0 - row[-1]=row[-2]=row[-3]=row[-4]=row[-5]=0 + if self.filters.ageing_based_on == "Supplier Invoice Date" and getdate(bill_date) > getdate( + self.filters.report_date + ): + row[-1] = row[-2] = row[-3] = row[-4] = row[-5] = 0 if self.filters.get(scrub(args.get("party_type"))): row.append(gle.account_currency) @@ -353,7 +408,7 @@ def prepare_row(self, party_naming_by, args, gle, outstanding_amount, credit_not pdc_details = ", ".join(pdc_details) row += [pdc_details, pdc_amount, remaining_balance] - if args.get('party_type') == 'Customer': + if args.get("party_type") == "Customer": # customer LPO row += [self.voucher_details.get(gle.voucher_no, {}).get("po_no")] @@ -362,8 +417,11 @@ def prepare_row(self, party_naming_by, args, gle, outstanding_amount, credit_not # customer territory / supplier group if args.get("party_type") == "Customer": - row += [self.get_territory(gle.party), self.get_customer_group(gle.party), - self.voucher_details.get(gle.voucher_no, {}).get("sales_person")] + row += [ + self.get_territory(gle.party), + self.get_customer_group(gle.party), + self.voucher_details.get(gle.voucher_no, {}).get("sales_person"), + ] if args.get("party_type") == "Supplier": row += [self.get_supplier_group(gle.party)] @@ -373,7 +431,14 @@ def prepare_row(self, party_naming_by, args, gle, outstanding_amount, credit_not def get_entries_after(self, report_date, party_type): # returns a distinct list - return list(set([(e.voucher_type, e.voucher_no) for e in self.get_gl_entries(party_type, report_date, for_future=True)])) + return list( + set( + [ + (e.voucher_type, e.voucher_no) + for e in self.get_gl_entries(party_type, report_date, for_future=True) + ] + ) + ) def get_entries_till(self, report_date, party_type): # returns a generator @@ -383,52 +448,74 @@ def get_entries_till(self, report_date, party_type): def is_receivable_or_payable(gle, dr_or_cr, future_vouchers, return_entries): return ( # advance - (not gle.against_voucher) or - + (not gle.against_voucher) + or # against sales order/purchase order - (gle.against_voucher_type in ["Sales Order", "Purchase Order"]) or - + (gle.against_voucher_type in ["Sales Order", "Purchase Order"]) + or # sales invoice/purchase invoice - (gle.against_voucher==gle.voucher_no and gle.get(dr_or_cr) > 0) or - + (gle.against_voucher == gle.voucher_no and gle.get(dr_or_cr) > 0) + or # standalone credit notes - (gle.against_voucher==gle.voucher_no and gle.voucher_no in return_entries and not return_entries.get(gle.voucher_no)) or - + ( + gle.against_voucher == gle.voucher_no + and gle.voucher_no in return_entries + and not return_entries.get(gle.voucher_no) + ) + or # entries adjusted with future vouchers ((gle.against_voucher_type, gle.against_voucher) in future_vouchers) ) @staticmethod def get_return_entries(party_type): - doctype = "Sales Invoice" if party_type=="Customer" else "Purchase Invoice" - return_entries = frappe._dict(frappe.get_all(doctype, - filters={"is_return": 1, "docstatus": 1}, fields=["name", "return_against"], as_list=1)) + doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" + return_entries = frappe._dict( + frappe.get_all( + doctype, + filters={"is_return": 1, "docstatus": 1}, + fields=["name", "return_against"], + as_list=1, + ) + ) return return_entries def get_outstanding_amount(self, gle, report_date, dr_or_cr, return_entries): payment_amount, credit_note_amount = 0.0, 0.0 - reverse_dr_or_cr = "credit" if dr_or_cr=="debit" else "debit" + reverse_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" for e in self.get_gl_entries_for(gle.party, gle.party_type, gle.voucher_type, gle.voucher_no): - if getdate(e.posting_date) <= report_date \ - and (e.name!=gle.name or (e.voucher_no in return_entries and not return_entries.get(e.voucher_no))): - - amount = flt(e.get(reverse_dr_or_cr), self.currency_precision) - flt(e.get(dr_or_cr), self.currency_precision) + if getdate(e.posting_date) <= report_date and ( + e.name != gle.name + or (e.voucher_no in return_entries and not return_entries.get(e.voucher_no)) + ): + amount = flt(e.get(reverse_dr_or_cr), self.currency_precision) - flt( + e.get(dr_or_cr), self.currency_precision + ) if e.voucher_no not in return_entries: payment_amount += amount else: credit_note_amount += amount - voucher_amount = flt(gle.get(dr_or_cr), self.currency_precision) - flt(gle.get(reverse_dr_or_cr), self.currency_precision) + voucher_amount = flt(gle.get(dr_or_cr), self.currency_precision) - flt( + gle.get(reverse_dr_or_cr), self.currency_precision + ) if gle.voucher_no in return_entries and not return_entries.get(gle.voucher_no): voucher_amount = 0 - outstanding_amount = flt((voucher_amount - payment_amount - credit_note_amount), self.currency_precision) + outstanding_amount = flt( + (voucher_amount - payment_amount - credit_note_amount), self.currency_precision + ) credit_note_amount = flt(credit_note_amount, self.currency_precision) return outstanding_amount, credit_note_amount, payment_amount def get_party_name(self, party_type, party_name): - return self.get_party_map(party_type).get(party_name, {}).get("customer_name" if party_type == "Customer" else "supplier_name") or "" + return ( + self.get_party_map(party_type) + .get(party_name, {}) + .get("customer_name" if party_type == "Customer" else "supplier_name") + or "" + ) def get_customer_contact(self, party_type, party_name): return self.get_party_map(party_type).get(party_name, {}).get("customer_primary_contact") @@ -449,8 +536,10 @@ def get_party_map(self, party_type): elif party_type == "Supplier": select_fields = "name, supplier_name, supplier_group" - self.party_map = dict(((r.name, r) for r in frappe.db.sql("select {0} from `tab{1}`" - .format(select_fields, party_type), as_dict=True))) + self.party_map = dict( + (r.name, r) + for r in frappe.db.sql(f"select {select_fields} from `tab{party_type}`", as_dict=True) + ) return self.party_map @@ -458,27 +547,32 @@ def get_gl_entries(self, party_type, date=None, for_future=False): conditions, values = self.prepare_conditions(party_type) if self.filters.get(scrub(party_type)): - select_fields = "sum(debit_in_account_currency) as debit, sum(credit_in_account_currency) as credit" + select_fields = ( + "sum(debit_in_account_currency) as debit, sum(credit_in_account_currency) as credit" + ) else: select_fields = "sum(debit) as debit, sum(credit) as credit" if date and not for_future: - conditions += " and posting_date <= '%s'" % date + conditions += f" and posting_date <= '{date}'" if date and for_future: - conditions += " and posting_date > '%s'" % date + conditions += f" and posting_date > '{date}'" - self.gl_entries = frappe.db.sql(""" + self.gl_entries = frappe.db.sql( + f""" select name, posting_date, account, party_type, party, voucher_type, voucher_no, - against_voucher_type, against_voucher, account_currency, remarks, {0} + against_voucher_type, against_voucher, account_currency, remarks, {select_fields} from `tabGL Entry` where - docstatus < 2 and party_type=%s and (party is not null and party != '') {1} + docstatus < 2 and party_type=%s and (party is not null and party != '') {conditions} group by voucher_type, voucher_no, against_voucher_type, against_voucher, party - order by posting_date, party""" - .format(select_fields, conditions), values, as_dict=True) + order by posting_date, party""", + values, + as_dict=True, + ) return self.gl_entries @@ -500,23 +594,23 @@ def prepare_conditions(self, party_type): conditions.append("party=%s") values.append(self.filters.get(party_type_field)) - if party_type_field=="customer": + if party_type_field == "customer": account_type = "Receivable" if self.filters.get("customer_group"): - lft, rgt = frappe.db.get_value("Customer Group", - self.filters.get("customer_group"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value( + "Customer Group", self.filters.get("customer_group"), ["lft", "rgt"] + ) - conditions.append("""party in (select name from tabCustomer - where exists(select name from `tabCustomer Group` where lft >= {0} and rgt <= {1} - and name=tabCustomer.customer_group))""".format(lft, rgt)) + conditions.append(f"""party in (select name from tabCustomer + where exists(select name from `tabCustomer Group` where lft >= {lft} and rgt <= {rgt} + and name=tabCustomer.customer_group))""") if self.filters.get("territory"): - lft, rgt = frappe.db.get_value("Territory", - self.filters.get("territory"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value("Territory", self.filters.get("territory"), ["lft", "rgt"]) - conditions.append("""party in (select name from tabCustomer - where exists(select name from `tabTerritory` where lft >= {0} and rgt <= {1} - and name=tabCustomer.territory))""".format(lft, rgt)) + conditions.append(f"""party in (select name from tabCustomer + where exists(select name from `tabTerritory` where lft >= {lft} and rgt <= {rgt} + and name=tabCustomer.territory))""") if self.filters.get("payment_terms_template"): conditions.append("party in (select name from tabCustomer where payment_terms=%s)") @@ -527,25 +621,30 @@ def prepare_conditions(self, party_type): values.append(self.filters.get("sales_partner")) if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value( + "Sales Person", self.filters.get("sales_person"), ["lft", "rgt"] + ) - conditions.append("""exists(select name from `tabSales Team` steam where - steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) + conditions.append(f"""exists(select name from `tabSales Team` steam where + steam.sales_person in (select name from `tabSales Person` where lft >= {lft} and rgt <= {rgt}) and ((steam.parent = voucher_no and steam.parenttype = voucher_type) or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) + or (steam.parent = party and steam.parenttype = 'Customer')))""") - elif party_type_field=="supplier": + elif party_type_field == "supplier": account_type = "Payable" if self.filters.get("supplier_group"): conditions.append("""party in (select name from tabSupplier where supplier_group=%s)""") values.append(self.filters.get("supplier_group")) - accounts = [d.name for d in frappe.get_all("Account", - filters={"account_type": account_type, "company": self.filters.company})] - conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) + accounts = [ + d.name + for d in frappe.get_all( + "Account", filters={"account_type": account_type, "company": self.filters.company} + ) + ] + conditions.append("account in ({})".format(",".join(["%s"] * len(accounts)))) values += accounts return " and ".join(conditions), values @@ -555,56 +654,54 @@ def get_gl_entries_for(self, party, party_type, against_voucher_type, against_vo self.gl_entries_map = {} for gle in self.get_gl_entries(party_type): if gle.against_voucher_type and gle.against_voucher: - self.gl_entries_map.setdefault(gle.party, {})\ - .setdefault(gle.against_voucher_type, {})\ - .setdefault(gle.against_voucher, [])\ - .append(gle) + self.gl_entries_map.setdefault(gle.party, {}).setdefault( + gle.against_voucher_type, {} + ).setdefault(gle.against_voucher, []).append(gle) - return self.gl_entries_map.get(party, {})\ - .get(against_voucher_type, {})\ - .get(against_voucher, []) + return self.gl_entries_map.get(party, {}).get(against_voucher_type, {}).get(against_voucher, []) def get_payment_term_detail(self, voucher_nos): payment_term_map = frappe._dict() - payment_terms_details = frappe.db.sql(""" select si.name, + payment_terms_details = frappe.db.sql( + """ select si.name, party_account_currency, currency, si.conversion_rate, ps.due_date, ps.payment_amount, ps.description from `tabSales Invoice` si, `tabPayment Schedule` ps where si.name = ps.parent and - si.docstatus = 1 and si.company = '%s' and - si.name in (%s) order by ps.due_date - """ % (frappe.db.escape(self.filters.company), ','.join(['%s'] *len(voucher_nos))), - (tuple(voucher_nos)), as_dict = 1) + si.docstatus = 1 and si.company = '{}' and + si.name in ({}) order by ps.due_date + """.format(frappe.db.escape(self.filters.company), ",".join(["%s"] * len(voucher_nos))), + (tuple(voucher_nos)), + as_dict=1, + ) for d in payment_terms_details: payment_term_amount = d.payment_amount - payment_term_map.setdefault(d.name, []).append(frappe._dict({ - "due_date": d.due_date, - "payment_term_amount": payment_term_amount, - "description": d.description - })) + payment_term_map.setdefault(d.name, []).append( + frappe._dict( + { + "due_date": d.due_date, + "payment_term_amount": payment_term_amount, + "description": d.description, + } + ) + ) return payment_term_map def get_chart_data(self, columns, data): - ageing_columns = columns[self.ageing_col_idx_start : self.ageing_col_idx_start+5] + ageing_columns = columns[self.ageing_col_idx_start : self.ageing_col_idx_start + 5] rows = [] for d in data: - rows.append( - { - 'values': d[self.ageing_col_idx_start : self.ageing_col_idx_start+5] - } - ) + rows.append({"values": d[self.ageing_col_idx_start : self.ageing_col_idx_start + 5]}) return { - "data": { - 'labels': [d.get("label") for d in ageing_columns], - 'datasets': rows - }, - "type": 'percentage' + "data": {"labels": [d.get("label") for d in ageing_columns], "datasets": rows}, + "type": "percentage", } + def execute(filters=None): args = { "party_type": "Customer", @@ -612,8 +709,10 @@ def execute(filters=None): } return ReceivablePayableReport(filters).run(args) -def get_ageing_data(first_range, second_range, third_range, - fourth_range, age_as_on, entry_date, outstanding_amount): + +def get_ageing_data( + first_range, second_range, third_range, fourth_range, age_as_on, entry_date, outstanding_amount +): # [0-30, 30-60, 60-90, 90-120, 120-above] outstanding_range = [0.0, 0.0, 0.0, 0.0, 0.0] @@ -627,14 +726,17 @@ def get_ageing_data(first_range, second_range, third_range, index = i break - if index is None: index = 4 + if index is None: + index = 4 outstanding_range[index] = outstanding_amount return [age] + outstanding_range + def get_pdc_details(party_type, report_date): pdc_details = frappe._dict() - pdc_via_pe = frappe.db.sql(""" + pdc_via_pe = frappe.db.sql( + """ select pref.reference_name as invoice_no, pent.party, pent.party_type, pent.posting_date as pdc_date, ifnull(pref.allocated_amount,0) as pdc_amount, @@ -646,21 +748,26 @@ def get_pdc_details(party_type, report_date): where pent.docstatus < 2 and pent.posting_date > %s and pent.party_type = %s - """, (report_date, party_type), as_dict=1) + """, + (report_date, party_type), + as_dict=1, + ) for pdc in pdc_via_pe: - pdc_details.setdefault((pdc.invoice_no, pdc.party), []).append(pdc) + pdc_details.setdefault((pdc.invoice_no, pdc.party), []).append(pdc) if scrub(party_type): - amount_field = ("jea.debit_in_account_currency" - if party_type == 'Supplier' else "jea.credit_in_account_currency") + amount_field = ( + "jea.debit_in_account_currency" if party_type == "Supplier" else "jea.credit_in_account_currency" + ) else: amount_field = "jea.debit + jea.credit" - pdc_via_je = frappe.db.sql(""" + pdc_via_je = frappe.db.sql( + f""" select jea.reference_name as invoice_no, jea.party, jea.party_type, - je.posting_date as pdc_date, ifnull({0},0) as pdc_amount, + je.posting_date as pdc_date, ifnull({amount_field},0) as pdc_amount, je.cheque_no as pdc_ref from `tabJournal Entry` as je inner join `tabJournal Entry Account` as jea @@ -669,68 +776,93 @@ def get_pdc_details(party_type, report_date): where je.docstatus < 2 and je.posting_date > %s and jea.party_type = %s - """.format(amount_field), (report_date, party_type), as_dict=1) + """, + (report_date, party_type), + as_dict=1, + ) for pdc in pdc_via_je: pdc_details.setdefault((pdc.invoice_no, pdc.party), []).append(pdc) return pdc_details + def get_dn_details(party_type, voucher_nos): dn_details = frappe._dict() if party_type == "Customer": - for si in frappe.db.sql(""" + for si in frappe.db.sql( + """ select parent, GROUP_CONCAT(delivery_note SEPARATOR ', ') as dn from `tabSales Invoice Item` where docstatus=1 and delivery_note is not null and delivery_note != '' - and parent in (%s) group by parent - """ %(','.join(['%s'] * len(voucher_nos))), tuple(voucher_nos) , as_dict=1): + and parent in ({}) group by parent + """.format(",".join(["%s"] * len(voucher_nos))), + tuple(voucher_nos), + as_dict=1, + ): dn_details.setdefault(si.parent, si.dn) - for si in frappe.db.sql(""" + for si in frappe.db.sql( + """ select against_sales_invoice as parent, GROUP_CONCAT(parent SEPARATOR ', ') as dn from `tabDelivery Note Item` where docstatus=1 and against_sales_invoice is not null and against_sales_invoice != '' - and against_sales_invoice in (%s) + and against_sales_invoice in ({}) group by against_sales_invoice - """ %(','.join(['%s'] * len(voucher_nos))), tuple(voucher_nos) , as_dict=1): + """.format(",".join(["%s"] * len(voucher_nos))), + tuple(voucher_nos), + as_dict=1, + ): if si.parent in dn_details: - dn_details[si.parent] += ', %s' %(si.dn) + dn_details[si.parent] += f", {si.dn}" else: dn_details.setdefault(si.parent, si.dn) return dn_details + def get_voucher_details(party_type, voucher_nos, dn_details): voucher_details = frappe._dict() if party_type == "Customer": - for si in frappe.db.sql(""" + for si in frappe.db.sql( + """ select inv.name, inv.due_date, inv.po_no, GROUP_CONCAT(steam.sales_person SEPARATOR ', ') as sales_person from `tabSales Invoice` inv left join `tabSales Team` steam on steam.parent = inv.name and steam.parenttype = 'Sales Invoice' - where inv.docstatus=1 and inv.name in (%s) + where inv.docstatus=1 and inv.name in ({}) group by inv.name - """ %(','.join(['%s'] *len(voucher_nos))), (tuple(voucher_nos)), as_dict=1): - si['delivery_note'] = dn_details.get(si.name) - voucher_details.setdefault(si.name, si) + """.format(",".join(["%s"] * len(voucher_nos))), + (tuple(voucher_nos)), + as_dict=1, + ): + si["delivery_note"] = dn_details.get(si.name) + voucher_details.setdefault(si.name, si) if party_type == "Supplier": - for pi in frappe.db.sql("""select name, due_date, bill_no, bill_date - from `tabPurchase Invoice` where docstatus = 1 and name in (%s) - """ %(','.join(['%s'] *len(voucher_nos))), (tuple(voucher_nos)), as_dict=1): + for pi in frappe.db.sql( + """select name, due_date, bill_no, bill_date + from `tabPurchase Invoice` where docstatus = 1 and name in ({}) + """.format(",".join(["%s"] * len(voucher_nos))), + (tuple(voucher_nos)), + as_dict=1, + ): voucher_details.setdefault(pi.name, pi) - for pi in frappe.db.sql("""select name, due_date, bill_no, bill_date from - `tabJournal Entry` where docstatus = 1 and bill_no is not NULL and name in (%s) - """ %(','.join(['%s'] *len(voucher_nos))), (tuple(voucher_nos)), as_dict=1): - voucher_details.setdefault(pi.name, pi) + for pi in frappe.db.sql( + """select name, due_date, bill_no, bill_date from + `tabJournal Entry` where docstatus = 1 and bill_no is not NULL and name in ({}) + """.format(",".join(["%s"] * len(voucher_nos))), + (tuple(voucher_nos)), + as_dict=1, + ): + voucher_details.setdefault(pi.name, pi) return voucher_details diff --git a/csf_tz/csf_tz/report/av_sales_invoice_trend/av_sales_invoice_trend.py b/csf_tz/csf_tz/report/av_sales_invoice_trend/av_sales_invoice_trend.py index f1669aea..788a8dda 100644 --- a/csf_tz/csf_tz/report/av_sales_invoice_trend/av_sales_invoice_trend.py +++ b/csf_tz/csf_tz/report/av_sales_invoice_trend/av_sales_invoice_trend.py @@ -2,6 +2,7 @@ from erpnext.controllers.trends import get_columns, get_data from frappe.utils import flt + def execute(filters=None): filters = filters or {} @@ -10,20 +11,29 @@ def execute(filters=None): columns, data = result["columns"], get_data(filters, result) # Find the index of the item_code column - item_idx = next((i for i, col in enumerate(columns) - if isinstance(col, dict) and col.get("fieldname") in ("item_code", "item")), 0) + item_idx = next( + ( + i + for i, col in enumerate(columns) + if isinstance(col, dict) and col.get("fieldname") in ("item_code", "item") + ), + 0, + ) # Fetch Bin summary with formatted qty bin_map = { d.item_code: [flt(d.total_qty, 2), d.warehouse_summary] - for d in frappe.db.sql(""" + for d in frappe.db.sql( + """ SELECT item_code, SUM(actual_qty) AS total_qty, GROUP_CONCAT(CONCAT(warehouse, ": ", FORMAT(actual_qty, 2)) SEPARATOR ", ") AS warehouse_summary FROM `tabBin` GROUP BY item_code - """, as_dict=True) + """, + as_dict=True, + ) } # Add Bin info to each row @@ -36,13 +46,9 @@ def execute(filters=None): "label": "Total Available Qty", "fieldname": "total_available_qty", "fieldtype": "Float", - "precision": 2 - }, - { - "label": "Warehouse", - "fieldname": "warehouse", - "fieldtype": "Data" + "precision": 2, }, + {"label": "Warehouse", "fieldname": "warehouse", "fieldtype": "Data"}, ] return columns, data diff --git a/csf_tz/csf_tz/report/credit_note_list/credit_note_list.html b/csf_tz/csf_tz/report/credit_note_list/credit_note_list.html index 59fe94fb..61b5550a 100644 --- a/csf_tz/csf_tz/report/credit_note_list/credit_note_list.html +++ b/csf_tz/csf_tz/report/credit_note_list/credit_note_list.html @@ -3,7 +3,7 @@ padding-left: 15mm !important;; padding-right: 15mm !important;; padding-top: 0mm; - font-size: 10pt; + font-size: 10pt; } .print-format td, .print-format th { vertical-align: top !important; @@ -19,7 +19,7 @@ width: 8.5in; height: 11in; } - + .print-format td, .print-format th { vertical-align: top !important; padding: 3px !important; @@ -28,8 +28,8 @@ -{% - var from_date = filters.from_date; +{% + var from_date = filters.from_date; var to_date = filters.to_date; %} @@ -44,7 +44,7 @@ ILALA Tax Region, P.O.Box – 25216, Dar es Salaam, -Tanzania +Tanzania Dear Sir, SUB: Issue Credit Note during the month of JULY, 2018. diff --git a/csf_tz/csf_tz/report/credit_note_list/credit_note_list.js b/csf_tz/csf_tz/report/credit_note_list/credit_note_list.js index 8bae38c0..fd96cfcd 100644 --- a/csf_tz/csf_tz/report/credit_note_list/credit_note_list.js +++ b/csf_tz/csf_tz/report/credit_note_list/credit_note_list.js @@ -25,4 +25,4 @@ frappe.query_reports["Credit Note List"] = { // $(wrapper).bind("show", function() { // frappe.query_report.load(); // }); -// }); \ No newline at end of file +// }); diff --git a/csf_tz/csf_tz/report/csf_tz_stock_movement/csf_tz_stock_movement.py b/csf_tz/csf_tz/report/csf_tz_stock_movement/csf_tz_stock_movement.py index 33748867..ced7924a 100644 --- a/csf_tz/csf_tz/report/csf_tz_stock_movement/csf_tz_stock_movement.py +++ b/csf_tz/csf_tz/report/csf_tz_stock_movement/csf_tz_stock_movement.py @@ -1,519 +1,467 @@ # Copyright (c) 2023, Aakvatech and contributors # For license information, please see license.txt +from itertools import groupby +from operator import itemgetter + import frappe +from erpnext.stock.utils import is_reposting_item_valuation_in_progress from frappe import _ from frappe.query_builder.functions import CombineDatetime from frappe.utils import flt -from erpnext.stock.utils import is_reposting_item_valuation_in_progress -from itertools import groupby -from operator import itemgetter -import math def execute(filters=None): - is_reposting_item_valuation_in_progress() - columns = get_columns(filters) - items = get_items(filters) - sl_entries = get_stock_ledger_entries(filters, items) - item_details = get_item_details(items, sl_entries) - - opening_balance_item_wise = [] - for row in sl_entries: - item_detail = item_details[row.item_code] - row.update(item_detail) - - row["opening_qty"] = row["opening_value"] = row["reconciliation_qty"] = row[ - "reconciliation_value" - ] = row["purchase_qty"] = row["purchase_value"] = row["sold_qty"] = row[ - "sold_value" - ] = row[ - "adjustment_qty" - ] = row[ - "adjustment_value" - ] = row[ - "consumed_qty" - ] = row[ - "consumed_value" - ] = row[ - "produced_qty" - ] = row[ - "produced_value" - ] = row[ - "received_qty" - ] = row[ - "received_value" - ] = row[ - "issued_qty" - ] = row[ - "issued_value" - ] = 0 - - if not any( - record["item_code"] == row.item_code - and record["warehouse"] == row.warehouse - for record in opening_balance_item_wise - ): - opening_balance_item_wise.append( - {"item_code": row.item_code, "warehouse": row.warehouse} - ) - opening_row = get_opening_balance( - filters, row.item_code, row.warehouse, sl_entries - ) - - if opening_row: - row["opening_qty"] = opening_row["qty_after_transaction"] - row["opening_value"] = ( - flt(row["opening_qty"]) * opening_row["valuation_rate"] - ) - - if row.voucher_type == "Stock Reconciliation": - row["reconciliation_qty"] = row.stock_value_difference / ( - row.valuation_rate or 1 - ) - - row["reconciliation_value"] = row.stock_value_difference - - if ( - row.voucher_type == "Purchase Receipt" - or row.voucher_type == "Purchase Invoice" - ): - row["purchase_qty"] = row.actual_qty - row["purchase_value"] = row.stock_value_difference - - if row.voucher_type == "Delivery Note" or row.voucher_type == "Sales Invoice": - row["sold_qty"] = row.actual_qty - row["sold_value"] = row.stock_value_difference - - if row.voucher_type == "Stock Entry": - row["adjustment_qty"] = row.actual_qty - row["adjustment_value"] = row.stock_value_difference - - stock_entry_type = frappe.db.get_value( - row.voucher_type, row.voucher_no, "stock_entry_type" - ) - if stock_entry_type == "Material Transfer for Manufacture": - row["consumed_qty"] = row.actual_qty - row["consumed_value"] = row.stock_value_difference - - if stock_entry_type == "Manufacture": - row["produced_qty"] = row.actual_qty - row["produced_value"] = row.stock_value_difference - - if stock_entry_type == "Material Receipt": - row["received_qty"] = row.actual_qty - row["received_value"] = row.stock_value_difference - - if stock_entry_type == "Material Issue": - row["issued_qty"] = row.actual_qty - row["issued_value"] = row.stock_value_difference - - row["closing_qty"] = ( - ((row.opening_qty or 0) + (row.purchase_qty or 0)) - + (row.sold_qty or 0) - + ( - (row.adjustment_qty or 0) - + ( - (row.consumed_qty or 0) - + (row.produced_qty or 0) - + (row.received_qty or 0) - + (row.issued_qty or 0) - ) - ) - + (row.reconciliation_qty or 0) - ) - - row["closing_value"] = ( - ((row.opening_value or 0) + (row.purchase_value or 0)) - + (row.sold_value or 0) - + ( - (row.adjustment_value or 0) - + ( - (row.consumed_value or 0) - + (row.produced_value or 0) - + (row.received_value or 0) - + (row.issued_value or 0) - ) - ) - + (row.reconciliation_value or 0) - ) - - prepared_data = prepare_data(sl_entries) - - return columns, prepared_data + is_reposting_item_valuation_in_progress() + columns = get_columns(filters) + items = get_items(filters) + sl_entries = get_stock_ledger_entries(filters, items) + item_details = get_item_details(items, sl_entries) + + opening_balance_item_wise = [] + for row in sl_entries: + item_detail = item_details[row.item_code] + row.update(item_detail) + + row["opening_qty"] = row["opening_value"] = row["reconciliation_qty"] = row[ + "reconciliation_value" + ] = row["purchase_qty"] = row["purchase_value"] = row["sold_qty"] = row["sold_value"] = row[ + "adjustment_qty" + ] = row["adjustment_value"] = row["consumed_qty"] = row["consumed_value"] = row["produced_qty"] = row[ + "produced_value" + ] = row["received_qty"] = row["received_value"] = row["issued_qty"] = row["issued_value"] = 0 + + if not any( + record["item_code"] == row.item_code and record["warehouse"] == row.warehouse + for record in opening_balance_item_wise + ): + opening_balance_item_wise.append({"item_code": row.item_code, "warehouse": row.warehouse}) + opening_row = get_opening_balance(filters, row.item_code, row.warehouse, sl_entries) + + if opening_row: + row["opening_qty"] = opening_row["qty_after_transaction"] + row["opening_value"] = flt(row["opening_qty"]) * opening_row["valuation_rate"] + + if row.voucher_type == "Stock Reconciliation": + row["reconciliation_qty"] = row.stock_value_difference / (row.valuation_rate or 1) + + row["reconciliation_value"] = row.stock_value_difference + + if row.voucher_type == "Purchase Receipt" or row.voucher_type == "Purchase Invoice": + row["purchase_qty"] = row.actual_qty + row["purchase_value"] = row.stock_value_difference + + if row.voucher_type == "Delivery Note" or row.voucher_type == "Sales Invoice": + row["sold_qty"] = row.actual_qty + row["sold_value"] = row.stock_value_difference + + if row.voucher_type == "Stock Entry": + row["adjustment_qty"] = row.actual_qty + row["adjustment_value"] = row.stock_value_difference + + stock_entry_type = frappe.db.get_value(row.voucher_type, row.voucher_no, "stock_entry_type") + if stock_entry_type == "Material Transfer for Manufacture": + row["consumed_qty"] = row.actual_qty + row["consumed_value"] = row.stock_value_difference + + if stock_entry_type == "Manufacture": + row["produced_qty"] = row.actual_qty + row["produced_value"] = row.stock_value_difference + + if stock_entry_type == "Material Receipt": + row["received_qty"] = row.actual_qty + row["received_value"] = row.stock_value_difference + + if stock_entry_type == "Material Issue": + row["issued_qty"] = row.actual_qty + row["issued_value"] = row.stock_value_difference + + row["closing_qty"] = ( + ((row.opening_qty or 0) + (row.purchase_qty or 0)) + + (row.sold_qty or 0) + + ( + (row.adjustment_qty or 0) + + ( + (row.consumed_qty or 0) + + (row.produced_qty or 0) + + (row.received_qty or 0) + + (row.issued_qty or 0) + ) + ) + + (row.reconciliation_qty or 0) + ) + + row["closing_value"] = ( + ((row.opening_value or 0) + (row.purchase_value or 0)) + + (row.sold_value or 0) + + ( + (row.adjustment_value or 0) + + ( + (row.consumed_value or 0) + + (row.produced_value or 0) + + (row.received_value or 0) + + (row.issued_value or 0) + ) + ) + + (row.reconciliation_value or 0) + ) + + prepared_data = prepare_data(sl_entries) + + return columns, prepared_data def prepare_data(data): - sorted_data = sorted(data, key=itemgetter("item_code")) - - grouped_data = { - key: list(group) - for key, group in groupby(sorted_data, key=itemgetter("item_code")) - } - result = [] - for item_code, group in grouped_data.items(): - result.append( - { - "date": group[0]["date"], - "item_code": item_code, - "stock_uom": group[0]["stock_uom"], - "opening_qty": sum(entry["opening_qty"] for entry in group), - "opening_value": sum(entry["opening_value"] for entry in group), - "purchase_qty": sum(entry["purchase_qty"] for entry in group), - "purchase_value": sum(entry["purchase_value"] for entry in group), - "sold_qty": sum(entry["sold_qty"] for entry in group), - "sold_value": sum(entry["sold_value"] for entry in group), - "adjustment_qty": sum(entry["adjustment_qty"] for entry in group), - "adjustment_value": sum(entry["adjustment_value"] for entry in group), - "consumed_qty": sum(entry["consumed_qty"] for entry in group), - "consumed_value": sum(entry["consumed_value"] for entry in group), - "produced_qty": sum(entry["produced_qty"] for entry in group), - "produced_value": sum(entry["produced_value"] for entry in group), - "received_qty": sum(entry["received_qty"] for entry in group), - "received_value": sum(entry["received_value"] for entry in group), - "issued_qty": sum(entry["issued_qty"] for entry in group), - "issued_value": sum(entry["issued_value"] for entry in group), - "reconciliation_qty": sum( - entry["reconciliation_qty"] for entry in group - ), - "reconciliation_value": sum( - entry["reconciliation_value"] for entry in group - ), - "closing_qty": sum(entry["closing_qty"] for entry in group), - "closing_value": sum(entry["closing_value"] for entry in group), - } - ) - return result + sorted_data = sorted(data, key=itemgetter("item_code")) + + grouped_data = {key: list(group) for key, group in groupby(sorted_data, key=itemgetter("item_code"))} + result = [] + for item_code, group in grouped_data.items(): + result.append( + { + "date": group[0]["date"], + "item_code": item_code, + "stock_uom": group[0]["stock_uom"], + "opening_qty": sum(entry["opening_qty"] for entry in group), + "opening_value": sum(entry["opening_value"] for entry in group), + "purchase_qty": sum(entry["purchase_qty"] for entry in group), + "purchase_value": sum(entry["purchase_value"] for entry in group), + "sold_qty": sum(entry["sold_qty"] for entry in group), + "sold_value": sum(entry["sold_value"] for entry in group), + "adjustment_qty": sum(entry["adjustment_qty"] for entry in group), + "adjustment_value": sum(entry["adjustment_value"] for entry in group), + "consumed_qty": sum(entry["consumed_qty"] for entry in group), + "consumed_value": sum(entry["consumed_value"] for entry in group), + "produced_qty": sum(entry["produced_qty"] for entry in group), + "produced_value": sum(entry["produced_value"] for entry in group), + "received_qty": sum(entry["received_qty"] for entry in group), + "received_value": sum(entry["received_value"] for entry in group), + "issued_qty": sum(entry["issued_qty"] for entry in group), + "issued_value": sum(entry["issued_value"] for entry in group), + "reconciliation_qty": sum(entry["reconciliation_qty"] for entry in group), + "reconciliation_value": sum(entry["reconciliation_value"] for entry in group), + "closing_qty": sum(entry["closing_qty"] for entry in group), + "closing_value": sum(entry["closing_value"] for entry in group), + } + ) + return result def get_columns(filters): - columns = [ - { - "label": _("Date"), - "fieldname": "date", - "fieldtype": "Datetime", - "width": 150, - }, - { - "label": _("Item"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100, - }, - { - "label": _("Stock UOM"), - "fieldname": "stock_uom", - "fieldtype": "Link", - "options": "UOM", - "width": 90, - }, - { - "label": _("Opening Qty"), - "fieldname": "opening_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Opening Value"), - "fieldname": "opening_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Purchase Qty"), - "fieldname": "purchase_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Purchase Value"), - "fieldname": "purchase_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Sold Qty"), - "fieldname": "sold_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Sold Value"), - "fieldname": "sold_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Adjustment Qty"), - "fieldname": "adjustment_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Adjustment Value"), - "fieldname": "adjustment_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Reconciliation Qty"), - "fieldname": "reconciliation_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Reconciliation Value"), - "fieldname": "reconciliation_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Consumed Qty"), - "fieldname": "consumed_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Consumed Value"), - "fieldname": "consumed_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Produced Qty"), - "fieldname": "produced_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Produced Value"), - "fieldname": "produced_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Received Qty"), - "fieldname": "received_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Received Value"), - "fieldname": "received_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Issued Qty"), - "fieldname": "issued_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Issued Value"), - "fieldname": "issued_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Closing Qty"), - "fieldname": "closing_qty", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - { - "label": _("Closing Value"), - "fieldname": "closing_value", - "fieldtype": "Float", - "width": 150, - "convertible": "qty", - }, - ] - - return columns + columns = [ + { + "label": _("Date"), + "fieldname": "date", + "fieldtype": "Datetime", + "width": 150, + }, + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("Opening Qty"), + "fieldname": "opening_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Opening Value"), + "fieldname": "opening_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Purchase Qty"), + "fieldname": "purchase_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Purchase Value"), + "fieldname": "purchase_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Sold Qty"), + "fieldname": "sold_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Sold Value"), + "fieldname": "sold_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Adjustment Qty"), + "fieldname": "adjustment_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Adjustment Value"), + "fieldname": "adjustment_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Reconciliation Qty"), + "fieldname": "reconciliation_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Reconciliation Value"), + "fieldname": "reconciliation_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Consumed Qty"), + "fieldname": "consumed_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Consumed Value"), + "fieldname": "consumed_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Produced Qty"), + "fieldname": "produced_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Produced Value"), + "fieldname": "produced_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Received Qty"), + "fieldname": "received_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Received Value"), + "fieldname": "received_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Issued Qty"), + "fieldname": "issued_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Issued Value"), + "fieldname": "issued_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Closing Qty"), + "fieldname": "closing_qty", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + { + "label": _("Closing Value"), + "fieldname": "closing_value", + "fieldtype": "Float", + "width": 150, + "convertible": "qty", + }, + ] + + return columns def get_stock_ledger_entries(filters, items): - sle = frappe.qb.DocType("Stock Ledger Entry") - query = ( - frappe.qb.from_(sle) - .select( - sle.item_code, - CombineDatetime(sle.posting_date, sle.posting_time).as_("date"), - sle.warehouse, - sle.posting_date, - sle.posting_time, - sle.actual_qty, - sle.valuation_rate, - sle.valuation_rate, - sle.company, - sle.voucher_type, - sle.qty_after_transaction, - sle.stock_value_difference, - sle.voucher_no, - sle.stock_value, - sle.batch_no, - sle.serial_no, - sle.project, - ) - .where( - (sle.docstatus < 2) - & (sle.is_cancelled == 0) - & (sle.posting_date[filters.from_date : filters.to_date]) - ) - .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) - .orderby(sle.creation) - ) - if filters.warehouse: - query = query.where(sle.warehouse == filters.get("warehouse")) - if filters.item_code: - query = query.where(sle.item_code == filters.get("item_code")) - - return query.run(as_dict=True) + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select( + sle.item_code, + CombineDatetime(sle.posting_date, sle.posting_time).as_("date"), + sle.warehouse, + sle.posting_date, + sle.posting_time, + sle.actual_qty, + sle.valuation_rate, + sle.valuation_rate, + sle.company, + sle.voucher_type, + sle.qty_after_transaction, + sle.stock_value_difference, + sle.voucher_no, + sle.stock_value, + sle.batch_no, + sle.serial_no, + sle.project, + ) + .where( + (sle.docstatus < 2) + & (sle.is_cancelled == 0) + & (sle.posting_date[filters.from_date : filters.to_date]) + ) + .orderby(CombineDatetime(sle.posting_date, sle.posting_time)) + .orderby(sle.creation) + ) + if filters.warehouse: + query = query.where(sle.warehouse == filters.get("warehouse")) + if filters.item_code: + query = query.where(sle.item_code == filters.get("item_code")) + + return query.run(as_dict=True) def get_items(filters): - item = frappe.qb.DocType("Item") - query = frappe.qb.from_(item).select(item.name) - conditions = [] + item = frappe.qb.DocType("Item") + query = frappe.qb.from_(item).select(item.name) + conditions = [] - if item_code := filters.get("item_code"): - conditions.append(item.name == item_code) - else: - if item_group := filters.get("item_group"): - if condition := get_item_group_condition(item_group, item): - conditions.append(condition) + if item_code := filters.get("item_code"): + conditions.append(item.name == item_code) + else: + if item_group := filters.get("item_group"): + if condition := get_item_group_condition(item_group, item): + conditions.append(condition) - items = [] - if conditions: - for condition in conditions: - query = query.where(condition) - items = [r[0] for r in query.run()] + items = [] + if conditions: + for condition in conditions: + query = query.where(condition) + items = [r[0] for r in query.run()] - return items + return items def get_item_details(items, sl_entries): - item_details = {} - if not items: - items = list(set(d.item_code for d in sl_entries)) + item_details = {} + if not items: + items = list(set(d.item_code for d in sl_entries)) - if not items: - return item_details + if not items: + return item_details - item = frappe.qb.DocType("Item") - query = ( - frappe.qb.from_(item) - .select( - item.name, - item.item_name, - item.description, - item.item_group, - item.brand, - item.stock_uom, - ) - .where(item.name.isin(items)) - ) + item = frappe.qb.DocType("Item") + query = ( + frappe.qb.from_(item) + .select( + item.name, + item.item_name, + item.description, + item.item_group, + item.brand, + item.stock_uom, + ) + .where(item.name.isin(items)) + ) - res = query.run(as_dict=True) + res = query.run(as_dict=True) - for item in res: - item_details.setdefault(item.name, item) + for item in res: + item_details.setdefault(item.name, item) - return item_details + return item_details def get_opening_balance(filters, item_code, warehouse, sl_entries): - if not filters.from_date: - return - - from erpnext.stock.stock_ledger import get_previous_sle - - last_entry = get_previous_sle( - { - "item_code": item_code, - "warehouse_condition": get_warehouse_condition(warehouse), - "posting_date": filters.from_date, - "posting_time": "00:00:00", - } - ) - - # check if any SLEs are actually Opening Stock Reconciliation - for sle in list(sl_entries): - if ( - sle.get("voucher_type") == "Stock Reconciliation" - and sle.posting_date == filters.from_date - and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") - == "Opening Stock" - ): - last_entry = sle - sl_entries.remove(sle) - - row = { - "item_code": item_code, - "qty_after_transaction": last_entry.get("qty_after_transaction", 0), - "valuation_rate": last_entry.get("valuation_rate", 0), - "stock_value": last_entry.get("stock_value", 0), - } - - return row + if not filters.from_date: + return + + from erpnext.stock.stock_ledger import get_previous_sle + + last_entry = get_previous_sle( + { + "item_code": item_code, + "warehouse_condition": get_warehouse_condition(warehouse), + "posting_date": filters.from_date, + "posting_time": "00:00:00", + } + ) + + # check if any SLEs are actually Opening Stock Reconciliation + for sle in list(sl_entries): + if ( + sle.get("voucher_type") == "Stock Reconciliation" + and sle.posting_date == filters.from_date + and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock" + ): + last_entry = sle + sl_entries.remove(sle) + + row = { + "item_code": item_code, + "qty_after_transaction": last_entry.get("qty_after_transaction", 0), + "valuation_rate": last_entry.get("valuation_rate", 0), + "stock_value": last_entry.get("stock_value", 0), + } + + return row def get_warehouse_condition(warehouse): - warehouse_details = frappe.db.get_value( - "Warehouse", warehouse, ["lft", "rgt"], as_dict=1 - ) - if warehouse_details: - return ( - " exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) - ) + warehouse_details = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"], as_dict=1) + if warehouse_details: + return f" exists (select name from `tabWarehouse` wh \ + where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and warehouse = wh.name)" - return "" + return "" def get_item_group_condition(item_group, item_table=None): - item_group_details = frappe.db.get_value( - "Item Group", item_group, ["lft", "rgt"], as_dict=1 - ) - if item_group_details: - if item_table: - ig = frappe.qb.DocType("Item Group") - return item_table.item_group.isin( - ( - frappe.qb.from_(ig) - .select(ig.name) - .where( - (ig.lft >= item_group_details.lft) - & (ig.rgt <= item_group_details.rgt) - & (item_table.item_group == ig.name) - ) - ) - ) - else: - return ( - "item.item_group in (select ig.name from `tabItem Group` ig \ - where ig.lft >= %s and ig.rgt <= %s and item.item_group = ig.name)" - % (item_group_details.lft, item_group_details.rgt) - ) + item_group_details = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"], as_dict=1) + if item_group_details: + if item_table: + ig = frappe.qb.DocType("Item Group") + return item_table.item_group.isin( + frappe.qb.from_(ig) + .select(ig.name) + .where( + (ig.lft >= item_group_details.lft) + & (ig.rgt <= item_group_details.rgt) + & (item_table.item_group == ig.name) + ) + ) + else: + return f"item.item_group in (select ig.name from `tabItem Group` ig \ + where ig.lft >= {item_group_details.lft} and ig.rgt <= {item_group_details.rgt} and item.item_group = ig.name)" diff --git a/csf_tz/csf_tz/report/employee_salary_register_with_monthly_comparison/employee_salary_register_with_monthly_comparison.py b/csf_tz/csf_tz/report/employee_salary_register_with_monthly_comparison/employee_salary_register_with_monthly_comparison.py index 4036d8ec..2cdee406 100644 --- a/csf_tz/csf_tz/report/employee_salary_register_with_monthly_comparison/employee_salary_register_with_monthly_comparison.py +++ b/csf_tz/csf_tz/report/employee_salary_register_with_monthly_comparison/employee_salary_register_with_monthly_comparison.py @@ -1,13 +1,14 @@ # Copyright (c) 2022, Aakvatech and contributors # For license information, please see license.txt +import calendar + import frappe from erpnext import get_company_currency from frappe import _, msgprint -from frappe.utils import getdate, flt, cstr, cint +from frappe.utils import cstr, flt, getdate from frappe.utils.nestedset import get_descendants_of -import calendar def execute(filters): company_currency = get_company_currency(filters.get("company")) @@ -21,17 +22,20 @@ def execute(filters): prev_salary_slips = get_prev_salary_slips(filters, company_currency, prev_first_date, prev_last_date) cur_salary_slips = get_cur_salary_slips(filters, company_currency) - + if len(prev_salary_slips) == 0: - msgprint(_("No salary slip found for the previous month: {0} {1}".format( - frappe.bold(calendar.month_name[prev_month]), frappe.bold(prev_year))) + msgprint( + _( + f"No salary slip found for the previous month: {frappe.bold(calendar.month_name[prev_month])} {frappe.bold(prev_year)}" + ) ) return columns, cur_salary_slips - + if len(cur_salary_slips) == 0: - msgprint(_("No salary slip found for the this month: {0} {1}".format( - frappe.bold(calendar.month_name[getdate(filters.from_date).month]), - frappe.bold(getdate(filters.from_date).year))) + msgprint( + _( + f"No salary slip found for the this month: {frappe.bold(calendar.month_name[getdate(filters.from_date).month])} {frappe.bold(getdate(filters.from_date).year)}" + ) ) return columns, prev_salary_slips @@ -39,6 +43,7 @@ def execute(filters): return columns, data + def get_columns(prev_month_name, cur_month_name, prev_year, cur_year): columns = [ { @@ -46,44 +51,40 @@ def get_columns(prev_month_name, cur_month_name, prev_year, cur_year): "label": _("Employee"), "fieldtype": "Link", "width": 150, - "options": "Employee" - }, - { - "fieldname": "employee_name", - "label": _("Employee Name"), - "fieldtype": "Data", - "width": 150 + "options": "Employee", }, + {"fieldname": "employee_name", "label": _("Employee Name"), "fieldtype": "Data", "width": 150}, { "fieldname": "department", "label": _("Department"), "fieldtype": "Link", "width": 150, - "options": "Department" + "options": "Department", }, { "fieldname": "prev_gross_pay", - "label": _("Gross Pay {0}-{1}".format(prev_month_name, prev_year)), + "label": _(f"Gross Pay {prev_month_name}-{prev_year}"), "fieldtype": "Float", "width": 250, - "precision": 2 + "precision": 2, }, { "fieldname": "cur_gross_pay", - "label": _("Gross Pay {0}-{1}".format(cur_month_name, cur_year)), + "label": _(f"Gross Pay {cur_month_name}-{cur_year}"), "fieldtype": "Float", "width": 250, - "precision": 2 + "precision": 2, }, { "fieldname": "gross_difference_amount", "label": _("Gross Difference Amount"), "fieldtype": "Data", - "width": 150 + "width": 150, }, ] return columns + def get_data(prev_ss, cur_ss): """Merge employee details from current and previous months""" @@ -93,57 +94,63 @@ def get_data(prev_ss, cur_ss): for cur_ss_row in cur_ss: for prev_ss_row in prev_ss: - if ( - cur_ss_row.employee == prev_ss_row.employee and - flt(cur_ss_row.cur_gross_pay, 2) == flt(prev_ss_row.prev_gross_pay, 2) + if cur_ss_row.employee == prev_ss_row.employee and flt(cur_ss_row.cur_gross_pay, 2) == flt( + prev_ss_row.prev_gross_pay, 2 ): unique_prev_employees.append(prev_ss_row.employee) unique_cur_employees.append(cur_ss_row.employee) - - elif ( - cur_ss_row.employee == prev_ss_row.employee and - flt(cur_ss_row.cur_gross_pay, 2) != flt(prev_ss_row.prev_gross_pay, 2) + + elif cur_ss_row.employee == prev_ss_row.employee and flt(cur_ss_row.cur_gross_pay, 2) != flt( + prev_ss_row.prev_gross_pay, 2 ): unique_prev_employees.append(prev_ss_row.employee) unique_cur_employees.append(cur_ss_row.employee) gross_amount_diff = flt(flt(cur_ss_row.cur_gross_pay) - flt(prev_ss_row.prev_gross_pay), 2) - cur_ss_row.update({ - "prev_gross_pay": prev_ss_row.prev_gross_pay, - "cur_gross_pay": cur_ss_row.cur_gross_pay, - "gross_difference_amount": get_difference_amount_detail(gross_amount_diff) - }) + cur_ss_row.update( + { + "prev_gross_pay": prev_ss_row.prev_gross_pay, + "cur_gross_pay": cur_ss_row.cur_gross_pay, + "gross_difference_amount": get_difference_amount_detail(gross_amount_diff), + } + ) data.append(cur_ss_row) # Update employee details for the current month if the employee is not in the list of employees for previous month if cur_ss_row.employee not in unique_cur_employees: unique_cur_employees.append(cur_ss_row.employee) - cur_ss_row.update({ - "prev_gross_pay": 0, - "cur_gross_pay": cur_ss_row.cur_gross_pay, - "gross_difference_amount": "+ " + str(cur_ss_row.cur_gross_pay) - }) + cur_ss_row.update( + { + "prev_gross_pay": 0, + "cur_gross_pay": cur_ss_row.cur_gross_pay, + "gross_difference_amount": "+ " + str(cur_ss_row.cur_gross_pay), + } + ) data.append(cur_ss_row) return update_unique_prev_employee_ss_details(data, prev_ss, unique_prev_employees) + def update_unique_prev_employee_ss_details(data, prev_ss, unique_prev_employees): - """"Updating unique employee details of previous month if the employee is not in the list of employees for the current month""" + """ "Updating unique employee details of previous month if the employee is not in the list of employees for the current month""" for prev_row in prev_ss: if prev_row.employee not in unique_prev_employees: unique_prev_employees.append(prev_row.employee) - prev_row.update({ - "prev_gross_pay": prev_row.prev_gross_pay, - "cur_gross_pay": 0, - "gross_difference_amount": "- " + str(prev_row.prev_gross_pay) - }) + prev_row.update( + { + "prev_gross_pay": prev_row.prev_gross_pay, + "cur_gross_pay": 0, + "gross_difference_amount": "- " + str(prev_row.prev_gross_pay), + } + ) data.append(prev_row) return data + def get_difference_amount_detail(bsc_amount_diff): """Show + or - sign on the amount difference between current and previous month""" @@ -156,37 +163,40 @@ def get_difference_amount_detail(bsc_amount_diff): result = "0" return result + def get_prev_salary_slips(filters, company_currency, prev_first_date, prev_last_date): """Get submitted salary slips for precious month""" custom_filters = filters - custom_filters.update({ - "prev_first_date": prev_first_date, - "prev_last_date": prev_last_date - }) + custom_filters.update({"prev_first_date": prev_first_date, "prev_last_date": prev_last_date}) prev_conditions = get_prev_conditions(custom_filters, company_currency) - prev_salary_slips = frappe.db.sql(""" - select name, employee, employee_name, department, gross_pay as prev_gross_pay from `tabSalary Slip` where %s - order by employee"""% - prev_conditions, filters, as_dict=1) + prev_salary_slips = frappe.db.sql( + f""" + select name, employee, employee_name, department, gross_pay as prev_gross_pay from `tabSalary Slip` where {prev_conditions} + order by employee""", + filters, + as_dict=1, + ) return prev_salary_slips or [] + def get_cur_salary_slips(filters, company_currency): """Get salary slips for the current month""" - filters.update({ - "from_date": filters.get("from_date"), - "to_date": filters.get("to_date") - }) + filters.update({"from_date": filters.get("from_date"), "to_date": filters.get("to_date")}) conditions, filters = get_cur_conditions(filters, company_currency) - salary_slips = frappe.db.sql(""" - select name, employee, employee_name, department, gross_pay as cur_gross_pay from `tabSalary Slip` where %s - order by employee"""% - conditions, filters, as_dict=1) - + salary_slips = frappe.db.sql( + f""" + select name, employee, employee_name, department, gross_pay as cur_gross_pay from `tabSalary Slip` where {conditions} + order by employee""", + filters, + as_dict=1, + ) + return salary_slips or [] + def get_prev_month_date(filters): """Get date deatils for previous month""" @@ -197,16 +207,17 @@ def get_prev_month_date(filters): prev_month = 12 prev_year = prev_year - 1 - prev_first_date = getdate(str(prev_year) + "-" + str(prev_month) + "-" + "01") - prev_last_date = getdate(str(prev_year) + "-" + str(prev_month) + "-" + "{0}".format( - calendar.monthrange(prev_year, prev_month)[1]) + prev_first_date = getdate(str(prev_year) + "-" + str(prev_month) + "-" + "01") + prev_last_date = getdate( + str(prev_year) + "-" + str(prev_month) + "-" + f"{calendar.monthrange(prev_year, prev_month)[1]}" ) return prev_first_date, prev_last_date, prev_month, prev_year + def get_prev_conditions(filters, company_currency): """Conditions that will be used to get salary slips for previous month""" - + # this is get submitted salary slips for the previous month conditions = "docstatus <= 1" @@ -222,11 +233,11 @@ def get_prev_conditions(filters, company_currency): conditions += " and currency = %(currency)s" if filters.get("department") and filters.get("company"): department_list = get_departments(filters.get("department"), filters.get("company")) - conditions += 'and department in (' + ','.join( - ("'"+n+"'" for n in department_list)) + ')' + conditions += "and department in (" + ",".join("'" + n + "'" for n in department_list) + ")" return conditions + def get_cur_conditions(filters, company_currency): """Conditions that will be used to get salary slips for current month""" @@ -234,7 +245,7 @@ def get_cur_conditions(filters, company_currency): doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} if filters.get("docstatus"): - conditions += "docstatus = {0}".format(doc_status[filters.get("docstatus")]) + conditions += "docstatus = {}".format(doc_status[filters.get("docstatus")]) if filters.get("from_date"): conditions += " and start_date >= %(from_date)s" @@ -248,12 +259,12 @@ def get_cur_conditions(filters, company_currency): conditions += " and currency = %(currency)s" if filters.get("department") and filters.get("company"): department_list = get_departments(filters.get("department"), filters.get("company")) - conditions += 'and department in (' + ','.join( - ("'"+n+"'" for n in department_list)) + ')' + conditions += "and department in (" + ",".join("'" + n + "'" for n in department_list) + ")" return conditions, filters -def get_departments(department,company): - departments_list = get_descendants_of("Department", department) - departments_list.append(department) - return departments_list \ No newline at end of file + +def get_departments(department, company): + departments_list = get_descendants_of("Department", department) + departments_list.append(department) + return departments_list diff --git a/csf_tz/csf_tz/report/excise_duty_stock/excise_duty_stock.py b/csf_tz/csf_tz/report/excise_duty_stock/excise_duty_stock.py index c084c05d..4e8dab76 100644 --- a/csf_tz/csf_tz/report/excise_duty_stock/excise_duty_stock.py +++ b/csf_tz/csf_tz/report/excise_duty_stock/excise_duty_stock.py @@ -1,195 +1,181 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import erpnext -from frappe import _ -from frappe.utils import flt, cint, getdate -from erpnext.stock.utils import add_additional_uom_columns +import frappe from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition - - +from erpnext.stock.utils import add_additional_uom_columns +from frappe import _ +from frappe.utils import cint, flt, getdate from six import iteritems def execute(filters=None): - if not filters: - filters = {} + if not filters: + filters = {} - validate_filters(filters) + validate_filters(filters) - if filters.get("company"): - company_currency = erpnext.get_company_currency(filters.get("company")) - else: - company_currency = frappe.db.get_single_value( - "Global Defaults", "default_currency" - ) + if filters.get("company"): + company_currency = erpnext.get_company_currency(filters.get("company")) + else: + company_currency = frappe.db.get_single_value("Global Defaults", "default_currency") - include_uom = filters.get("include_uom") - columns = get_columns(filters) - items = get_items(filters) - sle = get_stock_ledger_entries(filters, items) + include_uom = filters.get("include_uom") + columns = get_columns(filters) + items = get_items(filters) + sle = get_stock_ledger_entries(filters, items) - # if no stock ledger entry found return - if not sle: - return columns, [] + # if no stock ledger entry found return + if not sle: + return columns, [] - iwb_map = get_item_warehouse_map(filters, sle) - item_map = get_item_details(items, sle, filters) + iwb_map = get_item_warehouse_map(filters, sle) + item_map = get_item_details(items, sle, filters) - data = [] - conversion_factors = {} + data = [] + conversion_factors = {} - def _func(x): - return x[1] + def _func(x): + return x[1] - for company, item in sorted(iwb_map): - if item_map.get(item): - qty_dict = iwb_map[(company, item)] + for company, item in sorted(iwb_map): + if item_map.get(item): + qty_dict = iwb_map[(company, item)] - report_data = { - "currency": company_currency, - "item_code": item, - "company": company, - } - report_data.update(item_map[item]) - report_data.update(qty_dict) + report_data = { + "currency": company_currency, + "item_code": item, + "company": company, + } + report_data.update(item_map[item]) + report_data.update(qty_dict) - if include_uom: - conversion_factors.setdefault(item, item_map[item].conversion_factor) + if include_uom: + conversion_factors.setdefault(item, item_map[item].conversion_factor) - data.append(report_data) + data.append(report_data) - add_additional_uom_columns(columns, data, include_uom, conversion_factors) - return columns, data + add_additional_uom_columns(columns, data, include_uom, conversion_factors) + return columns, data def get_columns(filters): - """return columns""" - columns = [ - { - "label": _("Item"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100, - }, - {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, - { - "label": _("Item Group"), - "fieldname": "item_group", - "fieldtype": "Link", - "options": "Item Group", - "width": 100, - }, - { - "label": _("Stock UOM"), - "fieldname": "stock_uom", - "fieldtype": "Link", - "options": "UOM", - "width": 90, - }, - { - "label": _("Opening Qty"), - "fieldname": "opening_qty", - "fieldtype": "Float", - "width": 100, - "convertible": "qty", - }, - { - "label": _("In Qty"), - "fieldname": "in_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - { - "label": _("Out Qty"), - "fieldname": "out_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - { - "label": _("Balance Qty"), - "fieldname": "bal_qty", - "fieldtype": "Float", - "width": 100, - "convertible": "qty", - }, - { - "label": _("Excise Qty"), - "fieldname": "excise_stock", - "fieldtype": "Float", - "width": 100, - "convertible": "qty", - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - "width": 100, - }, - ] - - if filters.get("show_variant_attributes"): - columns += [ - {"label": att_name, "fieldname": att_name, "width": 100} - for att_name in get_variants_attributes() - ] - - return columns + """return columns""" + columns = [ + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("Opening Qty"), + "fieldname": "opening_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Balance Qty"), + "fieldname": "bal_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Excise Qty"), + "fieldname": "excise_stock", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, + ] + + if filters.get("show_variant_attributes"): + columns += [ + {"label": att_name, "fieldname": att_name, "width": 100} for att_name in get_variants_attributes() + ] + + return columns def get_conditions(filters): - conditions = "" - if not filters.get("from_date"): - frappe.throw(_("'From Date' is required")) - - if filters.get("to_date"): - conditions += " and sle.posting_date <= %s" % frappe.db.escape( - filters.get("to_date") - ) - else: - frappe.throw(_("'To Date' is required")) - - if filters.get("company"): - conditions += " and sle.company = %s" % frappe.db.escape(filters.get("company")) - - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 - ) - if warehouse_details: - conditions += ( - " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) - ) - - if filters.get("warehouse_type") and not filters.get("warehouse"): - conditions += ( - " and exists (select name from `tabWarehouse` wh \ - where wh.warehouse_type = '%s' and sle.warehouse = wh.name)" - % (filters.get("warehouse_type")) - ) - - return conditions + conditions = "" + if not filters.get("from_date"): + frappe.throw(_("'From Date' is required")) + + if filters.get("to_date"): + conditions += " and sle.posting_date <= {}".format(frappe.db.escape(filters.get("to_date"))) + else: + frappe.throw(_("'To Date' is required")) + + if filters.get("company"): + conditions += " and sle.company = {}".format(frappe.db.escape(filters.get("company"))) + + if filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + if warehouse_details: + conditions += f" and exists (select name from `tabWarehouse` wh \ + where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and sle.warehouse = wh.name)" + + if filters.get("warehouse_type") and not filters.get("warehouse"): + conditions += " and exists (select name from `tabWarehouse` wh \ + where wh.warehouse_type = '{}' and sle.warehouse = wh.name)".format(filters.get("warehouse_type")) + + return conditions def get_stock_ledger_entries(filters, items): - item_conditions_sql = "" - if items: - item_conditions_sql = " and sle.item_code in ({})".format( - ", ".join([frappe.db.escape(i, percent=False) for i in items]) - ) + item_conditions_sql = "" + if items: + item_conditions_sql = " and sle.item_code in ({})".format( + ", ".join([frappe.db.escape(i, percent=False) for i in items]) + ) - conditions = get_conditions(filters) + conditions = get_conditions(filters) - return frappe.db.sql( - """ + return frappe.db.sql( + f""" select sle.item_code, sle.warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, @@ -201,7 +187,7 @@ def get_stock_ledger_entries(filters, items): where sle.is_cancelled = 0 and (se.purpose != "Material Transfer") and i.excisable_item = 1 - and sle.docstatus < 2 %s %s + and sle.docstatus < 2 {item_conditions_sql} {conditions} UNION ALL select sle.item_code, sle.warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, @@ -213,7 +199,7 @@ def get_stock_ledger_entries(filters, items): inner join `tabItem` i on sle.item_code = i.name where sle.is_cancelled = 0 and i.excisable_item = 1 - and sle.docstatus < 2 %s %s + and sle.docstatus < 2 {item_conditions_sql} {conditions} UNION ALL select sle.item_code, sle.warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, @@ -225,175 +211,152 @@ def get_stock_ledger_entries(filters, items): where sle.is_cancelled = 0 and sle.voucher_type NOT IN ("Stock Entry", "Sales Invoice") and i.excisable_item = 1 - and sle.docstatus < 2 %s %s - order by 3""" # nosec - % ( - item_conditions_sql, - conditions, - item_conditions_sql, - conditions, - item_conditions_sql, - conditions, - ), - as_dict=1, - ) + and sle.docstatus < 2 {item_conditions_sql} {conditions} + order by 3""", + as_dict=1, + ) def get_item_warehouse_map(filters, sle): - iwb_map = {} - from_date = getdate(filters.get("from_date")) - to_date = getdate(filters.get("to_date")) + iwb_map = {} + from_date = getdate(filters.get("from_date")) + to_date = getdate(filters.get("to_date")) - float_precision = cint(frappe.db.get_default("float_precision")) or 3 + float_precision = cint(frappe.db.get_default("float_precision")) or 3 - for d in sle: - key = (d.company, d.item_code) - if key not in iwb_map: - iwb_map[key] = frappe._dict( - { - "opening_qty": 0.0, - "in_qty": 0.0, - "out_qty": 0.0, - "excise_stock": 0.0, - "bal_qty": 0.0, - } - ) + for d in sle: + key = (d.company, d.item_code) + if key not in iwb_map: + iwb_map[key] = frappe._dict( + { + "opening_qty": 0.0, + "in_qty": 0.0, + "out_qty": 0.0, + "excise_stock": 0.0, + "bal_qty": 0.0, + } + ) - qty_dict = iwb_map[(d.company, d.item_code)] + qty_dict = iwb_map[(d.company, d.item_code)] - if d.voucher_type == "Stock Reconciliation": - qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty) - else: - qty_diff = flt(d.actual_qty) + if d.voucher_type == "Stock Reconciliation": + qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty) + else: + qty_diff = flt(d.actual_qty) - if d.posting_date < from_date: - qty_dict.opening_qty += qty_diff + if d.posting_date < from_date: + qty_dict.opening_qty += qty_diff - elif d.posting_date >= from_date and d.posting_date <= to_date: - if flt(qty_diff, float_precision) >= 0: - qty_dict.in_qty += qty_diff - else: - qty_dict.out_qty += abs(qty_diff) - qty_dict.excise_stock += abs(d.excise_stock) or 0 + elif d.posting_date >= from_date and d.posting_date <= to_date: + if flt(qty_diff, float_precision) >= 0: + qty_dict.in_qty += qty_diff + else: + qty_dict.out_qty += abs(qty_diff) + qty_dict.excise_stock += abs(d.excise_stock) or 0 - qty_dict.bal_qty += qty_diff + qty_dict.bal_qty += qty_diff - iwb_map = filter_items_with_no_transactions(iwb_map, float_precision) + iwb_map = filter_items_with_no_transactions(iwb_map, float_precision) - return iwb_map + return iwb_map def filter_items_with_no_transactions(iwb_map, float_precision): - for company, item in sorted(iwb_map): - qty_dict = iwb_map[(company, item)] + for company, item in sorted(iwb_map): + qty_dict = iwb_map[(company, item)] - no_transactions = True - for key, val in iteritems(qty_dict): - val = flt(val, float_precision) - qty_dict[key] = val - if key != "val_rate" and val: - no_transactions = False + no_transactions = True + for key, val in iteritems(qty_dict): + val = flt(val, float_precision) + qty_dict[key] = val + if key != "val_rate" and val: + no_transactions = False - if no_transactions: - iwb_map.pop((company, item)) + if no_transactions: + iwb_map.pop((company, item)) - return iwb_map + return iwb_map def get_items(filters): - conditions = [] - if filters.get("item_code"): - conditions.append("item.name=%(item_code)s") - else: - if filters.get("item_group"): - conditions.append(get_item_group_condition(filters.get("item_group"))) - - items = [] - if conditions: - items = frappe.db.sql_list( - """select name from `tabItem` item where {}""".format( - " and ".join(conditions) - ), - filters, - ) - return items + conditions = [] + if filters.get("item_code"): + conditions.append("item.name=%(item_code)s") + else: + if filters.get("item_group"): + conditions.append(get_item_group_condition(filters.get("item_group"))) + + items = [] + if conditions: + items = frappe.db.sql_list( + """select name from `tabItem` item where {}""".format(" and ".join(conditions)), + filters, + ) + return items def get_item_details(items, sle, filters): - item_details = {} - if not items: - items = list(set([d.item_code for d in sle])) - - if not items: - return item_details - - cf_field = cf_join = "" - if filters.get("include_uom"): - cf_field = ", ucd.conversion_factor" - cf_join = ( - "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" - % frappe.db.escape(filters.get("include_uom")) - ) - - res = frappe.db.sql( - """ + item_details = {} + if not items: + items = list(set([d.item_code for d in sle])) + + if not items: + return item_details + + cf_field = cf_join = "" + if filters.get("include_uom"): + cf_field = ", ucd.conversion_factor" + cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom={}".format( + frappe.db.escape(filters.get("include_uom")) + ) + + res = frappe.db.sql( + """ select - item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom %s + item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom {} from `tabItem` item - %s + {} where - item.name in (%s) - """ - % (cf_field, cf_join, ",".join(["%s"] * len(items))), - items, - as_dict=1, - ) + item.name in ({}) + """.format(cf_field, cf_join, ",".join(["%s"] * len(items))), + items, + as_dict=1, + ) - for item in res: - item_details.setdefault(item.name, item) + for item in res: + item_details.setdefault(item.name, item) - if filters.get("show_variant_attributes", 0) == 1: - variant_values = get_variant_values_for(list(item_details)) - item_details = { - k: v.update(variant_values.get(k, {})) for k, v in iteritems(item_details) - } + if filters.get("show_variant_attributes", 0) == 1: + variant_values = get_variant_values_for(list(item_details)) + item_details = {k: v.update(variant_values.get(k, {})) for k, v in iteritems(item_details)} - return item_details + return item_details def validate_filters(filters): - if not (filters.get("item_code") or filters.get("warehouse")): - sle_count = flt( - frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0] - ) - if sle_count > 500000: - frappe.throw( - _( - "Please set filter based on Item or Warehouse due to a large amount of entries." - ) - ) + if not (filters.get("item_code") or filters.get("warehouse")): + sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) + if sle_count > 500000: + frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries.")) def get_variants_attributes(): - """Return all item variant attributes.""" - return [i.name for i in frappe.get_all("Item Attribute")] + """Return all item variant attributes.""" + return [i.name for i in frappe.get_all("Item Attribute")] def get_variant_values_for(items): - """Returns variant values for items.""" - attribute_map = {} - for attr in frappe.db.sql( - """select parent, attribute, attribute_value - from `tabItem Variant Attribute` where parent in (%s) - """ - % ", ".join(["%s"] * len(items)), - tuple(items), - as_dict=1, - ): - attribute_map.setdefault(attr["parent"], {}) - attribute_map[attr["parent"]].update( - {attr["attribute"]: attr["attribute_value"]} - ) - - return attribute_map + """Returns variant values for items.""" + attribute_map = {} + for attr in frappe.db.sql( + """select parent, attribute, attribute_value + from `tabItem Variant Attribute` where parent in ({}) + """.format(", ".join(["%s"] * len(items))), + tuple(items), + as_dict=1, + ): + attribute_map.setdefault(attr["parent"], {}) + attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]}) + + return attribute_map diff --git a/csf_tz/csf_tz/report/general_ledger_pro/general_ledger.py b/csf_tz/csf_tz/report/general_ledger_pro/general_ledger.py index ec007061..b30c5b67 100644 --- a/csf_tz/csf_tz/report/general_ledger_pro/general_ledger.py +++ b/csf_tz/csf_tz/report/general_ledger_pro/general_ledger.py @@ -1,17 +1,21 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals -import frappe, erpnext +from collections import OrderedDict + +import frappe from erpnext import get_company_currency, get_default_company -from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency -from frappe.utils import getdate, cstr, flt, fmt_money -from frappe import _, _dict -from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + get_dimension_with_children, +) from erpnext.accounts.report.financial_statements import get_cost_centers_with_children +from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency +from erpnext.accounts.utils import get_account_currency +from frappe import _, _dict +from frappe.utils import cstr, flt, getdate from six import iteritems -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children -from collections import OrderedDict + def execute(filters=None): if not filters: @@ -19,14 +23,13 @@ def execute(filters=None): account_details = {} - if filters and filters.get('print_in_account_currency') and \ - not filters.get('account'): + if filters and filters.get("print_in_account_currency") and not filters.get("account"): frappe.throw(_("Select an account to print in account currency")) for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): account_details.setdefault(acc.name, acc) - if filters.get('party'): + if filters.get("party"): filters.party = frappe.parse_json(filters.get("party")) validate_filters(filters, account_details) @@ -47,27 +50,31 @@ def validate_filters(filters, account_details): frappe.throw(_("{0} is mandatory").format(_("Company"))) if not filters.get("from_date") and not filters.get("to_date"): - frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) + frappe.throw( + _("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))) + ) if filters.get("account") and not account_details.get(filters.account): frappe.throw(_("Account {0} does not exists").format(filters.account)) - if (filters.get("account") and filters.get("group_by") == _('Group by Account') - and account_details[filters.account].is_group == 0): + if ( + filters.get("account") + and filters.get("group_by") == _("Group by Account") + and account_details[filters.account].is_group == 0 + ): frappe.throw(_("Can not filter based on Account, if grouped by Account")) - if (filters.get("voucher_no") - and filters.get("group_by") in [_('Group by Voucher')]): + if filters.get("voucher_no") and filters.get("group_by") in [_("Group by Voucher")]: frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher")) if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) - if filters.get('project'): - filters.project = frappe.parse_json(filters.get('project')) + if filters.get("project"): + filters.project = frappe.parse_json(filters.get("project")) - if filters.get('cost_center'): - filters.cost_center = frappe.parse_json(filters.get('cost_center')) + if filters.get("cost_center"): + filters.cost_center = frappe.parse_json(filters.get("cost_center")) def validate_party(filters): @@ -81,26 +88,29 @@ def validate_party(filters): if not frappe.db.exists(party_type, d): frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) + def set_account_currency(filters): - if filters.get("account") or (filters.get('party') and len(filters.party) == 1): - filters["company_currency"] = frappe.get_cached_value('Company', filters.company, "default_currency") + if filters.get("account") or (filters.get("party") and len(filters.party) == 1): + filters["company_currency"] = frappe.get_cached_value("Company", filters.company, "default_currency") account_currency = None if filters.get("account"): account_currency = get_account_currency(filters.account) elif filters.get("party"): gle_currency = frappe.db.get_value( - "GL Entry", { - "party_type": filters.party_type, "party": filters.party[0], "company": filters.company - }, - "account_currency" + "GL Entry", + {"party_type": filters.party_type, "party": filters.party[0], "company": filters.company}, + "account_currency", ) if gle_currency: account_currency = gle_currency else: - account_currency = (None if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] else - frappe.db.get_value(filters.party_type, filters.party[0], "default_currency")) + account_currency = ( + None + if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] + else frappe.db.get_value(filters.party_type, filters.party[0], "default_currency") + ) filters["account_currency"] = account_currency or filters.company_currency if filters.account_currency != filters.company_currency and not filters.presentation_currency: @@ -108,6 +118,7 @@ def set_account_currency(filters): return filters + def get_result(filters, account_details): gl_entries = get_gl_entries(filters) @@ -117,6 +128,7 @@ def get_result(filters, account_details): return result + def get_gl_entries(filters): currency_map = get_currency(filters) select_fields = """, debit, credit, debit_in_account_currency, @@ -128,26 +140,24 @@ def get_gl_entries(filters): order_by_statement = "order by posting_date, voucher_type, voucher_no" if filters.get("include_default_book_entries"): - filters['company_fb'] = frappe.db.get_value("Company", - filters.get("company"), 'default_finance_book') + filters["company_fb"] = frappe.db.get_value("Company", filters.get("company"), "default_finance_book") gl_entries = frappe.db.sql( - """ + f""" select name as gl_entry, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, project, against_voucher_type, against_voucher, account_currency, remarks, against, is_opening {select_fields} from `tabGL Entry` - where company=%(company)s {conditions} + where company=%(company)s {get_conditions(filters)} {order_by_statement} - """.format( - select_fields=select_fields, conditions=get_conditions(filters), - order_by_statement=order_by_statement - ), - filters, as_dict=1) + """, + filters, + as_dict=1, + ) - if filters.get('presentation_currency'): + if filters.get("presentation_currency"): return convert_to_presentation_currency(gl_entries, currency_map) else: return gl_entries @@ -157,8 +167,10 @@ def get_conditions(filters): conditions = [] if filters.get("account"): lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) - conditions.append("""account in (select name from tabAccount - where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + conditions.append( + f"""account in (select name from tabAccount + where lft>={lft} and rgt<={rgt} and docstatus<2)""" + ) if filters.get("cost_center"): filters.cost_center = get_cost_centers_with_children(filters.cost_center) @@ -176,8 +188,11 @@ def get_conditions(filters): if filters.get("party"): conditions.append("party in %(party)s") - if not (filters.get("account") or filters.get("party") or - filters.get("group_by") in ["Group by Account", "Group by Party"]): + if not ( + filters.get("account") + or filters.get("party") + or filters.get("group_by") in ["Group by Account", "Group by Party"] + ): conditions.append("posting_date >=%(from_date)s") conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") @@ -187,11 +202,14 @@ def get_conditions(filters): if filters.get("finance_book"): if filters.get("include_default_book_entries"): - conditions.append("(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)") + conditions.append( + "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) else: conditions.append("finance_book in (%(finance_book)s)") from frappe.desk.reportview import build_match_conditions + match_conditions = build_match_conditions("GL Entry") if match_conditions: @@ -202,12 +220,13 @@ def get_conditions(filters): if accounting_dimensions: for dimension in accounting_dimensions: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) - conditions.append("{0} in %({0})s".format(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) + conditions.append(f"{dimension.fieldname} in %({dimension.fieldname})s") else: - conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) + conditions.append(f"{dimension.fieldname} in (%({dimension.fieldname})s)") return "and {}".format(" and ".join(conditions)) if conditions else "" @@ -222,8 +241,8 @@ def get_data_with_opening_closing(filters, account_details, gl_entries): # Opening for filtered account data.append(totals.opening) - if filters.get("group_by") != _('Group by Voucher (Consolidated)'): - for acc, acc_dict in iteritems(gle_map): + if filters.get("group_by") != _("Group by Voucher (Consolidated)"): + for _acc, acc_dict in iteritems(gle_map): # acc if acc_dict.entries: # opening @@ -251,32 +270,36 @@ def get_data_with_opening_closing(filters, account_details, gl_entries): return data + def get_totals_dict(): def _get_debit_credit_dict(label): return _dict( - account="'{0}'".format(label), + account=f"'{label}'", debit=0.0, credit=0.0, debit_in_account_currency=0.0, - credit_in_account_currency=0.0 + credit_in_account_currency=0.0, ) + return _dict( - opening = _get_debit_credit_dict(_('Opening')), - total = _get_debit_credit_dict(_('Total')), - closing = _get_debit_credit_dict(_('Closing (Opening + Total)')) + opening=_get_debit_credit_dict(_("Opening")), + total=_get_debit_credit_dict(_("Total")), + closing=_get_debit_credit_dict(_("Closing (Opening + Total)")), ) + def group_by_field(group_by): - if group_by == _('Group by Party'): - return 'party' - elif group_by in [_('Group by Voucher (Consolidated)'), _('Group by Account')]: - return 'account' + if group_by == _("Group by Party"): + return "party" + elif group_by in [_("Group by Voucher (Consolidated)"), _("Group by Account")]: + return "account" else: - return 'voucher_no' + return "voucher_no" + def initialize_gle_map(gl_entries, filters): gle_map = OrderedDict() - group_by = group_by_field(filters.get('group_by')) + group_by = group_by_field(filters.get("group_by")) for gle in gl_entries: gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[])) @@ -287,7 +310,7 @@ def get_accountwise_gle(filters, gl_entries, gle_map): totals = get_totals_dict() entries = [] consolidated_gle = OrderedDict() - group_by = group_by_field(filters.get('group_by')) + group_by = group_by_field(filters.get("group_by")) def update_value_in_dict(data, key, gle): data[key].debit += flt(gle.debit) @@ -297,68 +320,80 @@ def update_value_in_dict(data, key, gle): data[key].credit_in_account_currency += flt(gle.credit_in_account_currency) if data[key].against_voucher and gle.against_voucher: - data[key].against_voucher += ', ' + gle.against_voucher + data[key].against_voucher += ", " + gle.against_voucher from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) for gle in gl_entries: - if (gle.posting_date < from_date or - (cstr(gle.is_opening) == "Yes" and not filters.get("show_opening_entries"))): - update_value_in_dict(gle_map[gle.get(group_by)].totals, 'opening', gle) - update_value_in_dict(totals, 'opening', gle) + if gle.posting_date < from_date or ( + cstr(gle.is_opening) == "Yes" and not filters.get("show_opening_entries") + ): + update_value_in_dict(gle_map[gle.get(group_by)].totals, "opening", gle) + update_value_in_dict(totals, "opening", gle) - update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle) - update_value_in_dict(totals, 'closing', gle) + update_value_in_dict(gle_map[gle.get(group_by)].totals, "closing", gle) + update_value_in_dict(totals, "closing", gle) elif gle.posting_date <= to_date: - update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle) - update_value_in_dict(totals, 'total', gle) - if filters.get("group_by") != _('Group by Voucher (Consolidated)'): + update_value_in_dict(gle_map[gle.get(group_by)].totals, "total", gle) + update_value_in_dict(totals, "total", gle) + if filters.get("group_by") != _("Group by Voucher (Consolidated)"): gle_map[gle.get(group_by)].entries.append(gle) - elif filters.get("group_by") == _('Group by Voucher (Consolidated)'): - key = (gle.get("voucher_type"), gle.get("voucher_no"), - gle.get("account"), gle.get("cost_center")) + elif filters.get("group_by") == _("Group by Voucher (Consolidated)"): + key = ( + gle.get("voucher_type"), + gle.get("voucher_no"), + gle.get("account"), + gle.get("cost_center"), + ) if key not in consolidated_gle: consolidated_gle.setdefault(key, gle) else: update_value_in_dict(consolidated_gle, key, gle) - update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle) - update_value_in_dict(totals, 'closing', gle) + update_value_in_dict(gle_map[gle.get(group_by)].totals, "closing", gle) + update_value_in_dict(totals, "closing", gle) - for key, value in consolidated_gle.items(): + for _key, value in consolidated_gle.items(): entries.append(value) return totals, entries + def get_result_as_list(data, filters): - balance, balance_in_account_currency = 0, 0 + balance = 0 inv_details = get_supplier_invoice_details() for d in data: - if not d.get('posting_date'): - balance, balance_in_account_currency = 0, 0 + if not d.get("posting_date"): + balance = 0 - balance = get_balance(d, balance, 'debit', 'credit') - d['balance'] = balance + balance = get_balance(d, balance, "debit", "credit") + d["balance"] = balance - d['account_currency'] = filters.account_currency - d['bill_no'] = inv_details.get(d.get('against_voucher'), '') + d["account_currency"] = filters.account_currency + d["bill_no"] = inv_details.get(d.get("against_voucher"), "") return data + def get_supplier_invoice_details(): inv_details = {} - for d in frappe.db.sql(""" select name, bill_no from `tabPurchase Invoice` - where docstatus = 1 and bill_no is not null and bill_no != '' """, as_dict=1): + for d in frappe.db.sql( + """ select name, bill_no from `tabPurchase Invoice` + where docstatus = 1 and bill_no is not null and bill_no != '' """, + as_dict=1, + ): inv_details[d.name] = d.bill_no return inv_details + def get_balance(row, balance, debit_field, credit_field): - balance += (row.get(debit_field, 0) - row.get(credit_field, 0)) + balance += row.get(debit_field, 0) - row.get(credit_field, 0) return balance + def get_columns(filters): if filters.get("presentation_currency"): currency = filters["presentation_currency"] @@ -375,104 +410,47 @@ def get_columns(filters): "fieldname": "gl_entry", "fieldtype": "Link", "options": "GL Entry", - "hidden": 1 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 90 + "hidden": 1, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, { "label": _("Account"), "fieldname": "account", "fieldtype": "Link", "options": "Account", - "width": 180 - }, - { - "label": _("Debit ({0})".format(currency)), - "fieldname": "debit", - "fieldtype": "Float", - "width": 100 + "width": 180, }, - { - "label": _("Credit ({0})".format(currency)), - "fieldname": "credit", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Balance ({0})".format(currency)), - "fieldname": "balance", - "fieldtype": "Float", - "width": 130 - } + {"label": _(f"Debit ({currency})"), "fieldname": "debit", "fieldtype": "Float", "width": 100}, + {"label": _(f"Credit ({currency})"), "fieldname": "credit", "fieldtype": "Float", "width": 100}, + {"label": _(f"Balance ({currency})"), "fieldname": "balance", "fieldtype": "Float", "width": 130}, ] - columns.extend([ - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": 120 - }, - { - "label": _("Voucher No"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 180 - }, - { - "label": _("Against Account"), - "fieldname": "against", - "width": 120 - }, - { - "label": _("Party Type"), - "fieldname": "party_type", - "width": 100 - }, - { - "label": _("Party"), - "fieldname": "party", - "width": 100 - }, - { - "label": _("Project"), - "options": "Project", - "fieldname": "project", - "width": 100 - }, - { - "label": _("Cost Center"), - "options": "Cost Center", - "fieldname": "cost_center", - "width": 100 - }, - { - "label": _("Against Voucher Type"), - "fieldname": "against_voucher_type", - "width": 100 - }, - { - "label": _("Against Voucher"), - "fieldname": "against_voucher", - "fieldtype": "Dynamic Link", - "options": "against_voucher_type", - "width": 100 - }, - { - "label": _("Supplier Invoice No"), - "fieldname": "bill_no", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Remarks"), - "fieldname": "remarks", - "width": 400 - } - ]) + columns.extend( + [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120}, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 180, + }, + {"label": _("Against Account"), "fieldname": "against", "width": 120}, + {"label": _("Party Type"), "fieldname": "party_type", "width": 100}, + {"label": _("Party"), "fieldname": "party", "width": 100}, + {"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}, + {"label": _("Cost Center"), "options": "Cost Center", "fieldname": "cost_center", "width": 100}, + {"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100}, + { + "label": _("Against Voucher"), + "fieldname": "against_voucher", + "fieldtype": "Dynamic Link", + "options": "against_voucher_type", + "width": 100, + }, + {"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100}, + {"label": _("Remarks"), "fieldname": "remarks", "width": 400}, + ] + ) return columns diff --git a/csf_tz/csf_tz/report/general_ledger_pro/general_ledger_pro.py b/csf_tz/csf_tz/report/general_ledger_pro/general_ledger_pro.py index 1d54e30a..508434d0 100644 --- a/csf_tz/csf_tz/report/general_ledger_pro/general_ledger_pro.py +++ b/csf_tz/csf_tz/report/general_ledger_pro/general_ledger_pro.py @@ -1,161 +1,161 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals +from collections import OrderedDict + import frappe -import erpnext from erpnext import get_company_currency, get_default_company -from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency -from frappe.utils import getdate, cstr, flt, fmt_money -from frappe import _, _dict -from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + get_dimension_with_children, +) from erpnext.accounts.report.financial_statements import get_cost_centers_with_children +from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency +from erpnext.accounts.utils import get_account_currency +from frappe import _, _dict +from frappe.utils import cstr, flt, getdate from six import iteritems -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children -from collections import OrderedDict def execute(filters=None): - if not filters: - return [], [] + if not filters: + return [], [] - account_details = {} + account_details = {} - if filters and filters.get('print_in_account_currency') and \ - not filters.get('account'): - frappe.throw(_("Select an account to print in account currency")) + if filters and filters.get("print_in_account_currency") and not filters.get("account"): + frappe.throw(_("Select an account to print in account currency")) - for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): - account_details.setdefault(acc.name, acc) + for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): + account_details.setdefault(acc.name, acc) - if filters.get('party'): - filters.party = frappe.parse_json(filters.get("party")) + if filters.get("party"): + filters.party = frappe.parse_json(filters.get("party")) - validate_filters(filters, account_details) + validate_filters(filters, account_details) - validate_party(filters) + validate_party(filters) - filters = set_account_currency(filters) + filters = set_account_currency(filters) - columns = get_columns(filters) + columns = get_columns(filters) - res = get_result(filters, account_details) + res = get_result(filters, account_details) - return columns, res + return columns, res def validate_filters(filters, account_details): - if not filters.get("company"): - frappe.throw(_("{0} is mandatory").format(_("Company"))) + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) - if not filters.get("from_date") and not filters.get("to_date"): - frappe.throw(_("{0} and {1} are mandatory").format( - frappe.bold(_("From Date")), frappe.bold(_("To Date")))) + if not filters.get("from_date") and not filters.get("to_date"): + frappe.throw( + _("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))) + ) - if filters.get("account") and not account_details.get(filters.account): - frappe.throw(_("Account {0} does not exists").format(filters.account)) + if filters.get("account") and not account_details.get(filters.account): + frappe.throw(_("Account {0} does not exists").format(filters.account)) - if (filters.get("account") and filters.get("group_by") == _('Group by Account') - and account_details[filters.account].is_group == 0): - frappe.throw( - _("Can not filter based on Account, if grouped by Account")) + if ( + filters.get("account") + and filters.get("group_by") == _("Group by Account") + and account_details[filters.account].is_group == 0 + ): + frappe.throw(_("Can not filter based on Account, if grouped by Account")) - if (filters.get("voucher_no") - and filters.get("group_by") in [_('Group by Voucher')]): - frappe.throw( - _("Can not filter based on Voucher No, if grouped by Voucher")) + if filters.get("voucher_no") and filters.get("group_by") in [_("Group by Voucher")]: + frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher")) - if filters.from_date > filters.to_date: - frappe.throw(_("From Date must be before To Date")) + if filters.from_date > filters.to_date: + frappe.throw(_("From Date must be before To Date")) - if filters.get('project'): - filters.project = frappe.parse_json(filters.get('project')) + if filters.get("project"): + filters.project = frappe.parse_json(filters.get("project")) - if filters.get('cost_center'): - filters.cost_center = frappe.parse_json(filters.get('cost_center')) + if filters.get("cost_center"): + filters.cost_center = frappe.parse_json(filters.get("cost_center")) def validate_party(filters): - party_type, party = filters.get("party_type"), filters.get("party") + party_type, party = filters.get("party_type"), filters.get("party") - if party: - if not party_type: - frappe.throw( - _("To filter based on Party, select Party Type first")) - else: - for d in party: - if not frappe.db.exists(party_type, d): - frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) + if party: + if not party_type: + frappe.throw(_("To filter based on Party, select Party Type first")) + else: + for d in party: + if not frappe.db.exists(party_type, d): + frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) def set_account_currency(filters): - if filters.get("account") or (filters.get('party') and len(filters.party) == 1): - filters["company_currency"] = frappe.get_cached_value( - 'Company', filters.company, "default_currency") - account_currency = None - - if filters.get("account"): - account_currency = get_account_currency(filters.account) - elif filters.get("party"): - gle_currency = frappe.db.get_value( - "GL Entry", { - "party_type": filters.party_type, "party": filters.party[0], "company": filters.company - }, - "account_currency" - ) - - if gle_currency: - account_currency = gle_currency - else: - account_currency = (None if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] else - frappe.db.get_value(filters.party_type, filters.party[0], "default_currency")) - - filters["account_currency"] = account_currency or filters.company_currency - if filters.account_currency != filters.company_currency and not filters.presentation_currency: - filters.presentation_currency = filters.account_currency - - return filters + if filters.get("account") or (filters.get("party") and len(filters.party) == 1): + filters["company_currency"] = frappe.get_cached_value("Company", filters.company, "default_currency") + account_currency = None + + if filters.get("account"): + account_currency = get_account_currency(filters.account) + elif filters.get("party"): + gle_currency = frappe.db.get_value( + "GL Entry", + {"party_type": filters.party_type, "party": filters.party[0], "company": filters.company}, + "account_currency", + ) + + if gle_currency: + account_currency = gle_currency + else: + account_currency = ( + None + if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] + else frappe.db.get_value(filters.party_type, filters.party[0], "default_currency") + ) + + filters["account_currency"] = account_currency or filters.company_currency + if filters.account_currency != filters.company_currency and not filters.presentation_currency: + filters.presentation_currency = filters.account_currency + + return filters def get_result(filters, account_details): - accounting_dimensions = [] - if filters.get("include_dimensions"): - accounting_dimensions = get_accounting_dimensions() + accounting_dimensions = [] + if filters.get("include_dimensions"): + accounting_dimensions = get_accounting_dimensions() - gl_entries = get_gl_entries(filters, accounting_dimensions) + gl_entries = get_gl_entries(filters, accounting_dimensions) - data = get_data_with_opening_closing(filters, account_details, - accounting_dimensions, gl_entries) + data = get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries) - result = get_result_as_list(data, filters) + result = get_result_as_list(data, filters) - return result + return result def get_gl_entries(filters, accounting_dimensions): - currency_map = get_currency(filters) - select_fields = """, debit, credit, debit_in_account_currency, + currency_map = get_currency(filters) + select_fields = """, debit, credit, debit_in_account_currency, credit_in_account_currency """ - order_by_statement = "order by posting_date, account, creation" + order_by_statement = "order by posting_date, account, creation" - if filters.get("group_by") == _("Group by Voucher"): - order_by_statement = "order by posting_date, voucher_type, voucher_no" + if filters.get("group_by") == _("Group by Voucher"): + order_by_statement = "order by posting_date, voucher_type, voucher_no" - if filters.get("include_default_book_entries"): - filters['company_fb'] = frappe.db.get_value("Company", - filters.get("company"), 'default_finance_book') + if filters.get("include_default_book_entries"): + filters["company_fb"] = frappe.db.get_value("Company", filters.get("company"), "default_finance_book") - dimension_fields = "" - if accounting_dimensions: - dimension_fields = ', '.join(accounting_dimensions) + ',' + dimension_fields = "" + if accounting_dimensions: + dimension_fields = ", ".join(accounting_dimensions) + "," - distributed_cost_center_query = "" - if filters and filters.get('cost_center'): - select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, + distributed_cost_center_query = "" + if filters and filters.get("cost_center"): + select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """ - distributed_cost_center_query = """ + distributed_cost_center_query = """ UNION ALL SELECT name as gl_entry, posting_date, @@ -182,10 +182,14 @@ def get_gl_entries(filters, accounting_dimensions): {conditions} AND posting_date <= %(to_date)s AND cost_center = DCC_allocation.parent - """.format(dimension_fields=dimension_fields, select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) + """.format( + dimension_fields=dimension_fields, + select_fields_with_percentage=select_fields_with_percentage, + conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ""), + ) - gl_entries_all_except_students = frappe.db.sql( - """ + gl_entries_all_except_students = frappe.db.sql( + f""" select gle.name as gl_entry, posting_date, account, party_type, party, voucher_type, voucher_no, {dimension_fields} @@ -193,17 +197,16 @@ def get_gl_entries(filters, accounting_dimensions): against_voucher_type, against_voucher, account_currency, remarks, against, is_opening, gle.creation {select_fields} from `tabGL Entry` as gle - where party != 'Student' and company=%(company)s {conditions} + where party != 'Student' and company=%(company)s {get_conditions(filters)} {distributed_cost_center_query} {order_by_statement} - """.format( - dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, - order_by_statement=order_by_statement - ), - filters, as_dict=1) + """, + filters, + as_dict=1, + ) - gl_entries_students = frappe.db.sql( - """ + gl_entries_students = frappe.db.sql( + f""" select gle.name as gl_entry, posting_date, account, party_type, CONCAT(std.first_name, " ", IFNULL(std.middle_name, ''), " ", IFNULL(std.last_name, '')) as party, voucher_type, voucher_no, {dimension_fields} @@ -212,383 +215,338 @@ def get_gl_entries(filters, accounting_dimensions): remarks, against, is_opening, gle.creation {select_fields} from `tabGL Entry` AS gle INNER JOIN `tabStudent` AS std ON gle.party = std.name - where gle.party_type = 'Student' and company=%(company)s {conditions} + where gle.party_type = 'Student' and company=%(company)s {get_conditions(filters)} {distributed_cost_center_query} {order_by_statement} - """.format( - dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, - order_by_statement=order_by_statement - ), - filters, as_dict=1) + """, + filters, + as_dict=1, + ) - gl_entries = (gl_entries_all_except_students or []) + \ - (gl_entries_students or []) + gl_entries = (gl_entries_all_except_students or []) + (gl_entries_students or []) - if filters.get('presentation_currency'): - return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) - else: - return gl_entries + if filters.get("presentation_currency"): + return convert_to_presentation_currency(gl_entries, currency_map, filters.get("company")) + else: + return gl_entries def get_conditions(filters): - conditions = [] - if filters.get("account"): - lft, rgt = frappe.db.get_value( - "Account", filters["account"], ["lft", "rgt"]) - conditions.append("""account in (select name from tabAccount - where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + conditions = [] + if filters.get("account"): + lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) + conditions.append( + f"""account in (select name from tabAccount + where lft>={lft} and rgt<={rgt} and docstatus<2)""" + ) + + if filters.get("cost_center"): + filters.cost_center = get_cost_centers_with_children(filters.cost_center) + conditions.append("cost_center in %(cost_center)s") - if filters.get("cost_center"): - filters.cost_center = get_cost_centers_with_children( - filters.cost_center) - conditions.append("cost_center in %(cost_center)s") + if filters.get("voucher_no"): + conditions.append("voucher_no=%(voucher_no)s") - if filters.get("voucher_no"): - conditions.append("voucher_no=%(voucher_no)s") + if filters.get("group_by") == "Group by Party" and not filters.get("party_type"): + conditions.append("party_type in ('Customer', 'Supplier')") - if filters.get("group_by") == "Group by Party" and not filters.get("party_type"): - conditions.append("party_type in ('Customer', 'Supplier')") + if filters.get("party_type"): + conditions.append("party_type=%(party_type)s") - if filters.get("party_type"): - conditions.append("party_type=%(party_type)s") + if filters.get("party"): + conditions.append("party in %(party)s") - if filters.get("party"): - conditions.append("party in %(party)s") + if not ( + filters.get("account") + or filters.get("party") + or filters.get("group_by") in ["Group by Account", "Group by Party"] + ): + conditions.append("posting_date >=%(from_date)s") - if not (filters.get("account") or filters.get("party") or - filters.get("group_by") in ["Group by Account", "Group by Party"]): - conditions.append("posting_date >=%(from_date)s") + conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") - conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") + if filters.get("project"): + conditions.append("project in %(project)s") - if filters.get("project"): - conditions.append("project in %(project)s") + if filters.get("finance_book"): + if filters.get("include_default_book_entries"): + conditions.append( + "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) + else: + conditions.append("finance_book in (%(finance_book)s)") - if filters.get("finance_book"): - if filters.get("include_default_book_entries"): - conditions.append( - "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)") - else: - conditions.append("finance_book in (%(finance_book)s)") + if not filters.get("show_cancelled_entries"): + conditions.append("is_cancelled = 0") - if not filters.get("show_cancelled_entries"): - conditions.append("is_cancelled = 0") + from frappe.desk.reportview import build_match_conditions - from frappe.desk.reportview import build_match_conditions - match_conditions = build_match_conditions("GL Entry") + match_conditions = build_match_conditions("GL Entry") - if match_conditions: - conditions.append(match_conditions) + if match_conditions: + conditions.append(match_conditions) - accounting_dimensions = get_accounting_dimensions(as_list=False) + accounting_dimensions = get_accounting_dimensions(as_list=False) - if accounting_dimensions: - for dimension in accounting_dimensions: - if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) - conditions.append( - "{0} in %({0})s".format(dimension.fieldname)) - else: - conditions.append( - "{0} in (%({0})s)".format(dimension.fieldname)) + if accounting_dimensions: + for dimension in accounting_dimensions: + if filters.get(dimension.fieldname): + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) + conditions.append(f"{dimension.fieldname} in %({dimension.fieldname})s") + else: + conditions.append(f"{dimension.fieldname} in (%({dimension.fieldname})s)") - return "and {}".format(" and ".join(conditions)) if conditions else "" + return "and {}".format(" and ".join(conditions)) if conditions else "" def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): - data = [] + data = [] - gle_map = initialize_gle_map(gl_entries, filters) + gle_map = initialize_gle_map(gl_entries, filters) - totals, entries = get_accountwise_gle( - filters, accounting_dimensions, gl_entries, gle_map) + totals, entries = get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map) - # Opening for filtered account - data.append(totals.opening) + # Opening for filtered account + data.append(totals.opening) - if filters.get("group_by") != _('Group by Voucher (Consolidated)'): - for acc, acc_dict in iteritems(gle_map): - # acc - if acc_dict.entries: - # opening - data.append({}) - if filters.get("group_by") != _("Group by Voucher"): - data.append(acc_dict.totals.opening) + if filters.get("group_by") != _("Group by Voucher (Consolidated)"): + for _acc, acc_dict in iteritems(gle_map): + # acc + if acc_dict.entries: + # opening + data.append({}) + if filters.get("group_by") != _("Group by Voucher"): + data.append(acc_dict.totals.opening) - data += acc_dict.entries + data += acc_dict.entries - # totals - data.append(acc_dict.totals.total) + # totals + data.append(acc_dict.totals.total) - # closing - if filters.get("group_by") != _("Group by Voucher"): - data.append(acc_dict.totals.closing) - data.append({}) - else: - data += entries + # closing + if filters.get("group_by") != _("Group by Voucher"): + data.append(acc_dict.totals.closing) + data.append({}) + else: + data += entries - # totals - data.append(totals.total) + # totals + data.append(totals.total) - # closing - data.append(totals.closing) + # closing + data.append(totals.closing) - return data + return data def get_totals_dict(): - def _get_debit_credit_dict(label): - return _dict( - account="'{0}'".format(label), - debit=0.0, - credit=0.0, - debit_in_account_currency=0.0, - credit_in_account_currency=0.0 - ) - return _dict( - opening=_get_debit_credit_dict(_('Opening')), - total=_get_debit_credit_dict(_('Total')), - closing=_get_debit_credit_dict(_('Closing (Opening + Total)')) - ) + def _get_debit_credit_dict(label): + return _dict( + account=f"'{label}'", + debit=0.0, + credit=0.0, + debit_in_account_currency=0.0, + credit_in_account_currency=0.0, + ) + + return _dict( + opening=_get_debit_credit_dict(_("Opening")), + total=_get_debit_credit_dict(_("Total")), + closing=_get_debit_credit_dict(_("Closing (Opening + Total)")), + ) def group_by_field(group_by): - if group_by == _('Group by Party'): - return 'party' - elif group_by in [_('Group by Voucher (Consolidated)'), _('Group by Account')]: - return 'account' - else: - return 'voucher_no' + if group_by == _("Group by Party"): + return "party" + elif group_by in [_("Group by Voucher (Consolidated)"), _("Group by Account")]: + return "account" + else: + return "voucher_no" def initialize_gle_map(gl_entries, filters): - gle_map = OrderedDict() - group_by = group_by_field(filters.get('group_by')) + gle_map = OrderedDict() + group_by = group_by_field(filters.get("group_by")) - for gle in gl_entries: - gle_map.setdefault(gle.get(group_by), _dict( - totals=get_totals_dict(), entries=[])) - return gle_map + for gle in gl_entries: + gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[])) + return gle_map def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): - totals = get_totals_dict() - entries = [] - consolidated_gle = OrderedDict() - group_by = group_by_field(filters.get('group_by')) - - def update_value_in_dict(data, key, gle): - data[key].debit += flt(gle.debit) - data[key].credit += flt(gle.credit) - - data[key].debit_in_account_currency += flt( - gle.debit_in_account_currency) - data[key].credit_in_account_currency += flt( - gle.credit_in_account_currency) - - if data[key].against_voucher and gle.against_voucher: - data[key].against_voucher += ', ' + gle.against_voucher - - from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) - for gle in gl_entries: - if (gle.posting_date < from_date or - (cstr(gle.is_opening) == "Yes" and not filters.get("show_opening_entries"))): - update_value_in_dict( - gle_map[gle.get(group_by)].totals, 'opening', gle) - update_value_in_dict(totals, 'opening', gle) - - update_value_in_dict( - gle_map[gle.get(group_by)].totals, 'closing', gle) - update_value_in_dict(totals, 'closing', gle) - - elif gle.posting_date <= to_date: - update_value_in_dict( - gle_map[gle.get(group_by)].totals, 'total', gle) - update_value_in_dict(totals, 'total', gle) - if filters.get("group_by") != _('Group by Voucher (Consolidated)'): - gle_map[gle.get(group_by)].entries.append(gle) - elif filters.get("group_by") == _('Group by Voucher (Consolidated)'): - keylist = [gle.get("voucher_type"), gle.get( - "voucher_no"), gle.get("account")] - for dim in accounting_dimensions: - keylist.append(gle.get(dim)) - keylist.append(gle.get("cost_center")) - key = tuple(keylist) - if key not in consolidated_gle: - consolidated_gle.setdefault(key, gle) - else: - update_value_in_dict(consolidated_gle, key, gle) - - update_value_in_dict( - gle_map[gle.get(group_by)].totals, 'closing', gle) - update_value_in_dict(totals, 'closing', gle) - - for key, value in consolidated_gle.items(): - entries.append(value) - - return totals, entries + totals = get_totals_dict() + entries = [] + consolidated_gle = OrderedDict() + group_by = group_by_field(filters.get("group_by")) + + def update_value_in_dict(data, key, gle): + data[key].debit += flt(gle.debit) + data[key].credit += flt(gle.credit) + + data[key].debit_in_account_currency += flt(gle.debit_in_account_currency) + data[key].credit_in_account_currency += flt(gle.credit_in_account_currency) + + if data[key].against_voucher and gle.against_voucher: + data[key].against_voucher += ", " + gle.against_voucher + + from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) + for gle in gl_entries: + if gle.posting_date < from_date or ( + cstr(gle.is_opening) == "Yes" and not filters.get("show_opening_entries") + ): + update_value_in_dict(gle_map[gle.get(group_by)].totals, "opening", gle) + update_value_in_dict(totals, "opening", gle) + + update_value_in_dict(gle_map[gle.get(group_by)].totals, "closing", gle) + update_value_in_dict(totals, "closing", gle) + + elif gle.posting_date <= to_date: + update_value_in_dict(gle_map[gle.get(group_by)].totals, "total", gle) + update_value_in_dict(totals, "total", gle) + if filters.get("group_by") != _("Group by Voucher (Consolidated)"): + gle_map[gle.get(group_by)].entries.append(gle) + elif filters.get("group_by") == _("Group by Voucher (Consolidated)"): + keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] + for dim in accounting_dimensions: + keylist.append(gle.get(dim)) + keylist.append(gle.get("cost_center")) + key = tuple(keylist) + if key not in consolidated_gle: + consolidated_gle.setdefault(key, gle) + else: + update_value_in_dict(consolidated_gle, key, gle) + + update_value_in_dict(gle_map[gle.get(group_by)].totals, "closing", gle) + update_value_in_dict(totals, "closing", gle) + + for _key, value in consolidated_gle.items(): + entries.append(value) + + return totals, entries def get_result_as_list(data, filters): - balance, balance_in_account_currency = 0, 0 - inv_details = get_supplier_invoice_details() + balance = 0 + inv_details = get_supplier_invoice_details() - for d in data: - if not d.get('posting_date'): - balance, balance_in_account_currency = 0, 0 + for d in data: + if not d.get("posting_date"): + balance = 0 - balance = get_balance(d, balance, 'debit', 'credit') - d['balance'] = balance + balance = get_balance(d, balance, "debit", "credit") + d["balance"] = balance - d['account_currency'] = filters.account_currency - d['bill_no'] = inv_details.get(d.get('against_voucher'), '') + d["account_currency"] = filters.account_currency + d["bill_no"] = inv_details.get(d.get("against_voucher"), "") - return data + return data def get_supplier_invoice_details(): - inv_details = {} - for d in frappe.db.sql(""" select name, bill_no from `tabPurchase Invoice` - where docstatus = 1 and bill_no is not null and bill_no != '' """, as_dict=1): - inv_details[d.name] = d.bill_no + inv_details = {} + for d in frappe.db.sql( + """ select name, bill_no from `tabPurchase Invoice` + where docstatus = 1 and bill_no is not null and bill_no != '' """, + as_dict=1, + ): + inv_details[d.name] = d.bill_no - return inv_details + return inv_details def get_balance(row, balance, debit_field, credit_field): - balance += (row.get(debit_field, 0) - row.get(credit_field, 0)) + balance += row.get(debit_field, 0) - row.get(credit_field, 0) - return balance + return balance def get_columns(filters): - if filters.get("presentation_currency"): - currency = filters["presentation_currency"] - else: - if filters.get("company"): - currency = get_company_currency(filters["company"]) - else: - company = get_default_company() - currency = get_company_currency(company) - - columns = [ - { - "label": _("GL Entry"), - "fieldname": "gl_entry", - "fieldtype": "Link", - "options": "GL Entry", - "hidden": 1 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 90 - }, - { - "label": _("Account"), - "fieldname": "account", - "fieldtype": "Link", - "options": "Account", - "width": 180 - }, - { - "label": _("Debit ({0})").format(currency), - "fieldname": "debit", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Credit ({0})").format(currency), - "fieldname": "credit", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Balance ({0})").format(currency), - "fieldname": "balance", - "fieldtype": "Float", - "width": 130 - } - ] - - columns.extend([ - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": 120 - }, - { - "label": _("Voucher No"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 180 - }, - { - "label": _("Against Account"), - "fieldname": "against", - "width": 120 - }, - { - "label": _("Party Type"), - "fieldname": "party_type", - "width": 100 - }, - { - "label": _("Party"), - "fieldname": "party", - "width": 100 - }, - { - "label": _("Project"), - "options": "Project", - "fieldname": "project", - "width": 100 - } - ]) - - if filters.get("include_dimensions"): - for dim in get_accounting_dimensions(as_list=False): - columns.append({ - "label": _(dim.label), - "options": dim.label, - "fieldname": dim.fieldname, - "width": 100 - }) - - columns.extend([ - { - "label": _("Cost Center"), - "options": "Cost Center", - "fieldname": "cost_center", - "width": 100 - }, - { - "label": _("Against Voucher Type"), - "fieldname": "against_voucher_type", - "width": 100 - }, - { - "label": _("Against Voucher"), - "fieldname": "against_voucher", - "fieldtype": "Dynamic Link", - "options": "against_voucher_type", - "width": 100 - }, - { - "label": _("Supplier Invoice No"), - "fieldname": "bill_no", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Remarks"), - "fieldname": "remarks", - "width": 400 - } - ]) - - return columns + if filters.get("presentation_currency"): + currency = filters["presentation_currency"] + else: + if filters.get("company"): + currency = get_company_currency(filters["company"]) + else: + company = get_default_company() + currency = get_company_currency(company) + + columns = [ + { + "label": _("GL Entry"), + "fieldname": "gl_entry", + "fieldtype": "Link", + "options": "GL Entry", + "hidden": 1, + }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, + { + "label": _("Account"), + "fieldname": "account", + "fieldtype": "Link", + "options": "Account", + "width": 180, + }, + { + "label": _("Debit ({0})").format(currency), + "fieldname": "debit", + "fieldtype": "Float", + "width": 100, + }, + { + "label": _("Credit ({0})").format(currency), + "fieldname": "credit", + "fieldtype": "Float", + "width": 100, + }, + { + "label": _("Balance ({0})").format(currency), + "fieldname": "balance", + "fieldtype": "Float", + "width": 130, + }, + ] + + columns.extend( + [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120}, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 180, + }, + {"label": _("Against Account"), "fieldname": "against", "width": 120}, + {"label": _("Party Type"), "fieldname": "party_type", "width": 100}, + {"label": _("Party"), "fieldname": "party", "width": 100}, + {"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}, + ] + ) + + if filters.get("include_dimensions"): + for dim in get_accounting_dimensions(as_list=False): + columns.append( + {"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100} + ) + + columns.extend( + [ + {"label": _("Cost Center"), "options": "Cost Center", "fieldname": "cost_center", "width": 100}, + {"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100}, + { + "label": _("Against Voucher"), + "fieldname": "against_voucher", + "fieldtype": "Dynamic Link", + "options": "against_voucher_type", + "width": 100, + }, + {"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100}, + {"label": _("Remarks"), "fieldname": "remarks", "width": 400}, + ] + ) + + return columns diff --git a/csf_tz/csf_tz/report/gross_profit_pro/gross_profit_pro.py b/csf_tz/csf_tz/report/gross_profit_pro/gross_profit_pro.py index 66035e31..e679b6ca 100644 --- a/csf_tz/csf_tz/report/gross_profit_pro/gross_profit_pro.py +++ b/csf_tz/csf_tz/report/gross_profit_pro/gross_profit_pro.py @@ -1,45 +1,131 @@ # Copyright (c) 2021, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe -from frappe import _, scrub -from erpnext.stock.utils import get_incoming_rate from erpnext.controllers.queries import get_match_cond -from frappe.utils import flt, cint - +from erpnext.stock.utils import get_incoming_rate +from frappe import _, scrub +from frappe.utils import cint, flt from csf_tz import console + def execute(filters=None): - if not filters: filters = frappe._dict() - filters.currency = frappe.get_cached_value('Company', filters.company, "default_currency") + if not filters: + filters = frappe._dict() + filters.currency = frappe.get_cached_value("Company", filters.company, "default_currency") gross_profit_data = GrossProfitGenerator(filters) data = [] - group_wise_columns = frappe._dict({ - "invoice": ["parent", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", \ - "warehouse", "qty", "base_rate", "buying_rate", "base_amount", - "buying_amount", "gross_profit", "gross_profit_percent", "project"], - "item_code": ["item_code", "item_name", "brand", "description", "qty", "base_rate", - "buying_rate", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], - "warehouse": ["warehouse", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "brand": ["brand", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "item_group": ["item_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "customer": ["customer", "customer_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "customer_group": ["customer_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "sales_person": ["sales_person", "allocated_amount", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], - "territory": ["territory", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"] - }) + group_wise_columns = frappe._dict( + { + "invoice": [ + "parent", + "customer", + "customer_group", + "posting_date", + "item_code", + "item_name", + "item_group", + "brand", + "description", + "warehouse", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + "project", + ], + "item_code": [ + "item_code", + "item_name", + "brand", + "description", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "warehouse": [ + "warehouse", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "brand": [ + "brand", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "item_group": [ + "item_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "customer": [ + "customer", + "customer_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "customer_group": [ + "customer_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "sales_person": [ + "sales_person", + "allocated_amount", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], + "territory": [ + "territory", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + } + ) columns = get_columns(group_wise_columns, filters) @@ -49,53 +135,59 @@ def execute(filters=None): row.append(src.get(col)) row.append(filters.currency) - if idx == len(gross_profit_data.grouped_data)-1: + if idx == len(gross_profit_data.grouped_data) - 1: row[0] = frappe.bold("Total") data.append(row) return columns, data + def get_columns(group_wise_columns, filters): columns = [] - column_map = frappe._dict({ - "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date:100", - "posting_time": _("Posting Time") + ":Data:100", - "item_code": _("Item Code") + ":Link/Item:100", - "item_name": _("Item Name") + ":Data:100", - "item_group": _("Item Group") + ":Link/Item Group:100", - "brand": _("Brand") + ":Link/Brand:100", - "description": _("Description") +":Data:100", - "warehouse": _("Warehouse") + ":Link/Warehouse:100", - "qty": _("Qty") + ":Float:80", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", - "buying_rate": _("Valuation Rate") + ":Currency/currency:100", - "base_amount": _("Selling Amount") + ":Currency/currency:100", - "buying_amount": _("Buying Amount") + ":Currency/currency:100", - "gross_profit": _("Gross Profit") + ":Currency/currency:100", - "gross_profit_percent": _("Gross Profit %") + ":Percent:100", - "project": _("Project") + ":Link/Project:100", - "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", - "customer": _("Customer") + ":Link/Customer:100", - "customer_group": _("Customer Group") + ":Link/Customer Group:100", - "territory": _("Territory") + ":Link/Territory:100" - }) + column_map = frappe._dict( + { + "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", + "posting_date": _("Posting Date") + ":Date:100", + "posting_time": _("Posting Time") + ":Data:100", + "item_code": _("Item Code") + ":Link/Item:100", + "item_name": _("Item Name") + ":Data:100", + "item_group": _("Item Group") + ":Link/Item Group:100", + "brand": _("Brand") + ":Link/Brand:100", + "description": _("Description") + ":Data:100", + "warehouse": _("Warehouse") + ":Link/Warehouse:100", + "qty": _("Qty") + ":Float:80", + "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", + "buying_rate": _("Valuation Rate") + ":Currency/currency:100", + "base_amount": _("Selling Amount") + ":Currency/currency:100", + "buying_amount": _("Buying Amount") + ":Currency/currency:100", + "gross_profit": _("Gross Profit") + ":Currency/currency:100", + "gross_profit_percent": _("Gross Profit %") + ":Percent:100", + "project": _("Project") + ":Link/Project:100", + "sales_person": _("Sales person"), + "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", + "customer": _("Customer") + ":Link/Customer:100", + "customer_group": _("Customer Group") + ":Link/Customer Group:100", + "territory": _("Territory") + ":Link/Territory:100", + } + ) for col in group_wise_columns.get(scrub(filters.group_by)): columns.append(column_map.get(col)) - columns.append({ - "fieldname": "currency", - "label" : _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "hidden": 1 - }) + columns.append( + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1, + } + ) return columns -class GrossProfitGenerator(object): + +class GrossProfitGenerator: def __init__(self, filters=None): self.data = [] self.average_buying_rate = {} @@ -124,17 +216,19 @@ def process(self): if row.update_stock: product_bundles = self.product_bundles.get(row.parenttype, {}).get(row.parent, frappe._dict()) elif row.dn_detail: - product_bundles = self.product_bundles.get("Delivery Note", {})\ - .get(row.delivery_note, frappe._dict()) + product_bundles = self.product_bundles.get("Delivery Note", {}).get( + row.delivery_note, frappe._dict() + ) row.item_row = row.dn_detail # get buying amount if row.item_code in product_bundles: - row.buying_amount = flt(self.get_buying_amount_from_product_bundle(row, - product_bundles[row.item_code]), self.currency_precision) + row.buying_amount = flt( + self.get_buying_amount_from_product_bundle(row, product_bundles[row.item_code]), + self.currency_precision, + ) else: - row.buying_amount = flt(self.get_buying_amount(row, row.item_code), - self.currency_precision) + row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision) # get buying rate if row.qty: @@ -146,7 +240,9 @@ def process(self): # calculate gross profit row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision) if row.base_amount: - row.gross_profit_percent = flt((row.gross_profit / row.base_amount) * 100.0, self.currency_precision) + row.gross_profit_percent = flt( + (row.gross_profit / row.base_amount) * 100.0, self.currency_precision + ) else: row.gross_profit_percent = 0.0 @@ -165,12 +261,12 @@ def get_average_rate_based_on_group_by(self): gross_profit=0, gross_profit_percent=0, base_rate=0, - buying_rate=0 + buying_rate=0, ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): - if i==0: + if i == 0: new_row = row else: new_row.qty += row.qty @@ -180,9 +276,11 @@ def get_average_rate_based_on_group_by(self): self.grouped_data.append(new_row) self.add_to_totals(new_row) else: - for i, row in enumerate(self.grouped[key]): - if row.parent in self.returned_invoices \ - and row.item_code in self.returned_invoices[row.parent]: + for _i, row in enumerate(self.grouped[key]): + if ( + row.parent in self.returned_invoices + and row.item_code in self.returned_invoices[row.parent] + ): returned_item_rows = self.returned_invoices[row.parent][row.item_code] for returned_item_row in returned_item_rows: row.qty += returned_item_row.qty @@ -197,14 +295,19 @@ def get_average_rate_based_on_group_by(self): def set_average_rate(self, new_row): self.set_average_gross_profit(new_row) - new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + new_row.buying_rate = ( + flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + ) new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 return new_row def set_average_gross_profit(self, new_row): new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) - new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ - if new_row.base_amount else 0 + new_row.gross_profit_percent = ( + flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) + if new_row.base_amount + else 0 + ) def add_to_totals(self, new_row): for key in self.totals: @@ -212,7 +315,8 @@ def add_to_totals(self, new_row): self.totals[key] += new_row[key] def get_returned_invoice_items(self): - returned_invoices = frappe.db.sql(""" + returned_invoices = frappe.db.sql( + """ select si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against from @@ -221,12 +325,15 @@ def get_returned_invoice_items(self): si.name = si_item.parent and si.docstatus = 1 and si.is_return = 1 - """, as_dict=1) + """, + as_dict=1, + ) self.returned_invoices = frappe._dict() for inv in returned_invoices: - self.returned_invoices.setdefault(inv.return_against, frappe._dict())\ - .setdefault(inv.item_code, []).append(inv) + self.returned_invoices.setdefault(inv.return_against, frappe._dict()).setdefault( + inv.item_code, [] + ).append(inv) def skip_row(self, row, product_bundles): if self.filters.get("group_by") != "Invoice": @@ -238,7 +345,7 @@ def skip_row(self, row, product_bundles): def get_buying_amount_from_product_bundle(self, row, product_bundle): buying_amount = 0.0 for packed_item in product_bundle: - if packed_item.get("parent_detail_docname")==row.item_row: + if packed_item.get("parent_detail_docname") == row.item_row: buying_amount += self.get_buying_amount(row, packed_item.item_code) return flt(buying_amount, self.currency_precision) @@ -249,7 +356,7 @@ def get_buying_amount(self, row, item_code): # stock_ledger_entries should already be filtered by item_code and warehouse and # sorted by posting_date desc, posting_time desc if item_code in self.non_stock_items and (row.project or row.cost_center): - #Issue 6089-Get last purchasing rate for non-stock item + # Issue 6089-Get last purchasing rate for non-stock item item_rate = self.get_last_purchase_rate(item_code, row) console("non stock items item_rate", item_rate) @@ -264,17 +371,31 @@ def get_buying_amount(self, row, item_code): for i, sle in enumerate(my_sle): # find the stock valution rate from stock ledger entry - if sle.voucher_type == parenttype and parent == sle.voucher_no and \ - sle.voucher_detail_no == row.item_row: - previous_stock_value = len(my_sle) > i+1 and \ - flt(my_sle[i+1].stock_value) or 0.0 - - if previous_stock_value: - return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) - else: - return flt(row.qty) * self.get_average_buying_rate(row, item_code) + if ( + sle.voucher_type == parenttype + and parent == sle.voucher_no + and sle.voucher_detail_no == row.item_row + ): + previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 + + if previous_stock_value: + return ( + (previous_stock_value - flt(sle.stock_value)) + * flt(row.qty) + / abs(flt(sle.qty)) + ) + else: + return flt(row.qty) * self.get_average_buying_rate(row, item_code) else: - console("unknown criteria hit", sle.voucher_type, parenttype, parent, sle.voucher_no, sle.voucher_detail_no, row.item_row) + console( + "unknown criteria hit", + sle.voucher_type, + parenttype, + parent, + sle.voucher_no, + sle.voucher_detail_no, + row.item_row, + ) else: console("NO update stock or dn detail and no my_sel") return flt(row.qty) * self.get_average_buying_rate(row, item_code) @@ -284,34 +405,39 @@ def get_buying_amount(self, row, item_code): def get_average_buying_rate(self, row, item_code): args = row - if not item_code in self.average_buying_rate: - args.update({ - 'voucher_type': row.parenttype, - 'voucher_no': row.parent, - 'allow_zero_valuation': True, - 'company': self.filters.company - }) + if item_code not in self.average_buying_rate: + args.update( + { + "voucher_type": row.parenttype, + "voucher_no": row.parent, + "allow_zero_valuation": True, + "company": self.filters.company, + } + ) average_buying_rate = get_incoming_rate(args) - self.average_buying_rate[item_code] = flt(average_buying_rate) + self.average_buying_rate[item_code] = flt(average_buying_rate) return self.average_buying_rate[item_code] def get_last_purchase_rate(self, item_code, row): - condition = '' + condition = "" if row.project: - condition += " AND a.project=%s" % (frappe.db.escape(row.project)) + condition += f" AND a.project={frappe.db.escape(row.project)}" elif row.cost_center: - condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) + condition += f" AND a.cost_center={frappe.db.escape(row.cost_center)}" if self.filters.to_date: - condition += " AND modified='%s'" % (self.filters.to_date) + condition += f" AND modified='{self.filters.to_date}'" - last_purchase_rate = frappe.db.sql(""" + last_purchase_rate = frappe.db.sql( + f""" select (a.base_rate / a.conversion_factor) from `tabPurchase Invoice Item` a where a.item_code = %s and a.docstatus=1 - {0} - order by a.modified desc limit 1""".format(condition), item_code) + {condition} + order by a.modified desc limit 1""", + item_code, + ) return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 @@ -324,7 +450,7 @@ def load_invoice_items(self): if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" - if self.filters.group_by=="Sales Person": + if self.filters.group_by == "Sales Person": sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" else: @@ -337,7 +463,8 @@ def load_invoice_items(self): if self.filters.get("item_code"): conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" - self.si_list = frappe.db.sql(""" + self.si_list = frappe.db.sql( + """ select `tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time, @@ -359,18 +486,28 @@ def load_invoice_items(self): where `tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond} order by - `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""" - .format(conditions=conditions, sales_person_cols=sales_person_cols, - sales_team_table=sales_team_table, match_cond = get_match_cond('Sales Invoice')), self.filters, as_dict=1) + `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format( + conditions=conditions, + sales_person_cols=sales_person_cols, + sales_team_table=sales_team_table, + match_cond=get_match_cond("Sales Invoice"), + ), + self.filters, + as_dict=1, + ) def load_stock_ledger_entries(self): - res = frappe.db.sql("""select item_code, voucher_type, voucher_no, + res = frappe.db.sql( + """select item_code, voucher_type, voucher_no, voucher_detail_no, stock_value, warehouse, actual_qty as qty from `tabStock Ledger Entry` where company=%(company)s and is_cancelled = 0 order by item_code desc, warehouse desc, posting_date desc, - posting_time desc, creation desc""", self.filters, as_dict=True) + posting_time desc, creation desc""", + self.filters, + as_dict=True, + ) self.sle = {} for r in res: if (r.item_code, r.warehouse) not in self.sle: @@ -381,12 +518,16 @@ def load_stock_ledger_entries(self): def load_product_bundle(self): self.product_bundles = {} - for d in frappe.db.sql("""select parenttype, parent, parent_item, + for d in frappe.db.sql( + """select parenttype, parent, parent_item, item_code, warehouse, -1*qty as total_qty, parent_detail_docname - from `tabPacked Item` where docstatus=1""", as_dict=True): - self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault(d.parent, - frappe._dict()).setdefault(d.parent_item, []).append(d) + from `tabPacked Item` where docstatus=1""", + as_dict=True, + ): + self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault( + d.parent, frappe._dict() + ).setdefault(d.parent_item, []).append(d) def load_non_stock_items(self): self.non_stock_items = frappe.db.sql_list("""select name from tabItem - where is_stock_item=0""") \ No newline at end of file + where is_stock_item=0""") diff --git a/csf_tz/csf_tz/report/import_exchange_differences/import_exchange_differences.py b/csf_tz/csf_tz/report/import_exchange_differences/import_exchange_differences.py index c61bc9e0..d003afe8 100644 --- a/csf_tz/csf_tz/report/import_exchange_differences/import_exchange_differences.py +++ b/csf_tz/csf_tz/report/import_exchange_differences/import_exchange_differences.py @@ -3,114 +3,103 @@ import frappe from frappe import _ -from frappe.utils import flt, getdate, cstr def execute(filters=None): - if not filters: - filters = {} - - columns = get_columns() - data = get_data(filters) - - return columns, data + if not filters: + filters = {} + + columns = get_columns() + data = get_data(filters) + + return columns, data def get_columns(): - return [ - { - "fieldname": "foreign_import_transaction", - "label": _("Import Transaction"), - "fieldtype": "Link", - "options": "Foreign Import Transaction", - "width": 150 - }, - { - "fieldname": "purchase_invoice", - "label": _("Purchase Invoice"), - "fieldtype": "Link", - "options": "Purchase Invoice", - "width": 150 - }, - { - "fieldname": "supplier", - "label": _("Supplier"), - "fieldtype": "Link", - "options": "Supplier", - "width": 150 - }, - { - "fieldname": "transaction_date", - "label": _("Transaction Date"), - "fieldtype": "Date", - "width": 100 - }, - { - "fieldname": "currency", - "label": _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "width": 80 - }, - { - "fieldname": "original_exchange_rate", - "label": _("Original Rate"), - "fieldtype": "Float", - "precision": 4, - "width": 100 - }, - { - "fieldname": "invoice_amount_foreign", - "label": _("Invoice Amount (Foreign)"), - "fieldtype": "Currency", - "options": "currency", - "width": 150 - }, - { - "fieldname": "invoice_amount_base", - "label": _("Invoice Amount (Base)"), - "fieldtype": "Currency", - "width": 150 - }, - { - "fieldname": "total_payments", - "label": _("Total Payments"), - "fieldtype": "Currency", - "options": "currency", - "width": 120 - }, - { - "fieldname": "payment_differences", - "label": _("Payment Differences"), - "fieldtype": "Currency", - "width": 120 - }, - { - "fieldname": "lcv_differences", - "label": _("LCV Differences"), - "fieldtype": "Currency", - "width": 120 - }, - { - "fieldname": "total_gain_loss", - "label": _("Total Gain/Loss"), - "fieldtype": "Currency", - "width": 120 - }, - { - "fieldname": "status", - "label": _("Status"), - "fieldtype": "Data", - "width": 100 - } - ] + return [ + { + "fieldname": "foreign_import_transaction", + "label": _("Import Transaction"), + "fieldtype": "Link", + "options": "Foreign Import Transaction", + "width": 150, + }, + { + "fieldname": "purchase_invoice", + "label": _("Purchase Invoice"), + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 150, + }, + { + "fieldname": "supplier", + "label": _("Supplier"), + "fieldtype": "Link", + "options": "Supplier", + "width": 150, + }, + {"fieldname": "transaction_date", "label": _("Transaction Date"), "fieldtype": "Date", "width": 100}, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "width": 80, + }, + { + "fieldname": "original_exchange_rate", + "label": _("Original Rate"), + "fieldtype": "Float", + "precision": 4, + "width": 100, + }, + { + "fieldname": "invoice_amount_foreign", + "label": _("Invoice Amount (Foreign)"), + "fieldtype": "Currency", + "options": "currency", + "width": 150, + }, + { + "fieldname": "invoice_amount_base", + "label": _("Invoice Amount (Base)"), + "fieldtype": "Currency", + "width": 150, + }, + { + "fieldname": "total_payments", + "label": _("Total Payments"), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + }, + { + "fieldname": "payment_differences", + "label": _("Payment Differences"), + "fieldtype": "Currency", + "width": 120, + }, + { + "fieldname": "lcv_differences", + "label": _("LCV Differences"), + "fieldtype": "Currency", + "width": 120, + }, + { + "fieldname": "total_gain_loss", + "label": _("Total Gain/Loss"), + "fieldtype": "Currency", + "width": 120, + }, + {"fieldname": "status", "label": _("Status"), "fieldtype": "Data", "width": 100}, + ] def get_data(filters): - conditions = get_conditions(filters) - - query = """ - SELECT + conditions = get_conditions(filters) + + query = f""" + SELECT fit.name as foreign_import_transaction, fit.purchase_invoice, fit.supplier, @@ -126,7 +115,7 @@ def get_data(filters): COALESCE(lcv_summary.lcv_differences, 0) as lcv_differences FROM `tabForeign Import Transaction` fit LEFT JOIN ( - SELECT + SELECT parent, SUM(payment_amount_foreign) as total_payments, SUM(exchange_difference) as payment_differences @@ -134,7 +123,7 @@ def get_data(filters): GROUP BY parent ) payment_summary ON payment_summary.parent = fit.name LEFT JOIN ( - SELECT + SELECT parent, SUM(exchange_difference) as lcv_differences FROM `tabForeign Import LCV Details` @@ -142,35 +131,35 @@ def get_data(filters): ) lcv_summary ON lcv_summary.parent = fit.name WHERE fit.docstatus = 1 {conditions} ORDER BY fit.transaction_date DESC, fit.name - """.format(conditions=conditions) - - data = frappe.db.sql(query, filters, as_dict=1) - - return data + """ + + data = frappe.db.sql(query, filters, as_dict=1) + + return data def get_conditions(filters): - conditions = [] - - if filters.get("company"): - conditions.append("AND fit.company = %(company)s") - - if filters.get("from_date"): - conditions.append("AND fit.transaction_date >= %(from_date)s") - - if filters.get("to_date"): - conditions.append("AND fit.transaction_date <= %(to_date)s") - - if filters.get("purchase_invoice"): - conditions.append("AND fit.purchase_invoice = %(purchase_invoice)s") - - if filters.get("supplier"): - conditions.append("AND fit.supplier = %(supplier)s") - - if filters.get("currency"): - conditions.append("AND fit.currency = %(currency)s") - - if filters.get("status"): - conditions.append("AND fit.status = %(status)s") - - return " ".join(conditions) + conditions = [] + + if filters.get("company"): + conditions.append("AND fit.company = %(company)s") + + if filters.get("from_date"): + conditions.append("AND fit.transaction_date >= %(from_date)s") + + if filters.get("to_date"): + conditions.append("AND fit.transaction_date <= %(to_date)s") + + if filters.get("purchase_invoice"): + conditions.append("AND fit.purchase_invoice = %(purchase_invoice)s") + + if filters.get("supplier"): + conditions.append("AND fit.supplier = %(supplier)s") + + if filters.get("currency"): + conditions.append("AND fit.currency = %(currency)s") + + if filters.get("status"): + conditions.append("AND fit.status = %(status)s") + + return " ".join(conditions) diff --git a/csf_tz/csf_tz/report/item_price_by_price_list/item_price_by_price_list.js b/csf_tz/csf_tz/report/item_price_by_price_list/item_price_by_price_list.js index 404bcfb2..c5592ea2 100644 --- a/csf_tz/csf_tz/report/item_price_by_price_list/item_price_by_price_list.js +++ b/csf_tz/csf_tz/report/item_price_by_price_list/item_price_by_price_list.js @@ -25,4 +25,4 @@ frappe.query_reports["Item Price by Price List"] = { "options": "Barcode" }, ] -} \ No newline at end of file +} diff --git a/csf_tz/csf_tz/report/item_price_by_price_list/item_price_by_price_list.py b/csf_tz/csf_tz/report/item_price_by_price_list/item_price_by_price_list.py index 15a0b845..d51bdf77 100644 --- a/csf_tz/csf_tz/report/item_price_by_price_list/item_price_by_price_list.py +++ b/csf_tz/csf_tz/report/item_price_by_price_list/item_price_by_price_list.py @@ -7,95 +7,98 @@ def execute(filters=None): - columns = get_columns() - data = get_data(filters) - return columns, data + columns = get_columns() + data = get_data(filters) + return columns, data def get_columns(): - # Dynamically fetch Price Lists for dynamic column creation - price_lists = db.get_list("Price List", filters={"selling": 1}, pluck="name") - price_list_columns = [ - { - "label": _(f"{price_list} Excl"), - "fieldname": f"rate_{price_list.replace(' ', '_').lower()}_excl", - "fieldtype": "Currency", - } - for price_list in price_lists - ] - price_list_columns += [ - { - "label": _(f"{price_list} Rate"), - "fieldname": f"rate_{price_list.replace(' ', '_').lower()}", - "fieldtype": "Currency", - } - for price_list in price_lists - ] - columns = ( - [ - { - "label": _("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - }, - { - "label": _("Item Description"), - "fieldname": "description", - "fieldtype": "Data", - }, - { - "label": _("Default Item Tax Template"), - "fieldname": "default_tax_template", - "fieldtype": "Link", - "options": "Item Tax Template", - }, - ] - + price_list_columns - + [ - { - "label": _("Total Qty"), - "fieldname": "total_qty", - "fieldtype": "Float", - }, - { - "label": _("Last Purchase Rate"), - "fieldname": "last_purchase_rate", - "fieldtype": "Currency", - }, - { - "label": _("Valuation Rate"), - "fieldname": "valuation_rate", - "fieldtype": "Currency", - }, - { - "label": _("Warehouse Qty"), - "fieldname": "warehouse_qty", - "fieldtype": "Data", - }, - ] - ) - return columns + # Dynamically fetch Price Lists for dynamic column creation + price_lists = db.get_list("Price List", filters={"selling": 1}, pluck="name") + price_list_columns = [ + { + "label": _(f"{price_list} Excl"), + "fieldname": f"rate_{price_list.replace(' ', '_').lower()}_excl", + "fieldtype": "Currency", + } + for price_list in price_lists + ] + price_list_columns += [ + { + "label": _(f"{price_list} Rate"), + "fieldname": f"rate_{price_list.replace(' ', '_').lower()}", + "fieldtype": "Currency", + } + for price_list in price_lists + ] + columns = ( + [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + }, + { + "label": _("Item Description"), + "fieldname": "description", + "fieldtype": "Data", + }, + { + "label": _("Default Item Tax Template"), + "fieldname": "default_tax_template", + "fieldtype": "Link", + "options": "Item Tax Template", + }, + ] + + price_list_columns + + [ + { + "label": _("Total Qty"), + "fieldname": "total_qty", + "fieldtype": "Float", + }, + { + "label": _("Last Purchase Rate"), + "fieldname": "last_purchase_rate", + "fieldtype": "Currency", + }, + { + "label": _("Valuation Rate"), + "fieldname": "valuation_rate", + "fieldtype": "Currency", + }, + { + "label": _("Warehouse Qty"), + "fieldname": "warehouse_qty", + "fieldtype": "Data", + }, + ] + ) + return columns def get_data(filters): - conditions = "" - if filters.get("barcode"): - # Fetch item code based on barcode - item_code = db.get_value("Item Barcode", {"barcode": filters.get("barcode")}, "parent") - if item_code: - # Fetch and assign the item description to the filters dictionary - item_description = frappe.db.get_value("Item", {"item_code": item_code}, "description") - if item_description: - filters["item_description"] = item_description - else: - frappe.msgprint(_("No description found for item with barcode: ") + filters.get("barcode"), title="Warning") - return [] # Exit function if no description is found + conditions = "" + if filters.get("barcode"): + # Fetch item code based on barcode + item_code = db.get_value("Item Barcode", {"barcode": filters.get("barcode")}, "parent") + if item_code: + # Fetch and assign the item description to the filters dictionary + item_description = frappe.db.get_value("Item", {"item_code": item_code}, "description") + if item_description: + filters["item_description"] = item_description + else: + frappe.msgprint( + _("No description found for item with barcode: ") + filters.get("barcode"), + title="Warning", + ) + return [] # Exit function if no description is found - if filters.get("item_description"): - conditions += f" AND (i.description LIKE '%{filters['item_description']}%' OR i.item_code = '{filters['item_description'] or filters.get('barcode')}')" - # Example SQL Query to fetch data - sql = f""" + if filters.get("item_description"): + conditions += f" AND (i.description LIKE '%{filters['item_description']}%' OR i.item_code = '{filters['item_description'] or filters.get('barcode')}')" + # Example SQL Query to fetch data + sql = f""" SELECT i.item_code, i.description, @@ -111,24 +114,22 @@ def get_data(filters): AND i.is_sales_item = 1 {conditions} GROUP BY i.item_code """ - data = db.sql( - sql, - as_dict=1, - ) + data = db.sql( + sql, + as_dict=1, + ) - # Add price list rates - for d in data: - for price_list in db.get_list( - "Price List", filters={"selling": 1}, pluck="name" - ): - rate = db.get_value( - "Item Price", - {"item_code": d.item_code, "price_list": price_list}, - "price_list_rate", - ) - d[f"rate_{price_list.replace(' ', '_').lower()}"] = rate if rate else 0.0 - d[f"rate_{price_list.replace(' ', '_').lower()}_excl"] = ( - rate / (1 + (flt(filters["tax_rate"]) / 100)) if rate else 0.0 - ) + # Add price list rates + for d in data: + for price_list in db.get_list("Price List", filters={"selling": 1}, pluck="name"): + rate = db.get_value( + "Item Price", + {"item_code": d.item_code, "price_list": price_list}, + "price_list_rate", + ) + d[f"rate_{price_list.replace(' ', '_').lower()}"] = rate if rate else 0.0 + d[f"rate_{price_list.replace(' ', '_').lower()}_excl"] = ( + rate / (1 + (flt(filters["tax_rate"]) / 100)) if rate else 0.0 + ) - return data + return data diff --git a/csf_tz/csf_tz/report/itemwise_stock_movement/itemwise_stock_movement.js b/csf_tz/csf_tz/report/itemwise_stock_movement/itemwise_stock_movement.js index a744125a..0d06c8c8 100644 --- a/csf_tz/csf_tz/report/itemwise_stock_movement/itemwise_stock_movement.js +++ b/csf_tz/csf_tz/report/itemwise_stock_movement/itemwise_stock_movement.js @@ -43,4 +43,4 @@ frappe.query_reports["Itemwise Stock Movement"] = { "options": "Brand", } ] -} \ No newline at end of file +} diff --git a/csf_tz/csf_tz/report/itemwise_stock_movement/itemwise_stock_movement.py b/csf_tz/csf_tz/report/itemwise_stock_movement/itemwise_stock_movement.py index 44876067..7800e823 100644 --- a/csf_tz/csf_tz/report/itemwise_stock_movement/itemwise_stock_movement.py +++ b/csf_tz/csf_tz/report/itemwise_stock_movement/itemwise_stock_movement.py @@ -117,9 +117,7 @@ def get_conditions(sle, filters, items): conditions = [sle.company == filters.get("company"), sle.is_cancelled == 0] if filters.get("warehouse"): - warehouse = frappe.db.get_value( - "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=True - ) + warehouse = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=True) if not warehouse: frappe.throw(_("Warehouse {0} not found").format(filters.get("warehouse"))) wh = frappe.qb.DocType("Warehouse") @@ -127,11 +125,7 @@ def get_conditions(sle, filters, items): ExistsCriterion( frappe.qb.from_(wh) .select(wh.name) - .where( - (wh.lft >= warehouse.lft) - & (wh.rgt <= warehouse.rgt) - & (sle.warehouse == wh.name) - ) + .where((wh.lft >= warehouse.lft) & (wh.rgt <= warehouse.rgt) & (sle.warehouse == wh.name)) ) ) diff --git "a/csf_tz/csf_tz/report/itx_230.01.e_\342\200\223_withholding_tax_statement/itx_230.01.e_\342\200\223_withholding_tax_statement.html" "b/csf_tz/csf_tz/report/itx_230.01.e_\342\200\223_withholding_tax_statement/itx_230.01.e_\342\200\223_withholding_tax_statement.html" index adb8b602..1755145c 100644 --- "a/csf_tz/csf_tz/report/itx_230.01.e_\342\200\223_withholding_tax_statement/itx_230.01.e_\342\200\223_withholding_tax_statement.html" +++ "b/csf_tz/csf_tz/report/itx_230.01.e_\342\200\223_withholding_tax_statement/itx_230.01.e_\342\200\223_withholding_tax_statement.html" @@ -3,14 +3,14 @@ padding-left: 10mm; padding-right: 5mm; padding-top: 0mm; - font-size: 13pt; + font-size: 13pt; font-family: Arial, Helvetica, sans-serif; } - + .print-format td, .print-format th { vertical-align: top !important; padding: 2px !important; - + } @media screen { @@ -19,7 +19,7 @@ height: 11in; } } - + .table-bordered > thead > tr > th.no-line { border:border: 1px solid black; !important; border-right: none !important; @@ -28,7 +28,7 @@ border-right-style: none !important; border-left-style: none !important; } - + .table-bordered{ border:none; } @@ -36,11 +36,11 @@ {% var start_date = filters.from_date %} {% var end_date = filters.to_date %} {% var tin = data[0][ __("TIN")] %} - + {% if ( end_date[5]+ end_date[6] == "06") { %} {% var tick1 = "X" %} {% } %} - + {% if ( end_date[5]+ end_date[6] == "12") { %} {% var tick2 = "X" %} {% } %} @@ -121,7 +121,7 @@ {%= tin[10] %} - + @@ -241,7 +241,7 @@ -



ITX 230.01.E – Withholding Tax Statement +



ITX 230.01.E – Withholding Tax Statement
@@ -252,7 +252,7 @@ -
+
@@ -333,15 +333,15 @@ - {% + {% var post_add=0; var gross_total=0; var tax_withheld=0; %} - {% for(var i=0, l=data.length; i - {% + {% var gross_total = gross_total + data[i][ __("Gross Payment")]; var tax_withheld = tax_withheld + data[i][ __("Tax Withheld")]; %} @@ -360,6 +360,6 @@ {% } %} - {% } %} + {% } %}
{%= tax_withheld.toLocaleString(undefined, {minimumFractionDigits: 0, maximumFractionDigits: 0}) %}
diff --git a/csf_tz/csf_tz/report/loan_repayment_details/loan_repayment_details.py b/csf_tz/csf_tz/report/loan_repayment_details/loan_repayment_details.py index b94171ad..454b7e23 100644 --- a/csf_tz/csf_tz/report/loan_repayment_details/loan_repayment_details.py +++ b/csf_tz/csf_tz/report/loan_repayment_details/loan_repayment_details.py @@ -5,6 +5,7 @@ from frappe import _ from frappe.utils import flt + def execute(filters=None): loans = [] data = [] @@ -14,36 +15,62 @@ def execute(filters=None): if repayments_from_salaries: loans += get__paid_loans(filters, repayments_from_salaries, True) - + if repayments_not_from_salaries: loans += get__paid_loans(filters, repayments_not_from_salaries, False) - + if all_repayments: loans += get_loans_not_started_to_be_paid(filters, all_repayments) - + custom_loan_repayment_records = get_custom_loan_repayment_not_from_salary(filters) for loan in loans: for custom_loan_repayment in custom_loan_repayment_records: if custom_loan_repayment.loan in loan.loan: loan.amount_paid_not_from_salary += flt(custom_loan_repayment.amount_paid_not_from_salary) - - loan.update({ - "loan_balance": flt(loan.total_payable_amount) - flt(loan.amount_paid_from_salary) - flt(loan.amount_paid_not_from_salary) - flt(loan.write_off_amount) - }) + + loan.update( + { + "loan_balance": flt(loan.total_payable_amount) + - flt(loan.amount_paid_from_salary) + - flt(loan.amount_paid_not_from_salary) + - flt(loan.write_off_amount) + } + ) data.append(loan) return columns, data + def get_columns(filters): return [ {"fieldname": "applicant", "fieldtype": "Data", "label": _("Applicant"), "width": "150px"}, {"fieldname": "applicant_name", "fieldtype": "Data", "label": _("Applicant Name"), "width": "150px"}, {"fieldname": "loan", "fieldtype": "Data", "label": _("Loan"), "width": "100px"}, - {"fieldname": "total_payable_amount", "fieldtype": "Currency", "label": _("Total Payable Amount"), "width": "150px"}, - {"fieldname": "amount_paid_from_salary", "fieldtype": "Currency", "label": _("Amount Paid From Salary"), "width": "150px"}, - {"fieldname": "amount_paid_not_from_salary", "fieldtype": "Currency", "label": _("Amount Paid not From Salary"), "width": "150px"}, - {"fieldname": "write_off_amount", "fieldtype": "Currency", "label": _("Write Off Amount"), "width": "150px"}, + { + "fieldname": "total_payable_amount", + "fieldtype": "Currency", + "label": _("Total Payable Amount"), + "width": "150px", + }, + { + "fieldname": "amount_paid_from_salary", + "fieldtype": "Currency", + "label": _("Amount Paid From Salary"), + "width": "150px", + }, + { + "fieldname": "amount_paid_not_from_salary", + "fieldtype": "Currency", + "label": _("Amount Paid not From Salary"), + "width": "150px", + }, + { + "fieldname": "write_off_amount", + "fieldtype": "Currency", + "label": _("Write Off Amount"), + "width": "150px", + }, {"fieldname": "loan_balance", "fieldtype": "Currency", "label": _("Loan Balance"), "width": "150px"}, ] @@ -51,11 +78,12 @@ def get_columns(filters): def get_loans_not_started_to_be_paid(filters, all_repayments): conditions = "" if filters.get("employee"): - conditions += " AND l.applicant = '%s' " % filters["employee"] + conditions += " AND l.applicant = '{}' ".format(filters["employee"]) - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT - l.applicant AS applicant, + l.applicant AS applicant, l.applicant_name AS applicant_name, GROUP_CONCAT(l.name ORDER BY l.posting_date SEPARATOR ', ') AS loan, SUM(l.total_payment) AS total_payable_amount, @@ -66,15 +94,16 @@ def get_loans_not_started_to_be_paid(filters, all_repayments): FROM `tabLoan` l LEFT JOIN `tabLoan Write Off` wr ON l.name = wr.loan and l.applicant = wr.applicant WHERE l.name NOT IN ({}) - AND l.status != "Closed" + AND l.status != "Closed" AND l.docstatus = 1 {conditions} GROUP BY l.applicant ORDER BY l.posting_date, l.applicant - """.format(", ".join( - frappe.db.escape(loan.against_loan) for loan in all_repayments), - conditions=conditions - ), as_dict=True) - + """.format( + ", ".join(frappe.db.escape(loan.against_loan) for loan in all_repayments), conditions=conditions + ), + as_dict=True, + ) + def get__paid_loans(filters, repayments, from_salary): conditional_columns = "" @@ -89,9 +118,10 @@ def get__paid_loans(filters, repayments, from_salary): SUM(l.total_amount_paid) AS amount_paid_not_from_salary, """ - loans = frappe.db.sql(""" + loans = frappe.db.sql( + """ SELECT - l.applicant AS applicant, + l.applicant AS applicant, l.applicant_name AS applicant_name, GROUP_CONCAT(l.name ORDER BY l.posting_date SEPARATOR ', ') AS loan, SUM(l.total_payment) AS total_payable_amount, @@ -103,11 +133,13 @@ def get__paid_loans(filters, repayments, from_salary): AND l.status != "Closed" GROUP BY l.applicant ORDER BY l.posting_date, l.applicant - """.format(", ".join( - frappe.db.escape(loan.against_loan) for loan in repayments), - conditional_columns=conditional_columns - ), as_dict=True) - + """.format( + ", ".join(frappe.db.escape(loan.against_loan) for loan in repayments), + conditional_columns=conditional_columns, + ), + as_dict=True, + ) + return loans @@ -118,27 +150,32 @@ def get_repayments(filters): else: employee = ["!=", ""] - repayments_from_salaries = frappe.get_all("Loan Repayment", + repayments_from_salaries = frappe.get_all( + "Loan Repayment", filters={"loan_type": "Staff Loan", "repay_from_salary": 1, "docstatus": 1, "applicant": employee}, fields=["Distinct(against_loan) as against_loan"], - order_by="posting_date" + order_by="posting_date", ) - repayments_not_from_salaries = frappe.get_all("Loan Repayment", + repayments_not_from_salaries = frappe.get_all( + "Loan Repayment", filters={"loan_type": "Staff Loan", "repay_from_salary": 0, "docstatus": 1, "applicant": employee}, fields=["Distinct(against_loan) as against_loan"], - order_by="posting_date" + order_by="posting_date", ) - + all_repayments = repayments_from_salaries + repayments_not_from_salaries return repayments_from_salaries, repayments_not_from_salaries, all_repayments def get_custom_loan_repayment_not_from_salary(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT loan, SUM(payment_amount) AS amount_paid_not_from_salary FROM `tabLoan Repayment Not From Salary` WHERE loan != "" GROUP BY loan - """, as_dict=True) \ No newline at end of file + """, + as_dict=True, + ) diff --git a/csf_tz/csf_tz/report/monthly_account_balance/monthly_account_balance.py b/csf_tz/csf_tz/report/monthly_account_balance/monthly_account_balance.py index 898fdec5..e6ecf6aa 100644 --- a/csf_tz/csf_tz/report/monthly_account_balance/monthly_account_balance.py +++ b/csf_tz/csf_tz/report/monthly_account_balance/monthly_account_balance.py @@ -2,97 +2,109 @@ # For license information, please see license.txt import frappe -from frappe.utils import flt from frappe import _ from frappe.query_builder import DocType +from frappe.utils import flt + def execute(filters=None): - if not filters: - filters = {} - - # Define the GL Entry doctype - gl_entry = DocType("GL Entry") - - # Build basic query to fetch all required data - query = ( - frappe.qb.from_(gl_entry) - .select( - gl_entry.account, - gl_entry.account_currency, - gl_entry.posting_date, - gl_entry.debit_in_account_currency, - gl_entry.credit_in_account_currency - ) - .where(gl_entry.is_cancelled == 0) - .orderby(gl_entry.account) - .orderby(gl_entry.posting_date) - ) - - # Add account filter if provided - if filters.get("account"): - # Handle both string and list formats for account filter - if isinstance(filters.get("account"), list): - account_list = filters.get("account") - else: - account_list = [acc.strip() for acc in filters.get("account").split(",")] - query = query.where(gl_entry.account.isin(account_list)) - - # Execute the query to get raw data - raw_data = query.run(as_dict=True) - - # Process data to calculate monthly aggregations - monthly_aggregation = {} - - for row in raw_data: - # Format month as YYYY-MM - if row.get('posting_date'): - month = row['posting_date'].strftime('%Y-%m') - else: - continue - - # Create unique key for account-currency-month combination - account_key = f"{row['account']}_{row['account_currency']}_{month}" - - if account_key not in monthly_aggregation: - monthly_aggregation[account_key] = { - 'account': row['account'], - 'account_currency': row['account_currency'], - 'month': month, - 'monthly_net': 0 - } - - # Calculate monthly net (debit - credit) - debit = flt(row.get('debit_in_account_currency', 0)) - credit = flt(row.get('credit_in_account_currency', 0)) - net_amount = debit - credit - monthly_aggregation[account_key]['monthly_net'] += net_amount - - # Convert to list and sort - monthly_data = list(monthly_aggregation.values()) - monthly_data.sort(key=lambda x: (x['account'], x['account_currency'], x['month'])) - - # Calculate running balance (closing balance) - account_balances = {} - final_data = [] - - for row in monthly_data: - account_key = f"{row['account']}_{row['account_currency']}" - - if account_key not in account_balances: - account_balances[account_key] = 0 - - account_balances[account_key] += flt(row['monthly_net']) - - final_row = row.copy() - final_row['closing_balance'] = account_balances[account_key] - final_data.append(final_row) - - columns = [ - {"label": _("Account"), "fieldname": "account", "fieldtype": "Link", "options": "Account", "width": 220}, - {"label": _("Currency"), "fieldname": "account_currency", "fieldtype": "Data", "width": 100}, - {"label": _("Month"), "fieldname": "month", "fieldtype": "Data", "width": 100}, - {"label": _("Monthly Net"), "fieldname": "monthly_net", "fieldtype": "Currency", "width": 200}, - {"label": _("Closing Balance"), "fieldname": "closing_balance", "fieldtype": "Currency", "width": 200} - ] - - return columns, final_data \ No newline at end of file + if not filters: + filters = {} + + # Define the GL Entry doctype + gl_entry = DocType("GL Entry") + + # Build basic query to fetch all required data + query = ( + frappe.qb.from_(gl_entry) + .select( + gl_entry.account, + gl_entry.account_currency, + gl_entry.posting_date, + gl_entry.debit_in_account_currency, + gl_entry.credit_in_account_currency, + ) + .where(gl_entry.is_cancelled == 0) + .orderby(gl_entry.account) + .orderby(gl_entry.posting_date) + ) + + # Add account filter if provided + if filters.get("account"): + # Handle both string and list formats for account filter + if isinstance(filters.get("account"), list): + account_list = filters.get("account") + else: + account_list = [acc.strip() for acc in filters.get("account").split(",")] + query = query.where(gl_entry.account.isin(account_list)) + + # Execute the query to get raw data + raw_data = query.run(as_dict=True) + + # Process data to calculate monthly aggregations + monthly_aggregation = {} + + for row in raw_data: + # Format month as YYYY-MM + if row.get("posting_date"): + month = row["posting_date"].strftime("%Y-%m") + else: + continue + + # Create unique key for account-currency-month combination + account_key = f"{row['account']}_{row['account_currency']}_{month}" + + if account_key not in monthly_aggregation: + monthly_aggregation[account_key] = { + "account": row["account"], + "account_currency": row["account_currency"], + "month": month, + "monthly_net": 0, + } + + # Calculate monthly net (debit - credit) + debit = flt(row.get("debit_in_account_currency", 0)) + credit = flt(row.get("credit_in_account_currency", 0)) + net_amount = debit - credit + monthly_aggregation[account_key]["monthly_net"] += net_amount + + # Convert to list and sort + monthly_data = list(monthly_aggregation.values()) + monthly_data.sort(key=lambda x: (x["account"], x["account_currency"], x["month"])) + + # Calculate running balance (closing balance) + account_balances = {} + final_data = [] + + for row in monthly_data: + account_key = f"{row['account']}_{row['account_currency']}" + + if account_key not in account_balances: + account_balances[account_key] = 0 + + account_balances[account_key] += flt(row["monthly_net"]) + + final_row = row.copy() + final_row["closing_balance"] = account_balances[account_key] + final_data.append(final_row) + + columns = [ + { + "label": _("Account"), + "fieldname": "account", + "fieldtype": "Link", + "options": "Account", + "width": 220, + }, + {"label": _("Currency"), "fieldname": "account_currency", "fieldtype": "Data", "width": 100}, + {"label": _("Month"), "fieldname": "month", "fieldtype": "Data", "width": 100}, + {"label": _("Monthly Net"), "fieldname": "monthly_net", "fieldtype": "Currency", "width": 200}, + { + "label": _("Closing Balance"), + "fieldname": "closing_balance", + "fieldtype": "Currency", + "width": 200, + }, + ] + + return columns, final_data diff --git a/csf_tz/csf_tz/report/monthly_timesheet_report/monthly_timesheet_report.py b/csf_tz/csf_tz/report/monthly_timesheet_report/monthly_timesheet_report.py index b3b52245..7e897322 100644 --- a/csf_tz/csf_tz/report/monthly_timesheet_report/monthly_timesheet_report.py +++ b/csf_tz/csf_tz/report/monthly_timesheet_report/monthly_timesheet_report.py @@ -2,10 +2,11 @@ # For license information, please see license.txt import frappe -import pandas as pd import numpy as np -from frappe import msgprint, _ -from frappe.utils import data, flt +import pandas as pd +from frappe import _ +from frappe.utils import flt + def execute(filters=None): conditions, filters = get_conditions(filters) @@ -15,51 +16,41 @@ def execute(filters=None): if filters.hours_per_day: columns = [_("Date") + "::160"] - + records = hours_per_day_data(conditions, filters) if not records: - frappe.throw("No Record found for the filters From Date: {0}, To Date: {1}, Hours Per Day: {2} you specified..., \ - Please change your filters and try again..!!".format( - frappe.bold(filters.from_date), - frappe.bold(filters.to_date), - frappe.bold(filters.hours_per_day), - )) - + frappe.throw( + f"No Record found for the filters From Date: {frappe.bold(filters.from_date)}, To Date: {frappe.bold(filters.to_date)}, Hours Per Day: {frappe.bold(filters.hours_per_day)} you specified..., \ + Please change your filters and try again..!!" + ) + df_colnames = [key for key in records[0].keys()] - + df = pd.DataFrame.from_records(records, columns=df_colnames) - + table_pvt = pd.pivot_table( - df, - index=["employee_name"], - values="total_hours", - columns="date", - fill_value = " ", - aggfunc="first" + df, index=["employee_name"], values="total_hours", columns="date", fill_value=" ", aggfunc="first" ) columns += table_pvt.columns.values.tolist() data += table_pvt.reset_index().values.tolist() - + if filters.hours_per_project: frappe.throw(frappe.bold("No Records..!!")) - elif filters.hours_per_project: columns = [_("Project") + ":Link/Project:180"] project_details = hours_per_project_data(conditions, filters) if not project_details: - frappe.throw("No Record found for the filters From Date: {0}, To Date: {1} and Hours Per Project: {2} you specified..., \ - Please change your filters and try again..!!".format( - frappe.bold(filters.from_date), - frappe.bold(filters.to_date), - frappe.bold(filters.hours_per_project) - )) - + frappe.throw( + f"No Record found for the filters From Date: {frappe.bold(filters.from_date)}, To Date: {frappe.bold(filters.to_date)} and Hours Per Project: {frappe.bold(filters.hours_per_project)} you specified..., \ + Please change your filters and try again..!!" + ) + project_colnames = [key for key in project_details[0].keys()] - + df_project = pd.DataFrame.from_records(project_details, columns=project_colnames) project_pvt = pd.pivot_table( @@ -67,9 +58,9 @@ def execute(filters=None): index=["employee_name"], values="hours", columns="project", - fill_value = " ", - aggfunc= np.sum, - margins = True + fill_value=" ", + aggfunc=np.sum, + margins=True, ) columns += project_pvt.columns.values.tolist() @@ -77,53 +68,57 @@ def execute(filters=None): if filters.hours_per_day: frappe.throw(frappe.bold("No Records..!!")) - + else: timesheet_rows = timesheet_details(conditions, filters) if not timesheet_rows: - frappe.throw("No Record found for the filters From Date: {0}, To Date: {1} you specified..., \ - Please change your filters and try again..!!".format( - frappe.bold(filters.from_date), - frappe.bold(filters.to_date), - )) + frappe.throw( + f"No Record found for the filters From Date: {frappe.bold(filters.from_date)}, To Date: {frappe.bold(filters.to_date)} you specified..., \ + Please change your filters and try again..!!" + ) for row in timesheet_rows: data.append(row) - + return columns, data def get_columns(filters): columns = [] - if ( filters.summerized_view != "Hours Used Per Day" and - filters.summerized_view != "Hours Used Per Project" ): + if ( + filters.summerized_view != "Hours Used Per Day" + and filters.summerized_view != "Hours Used Per Project" + ): columns += [ - {"fieldname": "date", "label": _("Date"), "fieldtype": "Date", "width": 120 }, + {"fieldname": "date", "label": _("Date"), "fieldtype": "Date", "width": 120}, # {"fieldname": "employee", "label": _("Employee"), "fieldtype": "Data", "width": 120 }, - {"fieldname": "employee_name", "label": _("Employee Name"), "fieldtype": "Data", "width": 120 }, - {"fieldname": "activity_type", "label": _("Actuvuty Type"), "fieldtype": "Data", "width": 120 }, - {"fieldname": "from_time", "label": _("From Time"), "fieldtype": "Time", "width": 120 }, - { "fieldname": "to_time", "label": _("To Time"), "fieldtype": "Time", "width": 120 }, - {"fieldname": "hours_used", "label": _("Hours Used"), "fieldtype": "Data", "width": 120 }, - {"fieldname": "task", "label": _("Task"), "fieldtype": "Data", "width": 120 }, - { "fieldname": "project", "label": _("Project"), "fieldtype": "Data", "width": 120 }, + {"fieldname": "employee_name", "label": _("Employee Name"), "fieldtype": "Data", "width": 120}, + {"fieldname": "activity_type", "label": _("Actuvuty Type"), "fieldtype": "Data", "width": 120}, + {"fieldname": "from_time", "label": _("From Time"), "fieldtype": "Time", "width": 120}, + {"fieldname": "to_time", "label": _("To Time"), "fieldtype": "Time", "width": 120}, + {"fieldname": "hours_used", "label": _("Hours Used"), "fieldtype": "Data", "width": 120}, + {"fieldname": "task", "label": _("Task"), "fieldtype": "Data", "width": 120}, + {"fieldname": "project", "label": _("Project"), "fieldtype": "Data", "width": 120}, ] return columns def get_conditions(filters): conditions = "" - if filters.get("from_date"): conditions += "ts.start_date >= %(from_date)s" - if filters.get("to_date"): conditions += "AND ts.start_date <= %(to_date)s" + if filters.get("from_date"): + conditions += "ts.start_date >= %(from_date)s" + if filters.get("to_date"): + conditions += "AND ts.start_date <= %(to_date)s" return conditions, filters def timesheet_details(conditions, filters): - employees = frappe.get_all("Timesheet", - filters=[["start_date", ">=", filters.from_date],["start_date", "<=", filters.to_date]], - fields=["start_date", "employee", "employee_name"] + employees = frappe.get_all( + "Timesheet", + filters=[["start_date", ">=", filters.from_date], ["start_date", "<=", filters.to_date]], + fields=["start_date", "employee", "employee_name"], ) logs_data = get_timesheet_logs(conditions, filters) @@ -133,18 +128,17 @@ def timesheet_details(conditions, filters): parent_row = { "date": emp["start_date"].strftime("%Y-%m-%d"), # "employee": emp["employee"], - "employee_name": emp["employee_name"] + "employee_name": emp["employee_name"], } data.append(parent_row) for log in logs_data: if ( - emp["start_date"].strftime("%Y-%m-%d") == log["date2"] and - emp["employee"] == log["employee"] and - emp["employee_name"] == log["employee_name"] + emp["start_date"].strftime("%Y-%m-%d") == log["date2"] + and emp["employee"] == log["employee"] + and emp["employee_name"] == log["employee_name"] ): - child_row = { "indent": 2, "activity_type": log.activity_type, @@ -152,11 +146,11 @@ def timesheet_details(conditions, filters): "to_time": log.to_time, "hours_used": flt(log.hours_used, 1), "task": log.task, - "project": log.project + "project": log.project, } data.append(child_row) - + else: continue return data @@ -164,44 +158,50 @@ def timesheet_details(conditions, filters): def hours_per_day_data(conditions, filters): data = [] - records = frappe.get_all("Timesheet", + records = frappe.get_all( + "Timesheet", filters=[["start_date", ">=", filters.from_date], ["start_date", "<=", filters.to_date]], - fields=["employee", "employee_name", "start_date", "total_hours"]) + fields=["employee", "employee_name", "start_date", "total_hours"], + ) for record in records: - data.append({ - "employee": record.employee, - "employee_name": record.employee_name, - "date": record.start_date.strftime("%d-%m-%Y"), - "total_hours": flt(record.total_hours, 1) - }) + data.append( + { + "employee": record.employee, + "employee_name": record.employee_name, + "date": record.start_date.strftime("%d-%m-%Y"), + "total_hours": flt(record.total_hours, 1), + } + ) return data def hours_per_project_data(conditions, filters): - project_details = frappe.db.sql(""" - SELECT ts.employee, - ts.employee_name, + project_details = frappe.db.sql( + f""" + SELECT ts.employee, + ts.employee_name, tsd.project, tsd.hours FROM `tabTimesheet Detail` tsd INNER JOIN `tabTimesheet` ts ON tsd.parent = ts.name WHERE {conditions} ORDER BY ts.start_date - """.format(conditions=conditions), filters, as_dict=1 + """, + filters, + as_dict=1, ) - + data = [] for entry in project_details: - entry.update({ - "hours": flt(entry.hours, 1) - }) + entry.update({"hours": flt(entry.hours, 1)}) data.append(entry) return data def get_timesheet_logs(conditions, filters): - timesheet_logs = frappe.db.sql(""" + timesheet_logs = frappe.db.sql( + f""" SELECT ts.employee AS employee, ts.employee_name AS employee_name, tsd.activity_type AS activity_type, @@ -215,6 +215,8 @@ def get_timesheet_logs(conditions, filters): INNER JOIN `tabTimesheet` ts ON tsd.parent = ts.name WHERE {conditions} ORDER BY ts.start_date - """.format(conditions=conditions), filters, as_dict=1 + """, + filters, + as_dict=1, ) return timesheet_logs diff --git a/csf_tz/csf_tz/report/multi_currency_ledger/multi_currency_ledger.js b/csf_tz/csf_tz/report/multi_currency_ledger/multi_currency_ledger.js index ead835b9..2d62d6f5 100644 --- a/csf_tz/csf_tz/report/multi_currency_ledger/multi_currency_ledger.js +++ b/csf_tz/csf_tz/report/multi_currency_ledger/multi_currency_ledger.js @@ -169,4 +169,3 @@ erpnext.dimension_filters.forEach((dimension) => { "options": dimension["document_type"] }); }); - diff --git a/csf_tz/csf_tz/report/multi_currency_ledger/multi_currency_ledger.py b/csf_tz/csf_tz/report/multi_currency_ledger/multi_currency_ledger.py index 2c367db4..f0c50b48 100644 --- a/csf_tz/csf_tz/report/multi_currency_ledger/multi_currency_ledger.py +++ b/csf_tz/csf_tz/report/multi_currency_ledger/multi_currency_ledger.py @@ -1,33 +1,37 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals +from collections import OrderedDict + import frappe from erpnext import get_company_currency, get_default_company -from erpnext.accounts.report.utils import get_currency, convert_to_presentation_currency -from frappe.utils import getdate, cstr, flt -from frappe import _, _dict -from erpnext.accounts.utils import get_account_currency +from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( + get_accounting_dimensions, + get_dimension_with_children, +) from erpnext.accounts.report.financial_statements import get_cost_centers_with_children +from erpnext.accounts.report.utils import convert_to_presentation_currency, get_currency +from erpnext.accounts.utils import get_account_currency +from frappe import _, _dict +from frappe.utils import cstr, flt, getdate from six import iteritems -from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions, get_dimension_with_children -from collections import OrderedDict + # from csf_tz import console + def execute(filters=None): if not filters: return [], [] account_details = {} - if filters and filters.get('print_in_account_currency') and \ - not filters.get('account'): + if filters and filters.get("print_in_account_currency") and not filters.get("account"): frappe.throw(_("Select an account to print in account currency")) for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): account_details.setdefault(acc.name, acc) - if filters.get('party'): + if filters.get("party"): filters.party = frappe.parse_json(filters.get("party")) validate_filters(filters, account_details) @@ -44,28 +48,30 @@ def execute(filters=None): def validate_filters(filters, account_details): - if not filters.get('company'): - frappe.throw(_('{0} is mandatory').format(_('Company'))) + if not filters.get("company"): + frappe.throw(_("{0} is mandatory").format(_("Company"))) if filters.get("account") and not account_details.get(filters.account): frappe.throw(_("Account {0} does not exists").format(filters.account)) - if (filters.get("account") and filters.get("group_by") == _('Group by Account') - and account_details[filters.account].is_group == 0): + if ( + filters.get("account") + and filters.get("group_by") == _("Group by Account") + and account_details[filters.account].is_group == 0 + ): frappe.throw(_("Can not filter based on Account, if grouped by Account")) - if (filters.get("voucher_no") - and filters.get("group_by") in [_('Group by Voucher')]): + if filters.get("voucher_no") and filters.get("group_by") in [_("Group by Voucher")]: frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher")) if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) - if filters.get('project'): - filters.project = frappe.parse_json(filters.get('project')) + if filters.get("project"): + filters.project = frappe.parse_json(filters.get("project")) - if filters.get('cost_center'): - filters.cost_center = frappe.parse_json(filters.get('cost_center')) + if filters.get("cost_center"): + filters.cost_center = frappe.parse_json(filters.get("cost_center")) def validate_party(filters): @@ -79,26 +85,29 @@ def validate_party(filters): if not frappe.db.exists(party_type, d): frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) + def set_account_currency(filters): - if filters.get("account") or (filters.get('party') and len(filters.party) == 1): - filters["company_currency"] = frappe.get_cached_value('Company', filters.company, "default_currency") + if filters.get("account") or (filters.get("party") and len(filters.party) == 1): + filters["company_currency"] = frappe.get_cached_value("Company", filters.company, "default_currency") account_currency = None if filters.get("account"): account_currency = get_account_currency(filters.account) elif filters.get("party"): gle_currency = frappe.db.get_value( - "GL Entry", { - "party_type": filters.party_type, "party": filters.party[0], "company": filters.company - }, - "account_currency" + "GL Entry", + {"party_type": filters.party_type, "party": filters.party[0], "company": filters.company}, + "account_currency", ) if gle_currency: account_currency = gle_currency else: - account_currency = (None if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] else - frappe.db.get_value(filters.party_type, filters.party[0], "default_currency")) + account_currency = ( + None + if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] + else frappe.db.get_value(filters.party_type, filters.party[0], "default_currency") + ) filters["account_currency"] = account_currency or filters.company_currency if filters.account_currency != filters.company_currency and not filters.presentation_currency: @@ -106,6 +115,7 @@ def set_account_currency(filters): return filters + def get_result(filters): gl_entries = get_gl_entries(filters) @@ -115,6 +125,7 @@ def get_result(filters): return result + def get_gl_entries(filters): currency_map = get_currency(filters) select_fields = """, debit, credit, debit_in_account_currency, @@ -126,33 +137,31 @@ def get_gl_entries(filters): order_by_statement = "order by posting_date, voucher_type, voucher_no" if filters.get("include_default_book_entries"): - filters['company_fb'] = frappe.db.get_value("Company", - filters.get("company"), 'default_finance_book') + filters["company_fb"] = frappe.db.get_value("Company", filters.get("company"), "default_finance_book") gl_entries = frappe.db.sql( - """ + f""" select posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, project, against_voucher_type, against_voucher, account_currency, remarks, against, is_opening {select_fields} from `tabGL Entry` - where company=%(company)s {conditions} + where company=%(company)s {get_conditions(filters)} {order_by_statement} - """.format( - select_fields=select_fields, conditions=get_conditions(filters), - order_by_statement=order_by_statement - ), - filters, as_dict=1) + """, + filters, + as_dict=1, + ) converted_gl_list = [] - company_currency = currency_map['company_currency'] + company_currency = currency_map["company_currency"] for entry in gl_entries: - docktype = entry['voucher_type'] - docname = entry['voucher_no'] - entry_currency = entry['account_currency'] + docktype = entry["voucher_type"] + docname = entry["voucher_no"] + entry_currency = entry["account_currency"] doc_currency = company_currency items_list = "" @@ -160,11 +169,14 @@ def get_gl_entries(filters): doc = frappe.db.sql( """ select - payment_type, paid_to_account_currency, paid_from_account_currency, received_amount, paid_amount + payment_type, paid_to_account_currency, paid_from_account_currency, received_amount, paid_amount from `tabPayment Entry` where name=%(docname)s limit 1 - """,{'docname':docname},as_dict=1) + """, + {"docname": docname}, + as_dict=1, + ) if doc[0].payment_type == "Receive": doc_currency = doc[0].paid_to_account_currency doc_amount = doc[0].received_amount @@ -172,7 +184,7 @@ def get_gl_entries(filters): doc_currency = doc[0].paid_from_account_currency doc_amount = doc[0].paid_amount elif doc[0].payment_type == "Internal Transfer": - if entry.get('credit'): + if entry.get("credit"): doc_currency = doc[0].paid_from_account_currency doc_amount = doc[0].paid_amount else: @@ -182,88 +194,97 @@ def get_gl_entries(filters): elif docktype == "Sales Invoice": if entry["party"]: doc = frappe.db.sql( - """ + """ select name, currency, grand_total ,rounded_total from `tabSales Invoice` where name=%(docname)s limit 1 - """,{'docname':entry['voucher_no']},as_dict=1) + """, + {"docname": entry["voucher_no"]}, + as_dict=1, + ) doc_currency = doc[0].currency doc_amount = doc[0].rounded_total or doc[0].grand_total - + items = frappe.db.sql( - """ + """ select item_name, service_start_date, service_end_date from `tabSales Invoice Item` where parent=%(docname)s - """,{'docname':entry['voucher_no']},as_dict=1) - + """, + {"docname": entry["voucher_no"]}, + as_dict=1, + ) + for item in items: - items_list += "({0}".format(item.item_name) + items_list += f"({item.item_name}" if item.service_start_date: - items_list += " , {0} ".format(item.service_start_date) + items_list += f" , {item.service_start_date} " if item.service_end_date: - items_list += ", {0}".format(item.service_end_date) + items_list += f", {item.service_end_date}" items_list += ") " - + elif docktype == "Purchase Invoice": if entry["party"]: doc = frappe.db.sql( - """ + """ select name, currency, grand_total, rounded_total from `tabPurchase Invoice` where name=%(docname)s limit 1 - """,{'docname':entry['voucher_no']},as_dict=1) + """, + {"docname": entry["voucher_no"]}, + as_dict=1, + ) doc_currency = doc[0].currency doc_amount = doc[0].rounded_total or doc[0].grand_total - + items = frappe.db.sql( - """ + """ select item_name, service_start_date, service_end_date from `tabPurchase Invoice Item` where parent=%(docname)s - """,{'docname':entry['voucher_no']},as_dict=1) - + """, + {"docname": entry["voucher_no"]}, + as_dict=1, + ) + for item in items: - items_list += "({0}".format(item.item_name) + items_list += f"({item.item_name}" if item.service_start_date: - items_list += " , {0} ".format(item.service_start_date) + items_list += f" , {item.service_start_date} " if item.service_end_date: - items_list += ", {0}".format(item.service_end_date) + items_list += f", {item.service_end_date}" items_list += ") " elif docktype == "Journal Entry": doc_currency = entry_currency - if entry.get('credit'): - doc_amount = entry['credit_in_account_currency'] + if entry.get("credit"): + doc_amount = entry["credit_in_account_currency"] else: - doc_amount = entry['debit_in_account_currency'] + doc_amount = entry["debit_in_account_currency"] if company_currency != doc_currency: + if entry.get("debit"): + entry["debit_foreign"] = doc_amount + entry["exchange_rate"] = entry.get("debit") / doc_amount - if entry.get('debit'): - entry['debit_foreign'] = doc_amount - entry['exchange_rate'] = entry.get('debit') / doc_amount + if entry.get("credit"): + entry["credit_foreign"] = doc_amount + entry["exchange_rate"] = entry.get("credit") / doc_amount - if entry.get('credit'): - entry['credit_foreign'] = doc_amount - entry['exchange_rate'] = entry.get('credit') / doc_amount + entry["foreign_currency"] = doc_currency - entry['foreign_currency'] = doc_currency - - - entry['items'] = str(items_list) if items_list else "" + entry["items"] = str(items_list) if items_list else "" converted_gl_list.append(entry) - - if filters.get('presentation_currency'): + if filters.get("presentation_currency"): return convert_to_presentation_currency(converted_gl_list, currency_map) else: return converted_gl_list @@ -273,8 +294,10 @@ def get_conditions(filters): conditions = [] if filters.get("account"): lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"]) - conditions.append("""account in (select name from tabAccount - where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt)) + conditions.append( + f"""account in (select name from tabAccount + where lft>={lft} and rgt<={rgt} and docstatus<2)""" + ) if filters.get("cost_center"): filters.cost_center = get_cost_centers_with_children(filters.cost_center) @@ -292,8 +315,11 @@ def get_conditions(filters): if filters.get("party"): conditions.append("party in %(party)s") - if not (filters.get("account") or filters.get("party") or - filters.get("group_by") in ["Group by Account", "Group by Party"]): + if not ( + filters.get("account") + or filters.get("party") + or filters.get("group_by") in ["Group by Account", "Group by Party"] + ): conditions.append("posting_date >=%(from_date)s") conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") @@ -308,6 +334,7 @@ def get_conditions(filters): conditions.append("finance_book in (%(finance_book)s)") from frappe.desk.reportview import build_match_conditions + match_conditions = build_match_conditions("GL Entry") if match_conditions: @@ -318,10 +345,11 @@ def get_conditions(filters): if accounting_dimensions: for dimension in accounting_dimensions: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) - conditions.append("{0} in %({0})s".format(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) + conditions.append(f"{dimension.fieldname} in %({dimension.fieldname})s") return "and {}".format(" and ".join(conditions)) if conditions else "" @@ -336,8 +364,8 @@ def get_data_with_opening_closing(filters, gl_entries): # Opening for filtered account data.append(totals.opening) - if filters.get("group_by") != _('Group by Voucher (Consolidated)'): - for acc, acc_dict in iteritems(gle_map): + if filters.get("group_by") != _("Group by Voucher (Consolidated)"): + for _acc, acc_dict in iteritems(gle_map): # acc if acc_dict.entries: # opening @@ -365,32 +393,36 @@ def get_data_with_opening_closing(filters, gl_entries): return data + def get_totals_dict(): def _get_debit_credit_dict(label): return _dict( - account="'{0}'".format(label), + account=f"'{label}'", debit=0.0, credit=0.0, debit_in_account_currency=0.0, - credit_in_account_currency=0.0 + credit_in_account_currency=0.0, ) + return _dict( - opening = _get_debit_credit_dict(_('Opening')), - total = _get_debit_credit_dict(_('Total')), - closing = _get_debit_credit_dict(_('Closing (Opening + Total)')) + opening=_get_debit_credit_dict(_("Opening")), + total=_get_debit_credit_dict(_("Total")), + closing=_get_debit_credit_dict(_("Closing (Opening + Total)")), ) + def group_by_field(group_by): - if group_by == _('Group by Party'): - return 'party' - elif group_by in [_('Group by Voucher (Consolidated)'), _('Group by Account')]: - return 'account' + if group_by == _("Group by Party"): + return "party" + elif group_by in [_("Group by Voucher (Consolidated)"), _("Group by Account")]: + return "account" else: - return 'voucher_no' + return "voucher_no" + def initialize_gle_map(gl_entries, filters): gle_map = OrderedDict() - group_by = group_by_field(filters.get('group_by')) + group_by = group_by_field(filters.get("group_by")) for gle in gl_entries: gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[])) @@ -401,7 +433,7 @@ def get_accountwise_gle(filters, gl_entries, gle_map): totals = get_totals_dict() entries = [] consolidated_gle = OrderedDict() - group_by = group_by_field(filters.get('group_by')) + group_by = group_by_field(filters.get("group_by")) def update_value_in_dict(data, key, gle): data[key].debit += flt(gle.debit) @@ -412,55 +444,64 @@ def update_value_in_dict(data, key, gle): from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) for gle in gl_entries: - if (gle.posting_date < from_date or - (cstr(gle.is_opening) == "Yes" and not filters.get("show_opening_entries"))): - update_value_in_dict(gle_map[gle.get(group_by)].totals, 'opening', gle) - update_value_in_dict(totals, 'opening', gle) + if gle.posting_date < from_date or ( + cstr(gle.is_opening) == "Yes" and not filters.get("show_opening_entries") + ): + update_value_in_dict(gle_map[gle.get(group_by)].totals, "opening", gle) + update_value_in_dict(totals, "opening", gle) - update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle) - update_value_in_dict(totals, 'closing', gle) + update_value_in_dict(gle_map[gle.get(group_by)].totals, "closing", gle) + update_value_in_dict(totals, "closing", gle) elif gle.posting_date <= to_date: - update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle) - update_value_in_dict(totals, 'total', gle) - if filters.get("group_by") != _('Group by Voucher (Consolidated)'): + update_value_in_dict(gle_map[gle.get(group_by)].totals, "total", gle) + update_value_in_dict(totals, "total", gle) + if filters.get("group_by") != _("Group by Voucher (Consolidated)"): gle_map[gle.get(group_by)].entries.append(gle) - elif filters.get("group_by") == _('Group by Voucher (Consolidated)'): - key = (gle.get("voucher_type"), gle.get("voucher_no"), - gle.get("account"), gle.get("cost_center")) + elif filters.get("group_by") == _("Group by Voucher (Consolidated)"): + key = ( + gle.get("voucher_type"), + gle.get("voucher_no"), + gle.get("account"), + gle.get("cost_center"), + ) if key not in consolidated_gle: consolidated_gle.setdefault(key, gle) else: update_value_in_dict(consolidated_gle, key, gle) - update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle) - update_value_in_dict(totals, 'closing', gle) + update_value_in_dict(gle_map[gle.get(group_by)].totals, "closing", gle) + update_value_in_dict(totals, "closing", gle) - for key, value in consolidated_gle.items(): + for _key, value in consolidated_gle.items(): entries.append(value) return totals, entries + def get_result_as_list(data, filters): balance = 0 inv_details = get_supplier_invoice_details() updated_data = [] for d in data: - if not d.get('posting_date'): + if not d.get("posting_date"): balance = 0 - balance = get_balance(d, balance, 'debit', 'credit') - d['balance'] = balance + balance = get_balance(d, balance, "debit", "credit") + d["balance"] = balance - d['account_currency'] = filters.account_currency - d['bill_no'] = inv_details.get(d.get('against_voucher'), '') + d["account_currency"] = filters.account_currency + d["bill_no"] = inv_details.get(d.get("against_voucher"), "") - if not ((d.get("credit_foreign") and d.get("debit_foreign")) and (d.get("credit_foreign") == d.get("debit_foreign"))): + if not ( + (d.get("credit_foreign") and d.get("debit_foreign")) + and (d.get("credit_foreign") == d.get("debit_foreign")) + ): updated_data.append(d) if d.get("voucher_type") == "Journal Entry" and filters.get("account"): - jv_doc = frappe.get_doc(d["voucher_type"],d["voucher_no"]) + jv_doc = frappe.get_doc(d["voucher_type"], d["voucher_no"]) for row in jv_doc.accounts: if row.account != d["account"]: new_entry = {} @@ -473,17 +514,23 @@ def get_result_as_list(data, filters): return updated_data + def get_supplier_invoice_details(): inv_details = {} - for d in frappe.db.sql(""" select name, bill_no from `tabPurchase Invoice` - where docstatus = 1 and bill_no is not null and bill_no != '' """, as_dict=1): + for d in frappe.db.sql( + """ select name, bill_no from `tabPurchase Invoice` + where docstatus = 1 and bill_no is not null and bill_no != '' """, + as_dict=1, + ): inv_details[d.name] = d.bill_no return inv_details + def get_balance(row, balance, debit_field, credit_field): - balance += (row.get(debit_field, 0) - row.get(credit_field, 0)) + balance += row.get(debit_field, 0) - row.get(credit_field, 0) return balance + def get_columns(filters): if filters.get("presentation_currency"): currency = filters["presentation_currency"] @@ -495,154 +542,64 @@ def get_columns(filters): currency = get_company_currency(company) columns = [ - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 90 - }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, { "label": _("Account"), "fieldname": "account", "fieldtype": "Link", "options": "Account", - "width": 180 - }, - { - "label": _("Debit ({0})".format(currency)), - "fieldname": "debit", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Credit ({0})".format(currency)), - "fieldname": "credit", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Debit Foreign"), - "fieldname": "debit_foreign", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Credit Foreign"), - "fieldname": "credit_foreign", - "fieldtype": "Float", - "width": 100 + "width": 180, }, + {"label": _(f"Debit ({currency})"), "fieldname": "debit", "fieldtype": "Float", "width": 100}, + {"label": _(f"Credit ({currency})"), "fieldname": "credit", "fieldtype": "Float", "width": 100}, + {"label": _("Debit Foreign"), "fieldname": "debit_foreign", "fieldtype": "Float", "width": 100}, + {"label": _("Credit Foreign"), "fieldname": "credit_foreign", "fieldtype": "Float", "width": 100}, { "label": _("Foreign Currency"), "fieldname": "foreign_currency", # "fieldtype": "Float", - "width": 100 + "width": 100, }, - { - "label": _("Exchange Rate"), - "fieldname": "exchange_rate", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Balance ({0})".format(currency)), - "fieldname": "balance", - "fieldtype": "Float", - "width": 130 - } + {"label": _("Exchange Rate"), "fieldname": "exchange_rate", "fieldtype": "Float", "width": 100}, + {"label": _(f"Balance ({currency})"), "fieldname": "balance", "fieldtype": "Float", "width": 130}, ] - columns.extend([ - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": 120 - }, - { - "label": _("Voucher No"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 180 - }, - { - "label": _("Against Account"), - "fieldname": "against", - "width": 120 - }, - { - "label": _("Against AC Name"), - "fieldname": "against_acount", - "width": 120 - }, - { - "label": _("Against Debit"), - "fieldname": "against_debit", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Against Credit"), - "fieldname": "against_credit", - "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Against Currency"), - "fieldname": "against_currency", - # "fieldtype": "Float", - "width": 100 - }, - { - "label": _("Party Type"), - "fieldname": "party_type", - "width": 100 - }, - { - "label": _("Party"), - "fieldname": "party", - "width": 100 - }, - { - "label": _("Project"), - "options": "Project", - "fieldname": "project", - "width": 100 - }, - { - "label": _("Cost Center"), - "options": "Cost Center", - "fieldname": "cost_center", - "width": 100 - }, - { - "label": _("Against Voucher Type"), - "fieldname": "against_voucher_type", - "width": 100 - }, - { - "label": _("Against Voucher"), - "fieldname": "against_voucher", - "fieldtype": "Dynamic Link", - "options": "against_voucher_type", - "width": 100 - }, - { - "label": _("Supplier Invoice No"), - "fieldname": "bill_no", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Remarks"), - "fieldname": "remarks", - "width": 400 - }, - { - "label": _("Items"), - "fieldname": "items", - "width": 400 - } - ]) + columns.extend( + [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120}, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 180, + }, + {"label": _("Against Account"), "fieldname": "against", "width": 120}, + {"label": _("Against AC Name"), "fieldname": "against_acount", "width": 120}, + {"label": _("Against Debit"), "fieldname": "against_debit", "fieldtype": "Float", "width": 100}, + {"label": _("Against Credit"), "fieldname": "against_credit", "fieldtype": "Float", "width": 100}, + { + "label": _("Against Currency"), + "fieldname": "against_currency", + # "fieldtype": "Float", + "width": 100, + }, + {"label": _("Party Type"), "fieldname": "party_type", "width": 100}, + {"label": _("Party"), "fieldname": "party", "width": 100}, + {"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}, + {"label": _("Cost Center"), "options": "Cost Center", "fieldname": "cost_center", "width": 100}, + {"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100}, + { + "label": _("Against Voucher"), + "fieldname": "against_voucher", + "fieldtype": "Dynamic Link", + "options": "against_voucher_type", + "width": 100, + }, + {"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100}, + {"label": _("Remarks"), "fieldname": "remarks", "width": 400}, + {"label": _("Items"), "fieldname": "items", "width": 400}, + ] + ) return columns diff --git a/csf_tz/csf_tz/report/output_vat_reconciliation/output_vat_reconciliation.py b/csf_tz/csf_tz/report/output_vat_reconciliation/output_vat_reconciliation.py index 4e9cc2da..5040adc1 100644 --- a/csf_tz/csf_tz/report/output_vat_reconciliation/output_vat_reconciliation.py +++ b/csf_tz/csf_tz/report/output_vat_reconciliation/output_vat_reconciliation.py @@ -1,85 +1,147 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe.utils import fmt_money + + def execute(filters=None): columns, data = get_columns(), [] credit_note_label = {"details": "Credit Note - Sales Returns"} sales_label = {"details": "Sales - Sales Returns"} totals = {} - generate_sales_returns(filters,data,totals,sales_label) - data[0]["std_sales"] = fmt_money(float(totals['total_std_sales']), 2, data[1]['invoice_currency']) - data[0]["vat"] = fmt_money(float(totals['vat']), 2, data[1]['invoice_currency']) - data[0]["ex_amount"] = fmt_money(float(totals['ex_amount']), 2, data[1]['invoice_currency']) - data[0]["total"] = fmt_money(float(totals['total']), 2, data[1]['invoice_currency']) - generate_credit_note(data,totals,credit_note_label) + generate_sales_returns(filters, data, totals, sales_label) + data[0]["std_sales"] = fmt_money(float(totals["total_std_sales"]), 2, data[1]["invoice_currency"]) + data[0]["vat"] = fmt_money(float(totals["vat"]), 2, data[1]["invoice_currency"]) + data[0]["ex_amount"] = fmt_money(float(totals["ex_amount"]), 2, data[1]["invoice_currency"]) + data[0]["total"] = fmt_money(float(totals["total"]), 2, data[1]["invoice_currency"]) + generate_credit_note(data, totals, credit_note_label) - data.append({ - "details": "Sales as VAT Returns", - "std_sales": fmt_money(float(totals['total_std_sales']), 2, data[1]['invoice_currency']), - "vat": fmt_money(float(totals['vat']), 2, data[1]['invoice_currency']), - "ex_amount": fmt_money(float(totals['ex_amount']), 2, data[1]['invoice_currency']), - "total": fmt_money(float(totals['total']), 2, data[1]['invoice_currency']) - }) + data.append( + { + "details": "Sales as VAT Returns", + "std_sales": fmt_money(float(totals["total_std_sales"]), 2, data[1]["invoice_currency"]), + "vat": fmt_money(float(totals["vat"]), 2, data[1]["invoice_currency"]), + "ex_amount": fmt_money(float(totals["ex_amount"]), 2, data[1]["invoice_currency"]), + "total": fmt_money(float(totals["total"]), 2, data[1]["invoice_currency"]), + } + ) return columns, data -def generate_credit_note(data,totals,credit_note_label): - for i in range(1,len(data)): - if data[i]['details'] != "Credit Note - Sales Returns": - credit_notes = frappe.get_list("Sales Invoice", filters={"is_return": 1, "return_against": "ACC-SINV-2019-07382", "docstatus": 1}, fields=["*"]) +def generate_credit_note(data, totals, credit_note_label): + for i in range(1, len(data)): + if data[i]["details"] != "Credit Note - Sales Returns": + credit_notes = frappe.get_list( + "Sales Invoice", + filters={"is_return": 1, "return_against": "ACC-SINV-2019-07382", "docstatus": 1}, + fields=["*"], + ) print(credit_notes) for ii in credit_notes: - if i == 1: data.append(credit_note_label) - totals['total_std_sales'] = totals['total_std_sales'] + float(ii.total) if "total_std_sales" in totals else float(ii.total) - totals['vat'] = totals['vat'] + float(ii.total_taxes_and_charges) if "vat" in totals else float(ii.total_taxes_and_charges) - totals['ex_amount'] = totals['ex_amount'] + float(ii.grand_total) if "ex_amount" in totals else float(ii.grand_total) - totals['total'] = totals['total'] + float(ii.total + ii.total_taxes_and_charges + ii.grand_total) if "total" in totals else float(ii.total + ii.total_taxes_and_charges + ii.grand_total) + totals["total_std_sales"] = ( + totals["total_std_sales"] + float(ii.total) + if "total_std_sales" in totals + else float(ii.total) + ) + totals["vat"] = ( + totals["vat"] + float(ii.total_taxes_and_charges) + if "vat" in totals + else float(ii.total_taxes_and_charges) + ) + totals["ex_amount"] = ( + totals["ex_amount"] + float(ii.grand_total) + if "ex_amount" in totals + else float(ii.grand_total) + ) + totals["total"] = ( + totals["total"] + float(ii.total + ii.total_taxes_and_charges + ii.grand_total) + if "total" in totals + else float(ii.total + ii.total_taxes_and_charges + ii.grand_total) + ) - data.append({ - "details": ii.name, - "std_sales": fmt_money(float(ii.total), 2, data[i]['invoice_currency']), - "vat": fmt_money(float(ii.total_taxes_and_charges), 2, data[i]['invoice_currency']), - "ex_amount": fmt_money(ii.grand_total, 2, data[i]['invoice_currency']), - "total": fmt_money( - float(ii.total + ii.total_taxes_and_charges + ii.grand_total),2, data[i]['invoice_currency']) - }) + data.append( + { + "details": ii.name, + "std_sales": fmt_money(float(ii.total), 2, data[i]["invoice_currency"]), + "vat": fmt_money(float(ii.total_taxes_and_charges), 2, data[i]["invoice_currency"]), + "ex_amount": fmt_money(ii.grand_total, 2, data[i]["invoice_currency"]), + "total": fmt_money( + float(ii.total + ii.total_taxes_and_charges + ii.grand_total), + 2, + data[i]["invoice_currency"], + ), + } + ) -def generate_sales_returns(filters,data,totals,sales_label): - efd_z_report_invoices = frappe.get_list("EFD Z Report Invoice", filters={"parent": filters.get("efd_report")}, - fields=["*"]) - for idx,i in enumerate(efd_z_report_invoices): +def generate_sales_returns(filters, data, totals, sales_label): + efd_z_report_invoices = frappe.get_list( + "EFD Z Report Invoice", filters={"parent": filters.get("efd_report")}, fields=["*"] + ) + for idx, i in enumerate(efd_z_report_invoices): sales_invoice = frappe.get_doc("Sales Invoice", i.invoice_number).__dict__ if idx == 0: data.append(sales_label) - totals['total_std_sales'] = totals['total_std_sales'] + float(sales_invoice['total']) if "total_std_sales" in totals else float(sales_invoice['total']) - totals['vat'] = totals['vat'] + float(sales_invoice['total_taxes_and_charges']) if "vat" in totals else float(sales_invoice['total_taxes_and_charges']) - totals['ex_amount'] = totals['ex_amount'] + float(sales_invoice['grand_total']) if "ex_amount" in totals else float(sales_invoice['grand_total']) - totals['total'] = totals['total'] + float(sales_invoice['total'] + sales_invoice['total_taxes_and_charges'] + sales_invoice['grand_total']) if "total" in totals else float(sales_invoice['total'] + sales_invoice['total_taxes_and_charges'] + sales_invoice['grand_total']) - data.append({ - "details": i.invoice_number, - "std_sales": fmt_money(float(sales_invoice['total']), 2, i.invoice_currency), - "vat": fmt_money(float(sales_invoice['total_taxes_and_charges']), 2, i.invoice_currency), - "ex_amount": fmt_money(sales_invoice['grand_total'], 2, i.invoice_currency), - "total": fmt_money( - float(sales_invoice['total'] + sales_invoice['total_taxes_and_charges'] + sales_invoice['grand_total']), - 2, i.invoice_currency), - "invoice_currency": i.invoice_currency - }) -def get_columns(): + totals["total_std_sales"] = ( + totals["total_std_sales"] + float(sales_invoice["total"]) + if "total_std_sales" in totals + else float(sales_invoice["total"]) + ) + totals["vat"] = ( + totals["vat"] + float(sales_invoice["total_taxes_and_charges"]) + if "vat" in totals + else float(sales_invoice["total_taxes_and_charges"]) + ) + totals["ex_amount"] = ( + totals["ex_amount"] + float(sales_invoice["grand_total"]) + if "ex_amount" in totals + else float(sales_invoice["grand_total"]) + ) + totals["total"] = ( + totals["total"] + + float( + sales_invoice["total"] + + sales_invoice["total_taxes_and_charges"] + + sales_invoice["grand_total"] + ) + if "total" in totals + else float( + sales_invoice["total"] + + sales_invoice["total_taxes_and_charges"] + + sales_invoice["grand_total"] + ) + ) + data.append( + { + "details": i.invoice_number, + "std_sales": fmt_money(float(sales_invoice["total"]), 2, i.invoice_currency), + "vat": fmt_money(float(sales_invoice["total_taxes_and_charges"]), 2, i.invoice_currency), + "ex_amount": fmt_money(sales_invoice["grand_total"], 2, i.invoice_currency), + "total": fmt_money( + float( + sales_invoice["total"] + + sales_invoice["total_taxes_and_charges"] + + sales_invoice["grand_total"] + ), + 2, + i.invoice_currency, + ), + "invoice_currency": i.invoice_currency, + } + ) + +def get_columns(): columns = [ {"label": "Details", "fieldname": "details", "fieldtype": "Data", "width": 200}, - {"label": "STD Sales", "fieldname": "std_sales","fieldtype": "Data", "width": 170}, - {"label": "VAT", "fieldname": "vat", "fieldtype": "Data","width": 170}, - {"label": "EX Amount", "fieldname": "ex_amount","fieldtype": "Data", "width": 170}, - {"label": "Total", "fieldname": "total", "fieldtype": "Data","width": 170}, + {"label": "STD Sales", "fieldname": "std_sales", "fieldtype": "Data", "width": 170}, + {"label": "VAT", "fieldname": "vat", "fieldtype": "Data", "width": 170}, + {"label": "EX Amount", "fieldname": "ex_amount", "fieldtype": "Data", "width": 170}, + {"label": "Total", "fieldname": "total", "fieldtype": "Data", "width": 170}, ] - return columns \ No newline at end of file + return columns diff --git a/csf_tz/csf_tz/report/particular_item_history_report/particular_item_history_report.py b/csf_tz/csf_tz/report/particular_item_history_report/particular_item_history_report.py index 05607df5..793d0d37 100644 --- a/csf_tz/csf_tz/report/particular_item_history_report/particular_item_history_report.py +++ b/csf_tz/csf_tz/report/particular_item_history_report/particular_item_history_report.py @@ -8,233 +8,233 @@ def execute(filters=None): - filters = frappe._dict(filters or {}) - validate_filters(filters) + filters = frappe._dict(filters or {}) + validate_filters(filters) - columns = get_columns() - data = get_data(filters) + columns = get_columns() + data = get_data(filters) - return columns, data + return columns, data def validate_filters(filters): - if not filters.get("from_date") or not filters.get("to_date"): - frappe.throw(_("From Date and To Date are required")) + if not filters.get("from_date") or not filters.get("to_date"): + frappe.throw(_("From Date and To Date are required")) - if getdate(filters.to_date) < getdate(filters.from_date): - frappe.throw(_("To Date must be on or after From Date")) + if getdate(filters.to_date) < getdate(filters.from_date): + frappe.throw(_("To Date must be on or after From Date")) def get_columns(): - return [ - { - "label": _("Item Code"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 140, - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 200, - }, - { - "label": _("Item Group"), - "fieldname": "item_group", - "fieldtype": "Link", - "options": "Item Group", - "width": 160, - }, - { - "label": _("Last Purchase Price"), - "fieldname": "last_purchase_price", - "fieldtype": "Currency", - "width": 140, - }, - { - "label": _("Valuation Rate (FIFO)"), - "fieldname": "valuation_rate", - "fieldtype": "Currency", - "width": 160, - }, - { - "label": _("Price List"), - "fieldname": "price_list", - "fieldtype": "Link", - "options": "Price List", - "width": 160, - }, - { - "label": _("Current Selling Price"), - "fieldname": "current_selling_price", - "fieldtype": "Currency", - "width": 160, - }, - { - "label": _("Quantity Sold"), - "fieldname": "qty_sold", - "fieldtype": "Float", - "width": 120, - }, - { - "label": _("Available Quantity"), - "fieldname": "available_qty", - "fieldtype": "Float", - "width": 140, - }, - ] + return [ + { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 140, + }, + { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 200, + }, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 160, + }, + { + "label": _("Last Purchase Price"), + "fieldname": "last_purchase_price", + "fieldtype": "Currency", + "width": 140, + }, + { + "label": _("Valuation Rate (FIFO)"), + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "width": 160, + }, + { + "label": _("Price List"), + "fieldname": "price_list", + "fieldtype": "Link", + "options": "Price List", + "width": 160, + }, + { + "label": _("Current Selling Price"), + "fieldname": "current_selling_price", + "fieldtype": "Currency", + "width": 160, + }, + { + "label": _("Quantity Sold"), + "fieldname": "qty_sold", + "fieldtype": "Float", + "width": 120, + }, + { + "label": _("Available Quantity"), + "fieldname": "available_qty", + "fieldtype": "Float", + "width": 140, + }, + ] def get_data(filters): - Item = frappe.qb.DocType("Item") - items = ( - frappe.qb.from_(Item) - .select( - Item.name.as_("item_code"), - Item.item_name, - Item.item_group, - Item.last_purchase_rate, - ) - .where(Item.disabled == 0) - .run(as_dict=True) - ) - - sold_map = get_quantity_sold(filters) - bin_map = get_bin_snapshot(filters) - price_map = get_current_selling_prices(filters) - - data = [] - for item in items: - item_code = item.item_code - bin_row = bin_map.get(item_code, {}) - price_entries = price_map.get(item_code, [{}]) - - for price_info in price_entries: - data.append( - { - "item_code": item_code, - "item_name": item.item_name, - "item_group": item.item_group, - "last_purchase_price": flt(item.last_purchase_rate), - "valuation_rate": flt(bin_row.get("valuation_rate")), - "price_list": price_info.get("price_list"), - "current_selling_price": flt(price_info.get("price_list_rate")), - "qty_sold": flt(sold_map.get(item_code)), - "available_qty": flt(bin_row.get("available_qty")), - } - ) - - return data + Item = frappe.qb.DocType("Item") + items = ( + frappe.qb.from_(Item) + .select( + Item.name.as_("item_code"), + Item.item_name, + Item.item_group, + Item.last_purchase_rate, + ) + .where(Item.disabled == 0) + .run(as_dict=True) + ) + + sold_map = get_quantity_sold(filters) + bin_map = get_bin_snapshot(filters) + price_map = get_current_selling_prices(filters) + + data = [] + for item in items: + item_code = item.item_code + bin_row = bin_map.get(item_code, {}) + price_entries = price_map.get(item_code, [{}]) + + for price_info in price_entries: + data.append( + { + "item_code": item_code, + "item_name": item.item_name, + "item_group": item.item_group, + "last_purchase_price": flt(item.last_purchase_rate), + "valuation_rate": flt(bin_row.get("valuation_rate")), + "price_list": price_info.get("price_list"), + "current_selling_price": flt(price_info.get("price_list_rate")), + "qty_sold": flt(sold_map.get(item_code)), + "available_qty": flt(bin_row.get("available_qty")), + } + ) + + return data def get_filtered_warehouses(filters): - warehouse = filters.get("warehouse") - if not warehouse: - return None + warehouse = filters.get("warehouse") + if not warehouse: + return None - if frappe.db.get_value("Warehouse", warehouse, "is_group"): - lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - descendants = frappe.db.get_all( - "Warehouse", - {"lft": (">", lft), "rgt": ("<", rgt), "is_group": 0}, - pluck="name", - ) - return descendants or [warehouse] + if frappe.db.get_value("Warehouse", warehouse, "is_group"): + lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) + descendants = frappe.db.get_all( + "Warehouse", + {"lft": (">", lft), "rgt": ("<", rgt), "is_group": 0}, + pluck="name", + ) + return descendants or [warehouse] - return [warehouse] + return [warehouse] def get_quantity_sold(filters): - SalesInvoice = frappe.qb.DocType("Sales Invoice") - SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item") + SalesInvoice = frappe.qb.DocType("Sales Invoice") + SalesInvoiceItem = frappe.qb.DocType("Sales Invoice Item") - query = ( - frappe.qb.from_(SalesInvoiceItem) - .join(SalesInvoice) - .on(SalesInvoice.name == SalesInvoiceItem.parent) - .select( - SalesInvoiceItem.item_code, - qb_functions.Sum(SalesInvoiceItem.stock_qty).as_("qty_sold"), - ) - .where(SalesInvoice.docstatus == 1) - .where(SalesInvoice.posting_date.between(filters.from_date, filters.to_date)) - .groupby(SalesInvoiceItem.item_code) - ) + query = ( + frappe.qb.from_(SalesInvoiceItem) + .join(SalesInvoice) + .on(SalesInvoice.name == SalesInvoiceItem.parent) + .select( + SalesInvoiceItem.item_code, + qb_functions.Sum(SalesInvoiceItem.stock_qty).as_("qty_sold"), + ) + .where(SalesInvoice.docstatus == 1) + .where(SalesInvoice.posting_date.between(filters.from_date, filters.to_date)) + .groupby(SalesInvoiceItem.item_code) + ) - warehouses = get_filtered_warehouses(filters) - if warehouses is not None: - query = query.where(SalesInvoiceItem.warehouse.isin(warehouses)) + warehouses = get_filtered_warehouses(filters) + if warehouses is not None: + query = query.where(SalesInvoiceItem.warehouse.isin(warehouses)) - rows = query.run(as_dict=True) + rows = query.run(as_dict=True) - return {row.item_code: row.qty_sold for row in rows} + return {row.item_code: row.qty_sold for row in rows} def get_bin_snapshot(filters): - Bin = frappe.qb.DocType("Bin") + Bin = frappe.qb.DocType("Bin") - query = ( - frappe.qb.from_(Bin) - .select( - Bin.item_code, - qb_functions.Sum(Bin.actual_qty).as_("available_qty"), - qb_functions.Sum(Bin.stock_value).as_("stock_value"), - ) - .groupby(Bin.item_code) - ) + query = ( + frappe.qb.from_(Bin) + .select( + Bin.item_code, + qb_functions.Sum(Bin.actual_qty).as_("available_qty"), + qb_functions.Sum(Bin.stock_value).as_("stock_value"), + ) + .groupby(Bin.item_code) + ) - warehouses = get_filtered_warehouses(filters) - if warehouses is not None: - query = query.where(Bin.warehouse.isin(warehouses)) + warehouses = get_filtered_warehouses(filters) + if warehouses is not None: + query = query.where(Bin.warehouse.isin(warehouses)) - rows = query.run(as_dict=True) + rows = query.run(as_dict=True) - for row in rows: - total_qty = flt(row.get("available_qty")) - row["valuation_rate"] = flt(row.get("stock_value")) / total_qty if total_qty else 0 + for row in rows: + total_qty = flt(row.get("available_qty")) + row["valuation_rate"] = flt(row.get("stock_value")) / total_qty if total_qty else 0 - return {row.item_code: row for row in rows} + return {row.item_code: row for row in rows} def get_current_selling_prices(filters): - today = nowdate() - ItemPrice = frappe.qb.DocType("Item Price") - - query = ( - frappe.qb.from_(ItemPrice) - .select( - ItemPrice.item_code, - ItemPrice.price_list, - ItemPrice.price_list_rate, - ItemPrice.valid_from, - ItemPrice.modified, - ) - .where(ItemPrice.selling == 1) - .where(ItemPrice.valid_from.isnull() | (ItemPrice.valid_from <= today)) - .where(ItemPrice.valid_upto.isnull() | (ItemPrice.valid_upto >= today)) - .orderby(ItemPrice.item_code) - .orderby(qb_functions.IfNull(ItemPrice.valid_from, "1900-01-01"), order=frappe.qb.desc) - .orderby(ItemPrice.modified, order=frappe.qb.desc) - ) - - if filters.get("price_list"): - query = query.where(ItemPrice.price_list == filters.price_list) - - rows = query.run(as_dict=True) - - price_map = {} - seen = set() - for row in rows: - key = (row.item_code, row.price_list) - if key not in seen: - seen.add(key) - price_map.setdefault(row.item_code, []).append( - { - "price_list": row.price_list, - "price_list_rate": row.price_list_rate, - } - ) - - return price_map + today = nowdate() + ItemPrice = frappe.qb.DocType("Item Price") + + query = ( + frappe.qb.from_(ItemPrice) + .select( + ItemPrice.item_code, + ItemPrice.price_list, + ItemPrice.price_list_rate, + ItemPrice.valid_from, + ItemPrice.modified, + ) + .where(ItemPrice.selling == 1) + .where(ItemPrice.valid_from.isnull() | (ItemPrice.valid_from <= today)) + .where(ItemPrice.valid_upto.isnull() | (ItemPrice.valid_upto >= today)) + .orderby(ItemPrice.item_code) + .orderby(qb_functions.IfNull(ItemPrice.valid_from, "1900-01-01"), order=frappe.qb.desc) + .orderby(ItemPrice.modified, order=frappe.qb.desc) + ) + + if filters.get("price_list"): + query = query.where(ItemPrice.price_list == filters.price_list) + + rows = query.run(as_dict=True) + + price_map = {} + seen = set() + for row in rows: + key = (row.item_code, row.price_list) + if key not in seen: + seen.add(key) + price_map.setdefault(row.item_code, []).append( + { + "price_list": row.price_list, + "price_list_rate": row.price_list_rate, + } + ) + + return price_map diff --git a/csf_tz/csf_tz/report/paye_report_mapping/paye_report_mapping.py b/csf_tz/csf_tz/report/paye_report_mapping/paye_report_mapping.py index a7511a84..f938ba12 100644 --- a/csf_tz/csf_tz/report/paye_report_mapping/paye_report_mapping.py +++ b/csf_tz/csf_tz/report/paye_report_mapping/paye_report_mapping.py @@ -116,11 +116,8 @@ def get_data(filters): { "sn": idx, "employee_tin": employee_details.get("employee_tin"), - "employee_name": employee_details.get("employee_name") - or slip.employee_name, - "national_identification_number": employee_details.get( - "national_identification_number" - ), + "employee_name": employee_details.get("employee_name") or slip.employee_name, + "national_identification_number": employee_details.get("national_identification_number"), "type_of_employment": employee_details.get("type_of_employment"), "residential_status": employee_details.get("residential_status"), "social_security_number": employee_details.get("social_security_number"), diff --git a/csf_tz/csf_tz/report/salary_register_csf/salary_register_csf.py b/csf_tz/csf_tz/report/salary_register_csf/salary_register_csf.py index cb55eedd..3bbf5be0 100644 --- a/csf_tz/csf_tz/report/salary_register_csf/salary_register_csf.py +++ b/csf_tz/csf_tz/report/salary_register_csf/salary_register_csf.py @@ -2,545 +2,490 @@ # For license information, please see license.txt -import frappe import erpnext +import frappe from frappe import _ from frappe.utils import flt from frappe.utils.nestedset import get_descendants_of -salary_slip = frappe.qb.DocType("Salary Slip") -salary_detail = frappe.qb.DocType("Salary Detail") -salary_component = frappe.qb.DocType("Salary Component") - - def execute(filters=None): - if not filters: - filters = {} - currency = None - if filters.get("currency"): - currency = filters.get("currency") - company_currency = erpnext.get_company_currency(filters.get("company")) + if not filters: + filters = {} + currency = None + if filters.get("currency"): + currency = filters.get("currency") + company_currency = erpnext.get_company_currency(filters.get("company")) - if currency and currency == company_currency and filters.get("multi_currency"): - frappe.throw( - _( - f"Currency: {currency} on report filters and default company currency: {company_currency} cannot be same for Multi Currency Report, please change one of them." - ) - ) + if currency and currency == company_currency and filters.get("multi_currency"): + frappe.throw( + _( + f"Currency: {currency} on report filters and default company currency: {company_currency} cannot be same for Multi Currency Report, please change one of them." + ) + ) - salary_slips = get_salary_slips(filters) - if not salary_slips: - frappe.msgprint("No record found for the filters above") - return [], [] + salary_slips = get_salary_slips(filters) + if not salary_slips: + frappe.msgprint("No record found for the filters above") + return [], [] - return get_data(filters, salary_slips, currency, company_currency) + return get_data(filters, salary_slips, currency, company_currency) def get_data(filters, salary_slips, currency, company_currency): - earning_types, ded_types = get_earning_and_deduction_types(salary_slips) - columns = get_columns(filters, company_currency, earning_types, ded_types) - - ss_earning_map = get_salary_slip_details( - salary_slips, currency, company_currency, "earnings" - ) - ss_ded_map = get_salary_slip_details( - salary_slips, currency, company_currency, "deductions" - ) - - doj_map = get_employee_doj_map() - - data = [] - replace_currency_label = False - unique_columns = [column.get("fieldname") for column in columns] - for ss in salary_slips: - row = { - "salary_slip_id": ss.name, - "employee": ss.employee, - "employee_name": ss.employee_name, - "data_of_joining": doj_map.get(ss.employee), - "branch": ss.branch, - "department": ss.department, - "designation": ss.designation, - "company": ss.company, - "start_date": ss.start_date, - "end_date": ss.end_date, - "payment_days": ss.payment_days, - "currency": ss.currency, - } - if filters.get("multi_currency") and ss.currency != company_currency: - row["exchange_rate"] = ss.exchange_rate - - if "exchange_rate" not in unique_columns: - columns.append( - { - "label": _("Exchange Rate"), - "fieldname": "exchange_rate", - "fieldtype": "Float", - "width": 120, - } - ) - unique_columns.append("exchange_rate") - - row["leave_without_pay"] = ss.leave_without_pay - if "leave_without_pay" not in unique_columns: - columns.append( - { - "label": _("Leave Without Pay"), - "fieldname": "leave_without_pay", - "fieldtype": "Currency", - "options": "currency", - "width": 100, - } - ) - unique_columns.append("leave_without_pay") - - if filters.get("multi_currency") and ss.currency != company_currency: - row["leave_without_pay_" + str(company_currency).lower()] = flt( - ss.leave_without_pay - ) * flt(ss.exchange_rate) - - if ( - "leave_without_pay_" + str(company_currency).lower() - not in unique_columns - ): - columns.append( - { - "label": _(f"Leave Without Pay {company_currency}"), - "fieldname": "leave_without_pay_" - + str(company_currency).lower(), - "fieldtype": "Currency", - "options": "company_currency", - "width": 120, - } - ) - unique_columns.append( - "leave_without_pay_" + str(company_currency).lower() - ) - - update_column_width(ss, columns) - - for e in earning_types: - row.update({frappe.scrub(e): ss_earning_map.get(ss.name, {}).get(e)}) - if frappe.scrub(e) not in unique_columns: - columns.append( - { - "label": e, - "fieldname": frappe.scrub(e), - "fieldtype": "Currency", - "options": "currency", - "width": 120, - } - ) - unique_columns.append(frappe.scrub(e)) - - if filters.get("multi_currency") and ss.currency != company_currency: - e_amount = ss_earning_map.get(ss.name, {}).get(e) or 0 - row.update( - { - frappe.scrub(e) - + "_" - + str(company_currency).lower(): e_amount - * flt(ss.exchange_rate) - } - ) - - if ( - frappe.scrub(e) + "_" + str(company_currency).lower() - not in unique_columns - ): - columns.append( - { - "label": e + " " + str(company_currency), - "fieldname": frappe.scrub(e) - + "_" - + str(company_currency).lower(), - "fieldtype": "Currency", - "options": "company_currency", - "width": 120, - } - ) - unique_columns.append( - frappe.scrub(e) + "_" + str(company_currency).lower() - ) - - row["gross_pay"] = ss.gross_pay - if "gross_pay" not in unique_columns: - columns.append( - { - "label": _("Gross Pay"), - "fieldname": "gross_pay", - "fieldtype": "Currency", - "options": "currency", - "width": 120, - } - ) - unique_columns.append("gross_pay") - - if filters.get("multi_currency") and ss.currency != company_currency: - row.update( - { - "gross_pay_" - + str(company_currency).lower(): flt(ss.gross_pay) - * flt(ss.exchange_rate), - } - ) - - if "gross_pay_" + str(company_currency).lower() not in unique_columns: - columns.append( - { - "label": _(f"Gross Pay {company_currency}"), - "fieldname": "gross_pay_" + str(company_currency).lower(), - "fieldtype": "Currency", - "options": "company_currency", - "width": 120, - } - ) - unique_columns.append("gross_pay_" + str(company_currency).lower()) - - for d in ded_types: - row.update({frappe.scrub(d): ss_ded_map.get(ss.name, {}).get(d)}) - if frappe.scrub(d) not in unique_columns: - columns.append( - { - "label": d, - "fieldname": frappe.scrub(d), - "fieldtype": "Currency", - "options": "currency", - "width": 120, - } - ) - unique_columns.append(frappe.scrub(d)) - - if filters.get("multi_currency") and ss.currency != company_currency: - d_amount = ss_ded_map.get(ss.name, {}).get(d) or 0 - row.update( - { - frappe.scrub(d) - + "_" - + str(company_currency).lower(): d_amount - * flt(ss.exchange_rate) - } - ) - if ( - frappe.scrub(d) + "_" + str(company_currency).lower() - not in unique_columns - ): - columns.append( - { - "label": d + " " + str(company_currency), - "fieldname": frappe.scrub(d) - + "_" - + str(company_currency).lower(), - "fieldtype": "Currency", - "options": "company_currency", - "width": 120, - } - ) - unique_columns.append( - frappe.scrub(d) + "_" + str(company_currency).lower() - ) - - row.update( - { - "loan_repayment": ss.total_loan_repayment, - "total_deduction": ss.total_deduction, - "net_pay": ss.net_pay, - } - ) - if filters.get("multi_currency") and ss.currency != company_currency: - row.update( - { - "loan_repayment_" - + str(company_currency).lower(): flt(ss.total_loan_repayment) - * flt(ss.exchange_rate), - "total_deduction_" - + str(company_currency).lower(): flt(ss.total_deduction) - * flt(ss.exchange_rate), - "net_pay_" - + str(company_currency).lower(): flt(ss.net_pay) - * flt(ss.exchange_rate), - } - ) - - for field in ["loan_repayment", "total_deduction", "net_pay"]: - if field not in unique_columns: - columns.append( - { - "label": _(frappe.unscrub(field)), - "fieldname": field, - "fieldtype": "Currency", - "options": "currency", - "width": 120, - } - ) - unique_columns.append(field) - - if filters.get("multi_currency") and ss.currency != company_currency: - report_fieldname = field + "_" + str(company_currency).lower() - if report_fieldname not in unique_columns: - columns.append( - { - "label": _(f"{frappe.unscrub(field)} {company_currency}"), - "fieldname": report_fieldname, - "fieldtype": "Currency", - "options": "company_currency", - "width": 120, - } - ) - unique_columns.append(report_fieldname) - - data.append(row) - - if not replace_currency_label: - for col in columns: - if ( - # col.get("options") == "currency" - company_currency - in col["label"] - ): - col["label"] = col["label"].replace(ss.currency, "") - replace_currency_label = True - - return columns, data + earning_types, ded_types = get_earning_and_deduction_types(salary_slips) + columns = get_columns(filters, company_currency, earning_types, ded_types) + + ss_earning_map = get_salary_slip_details(salary_slips, currency, company_currency, "earnings") + ss_ded_map = get_salary_slip_details(salary_slips, currency, company_currency, "deductions") + + doj_map = get_employee_doj_map() + + data = [] + replace_currency_label = False + unique_columns = [column.get("fieldname") for column in columns] + for ss in salary_slips: + row = { + "salary_slip_id": ss.name, + "employee": ss.employee, + "employee_name": ss.employee_name, + "data_of_joining": doj_map.get(ss.employee), + "branch": ss.branch, + "department": ss.department, + "designation": ss.designation, + "company": ss.company, + "start_date": ss.start_date, + "end_date": ss.end_date, + "payment_days": ss.payment_days, + "currency": ss.currency, + } + if filters.get("multi_currency") and ss.currency != company_currency: + row["exchange_rate"] = ss.exchange_rate + + if "exchange_rate" not in unique_columns: + columns.append( + { + "label": _("Exchange Rate"), + "fieldname": "exchange_rate", + "fieldtype": "Float", + "width": 120, + } + ) + unique_columns.append("exchange_rate") + + row["leave_without_pay"] = ss.leave_without_pay + if "leave_without_pay" not in unique_columns: + columns.append( + { + "label": _("Leave Without Pay"), + "fieldname": "leave_without_pay", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + } + ) + unique_columns.append("leave_without_pay") + + if filters.get("multi_currency") and ss.currency != company_currency: + row["leave_without_pay_" + str(company_currency).lower()] = flt(ss.leave_without_pay) * flt( + ss.exchange_rate + ) + + if "leave_without_pay_" + str(company_currency).lower() not in unique_columns: + columns.append( + { + "label": _(f"Leave Without Pay {company_currency}"), + "fieldname": "leave_without_pay_" + str(company_currency).lower(), + "fieldtype": "Currency", + "options": "company_currency", + "width": 120, + } + ) + unique_columns.append("leave_without_pay_" + str(company_currency).lower()) + + update_column_width(ss, columns) + + for e in earning_types: + row.update({frappe.scrub(e): ss_earning_map.get(ss.name, {}).get(e)}) + if frappe.scrub(e) not in unique_columns: + columns.append( + { + "label": e, + "fieldname": frappe.scrub(e), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) + unique_columns.append(frappe.scrub(e)) + + if filters.get("multi_currency") and ss.currency != company_currency: + e_amount = ss_earning_map.get(ss.name, {}).get(e) or 0 + row.update( + {frappe.scrub(e) + "_" + str(company_currency).lower(): e_amount * flt(ss.exchange_rate)} + ) + + if frappe.scrub(e) + "_" + str(company_currency).lower() not in unique_columns: + columns.append( + { + "label": e + " " + str(company_currency), + "fieldname": frappe.scrub(e) + "_" + str(company_currency).lower(), + "fieldtype": "Currency", + "options": "company_currency", + "width": 120, + } + ) + unique_columns.append(frappe.scrub(e) + "_" + str(company_currency).lower()) + + row["gross_pay"] = ss.gross_pay + if "gross_pay" not in unique_columns: + columns.append( + { + "label": _("Gross Pay"), + "fieldname": "gross_pay", + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) + unique_columns.append("gross_pay") + + if filters.get("multi_currency") and ss.currency != company_currency: + row.update( + { + "gross_pay_" + str(company_currency).lower(): flt(ss.gross_pay) * flt(ss.exchange_rate), + } + ) + + if "gross_pay_" + str(company_currency).lower() not in unique_columns: + columns.append( + { + "label": _(f"Gross Pay {company_currency}"), + "fieldname": "gross_pay_" + str(company_currency).lower(), + "fieldtype": "Currency", + "options": "company_currency", + "width": 120, + } + ) + unique_columns.append("gross_pay_" + str(company_currency).lower()) + + for d in ded_types: + row.update({frappe.scrub(d): ss_ded_map.get(ss.name, {}).get(d)}) + if frappe.scrub(d) not in unique_columns: + columns.append( + { + "label": d, + "fieldname": frappe.scrub(d), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) + unique_columns.append(frappe.scrub(d)) + + if filters.get("multi_currency") and ss.currency != company_currency: + d_amount = ss_ded_map.get(ss.name, {}).get(d) or 0 + row.update( + {frappe.scrub(d) + "_" + str(company_currency).lower(): d_amount * flt(ss.exchange_rate)} + ) + if frappe.scrub(d) + "_" + str(company_currency).lower() not in unique_columns: + columns.append( + { + "label": d + " " + str(company_currency), + "fieldname": frappe.scrub(d) + "_" + str(company_currency).lower(), + "fieldtype": "Currency", + "options": "company_currency", + "width": 120, + } + ) + unique_columns.append(frappe.scrub(d) + "_" + str(company_currency).lower()) + + row.update( + { + "loan_repayment": ss.total_loan_repayment, + "total_deduction": ss.total_deduction, + "net_pay": ss.net_pay, + } + ) + if filters.get("multi_currency") and ss.currency != company_currency: + row.update( + { + "loan_repayment_" + str(company_currency).lower(): flt(ss.total_loan_repayment) + * flt(ss.exchange_rate), + "total_deduction_" + str(company_currency).lower(): flt(ss.total_deduction) + * flt(ss.exchange_rate), + "net_pay_" + str(company_currency).lower(): flt(ss.net_pay) * flt(ss.exchange_rate), + } + ) + + for field in ["loan_repayment", "total_deduction", "net_pay"]: + if field not in unique_columns: + columns.append( + { + "label": _(frappe.unscrub(field)), + "fieldname": field, + "fieldtype": "Currency", + "options": "currency", + "width": 120, + } + ) + unique_columns.append(field) + + if filters.get("multi_currency") and ss.currency != company_currency: + report_fieldname = field + "_" + str(company_currency).lower() + if report_fieldname not in unique_columns: + columns.append( + { + "label": _(f"{frappe.unscrub(field)} {company_currency}"), + "fieldname": report_fieldname, + "fieldtype": "Currency", + "options": "company_currency", + "width": 120, + } + ) + unique_columns.append(report_fieldname) + + data.append(row) + + if not replace_currency_label: + for col in columns: + if ( + # col.get("options") == "currency" + company_currency in col["label"] + ): + col["label"] = col["label"].replace(ss.currency, "") + replace_currency_label = True + + return columns, data def get_earning_and_deduction_types(salary_slips): - salary_component_and_type = {_("Earning"): [], _("Deduction"): []} - salary_components = get_salary_components(salary_slips) + salary_component_and_type = {_("Earning"): [], _("Deduction"): []} + salary_components = get_salary_components(salary_slips) - for component in salary_components: - component_type = get_salary_component_type(component.salary_component) - salary_component_and_type[_(component_type)].append(component.salary_component) - return sorted(salary_component_and_type[_("Earning")]), sorted( - salary_component_and_type[_("Deduction")] - ) + for component in salary_components: + component_type = get_salary_component_type(component.salary_component) + salary_component_and_type[_(component_type)].append(component.salary_component) + return sorted(salary_component_and_type[_("Earning")]), sorted(salary_component_and_type[_("Deduction")]) def update_column_width(ss, columns): - if ss.branch is not None: - columns[3].update({"width": 120}) - if ss.department is not None: - columns[4].update({"width": 120}) - if ss.designation is not None: - columns[5].update({"width": 120}) - if ss.leave_without_pay is not None: - columns[9].update({"width": 120}) + if ss.branch is not None: + columns[3].update({"width": 120}) + if ss.department is not None: + columns[4].update({"width": 120}) + if ss.designation is not None: + columns[5].update({"width": 120}) + if ss.leave_without_pay is not None: + columns[9].update({"width": 120}) def get_columns(filters, company_currency, earning_types, ded_types): - columns = [ - { - "label": _("Salary Slip ID"), - "fieldname": "salary_slip_id", - "fieldtype": "Link", - "options": "Salary Slip", - "width": 150, - }, - { - "label": _("Employee"), - "fieldname": "employee", - "fieldtype": "Link", - "options": "Employee", - "width": 120, - }, - { - "label": _("Employee Name"), - "fieldname": "employee_name", - "fieldtype": "Data", - "width": 140, - }, - { - "label": _("Date of Joining"), - "fieldname": "data_of_joining", - "fieldtype": "Date", - "width": 80, - }, - { - "label": _("Branch"), - "fieldname": "branch", - "fieldtype": "Link", - "options": "Branch", - "width": -1, - }, - { - "label": _("Department"), - "fieldname": "department", - "fieldtype": "Link", - "options": "Department", - "width": -1, - }, - { - "label": _("Designation"), - "fieldname": "designation", - "fieldtype": "Link", - "options": "Designation", - "width": 120, - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - "width": 120, - }, - { - "label": _("Start Date"), - "fieldname": "start_date", - "fieldtype": "Date", - "width": 80, - }, - { - "label": _("End Date"), - "fieldname": "end_date", - "fieldtype": "Date", - "width": 80, - }, - # { - # "label": _("Currency"), - # "fieldtype": "Link", - # "fieldname": "currency", - # "options": "Currency", - # "hidden": 1, - # }, - { - "label": _("Payment Days"), - "fieldname": "payment_days", - "fieldtype": "Int", - "width": 120, - }, - ] - return columns + columns = [ + { + "label": _("Salary Slip ID"), + "fieldname": "salary_slip_id", + "fieldtype": "Link", + "options": "Salary Slip", + "width": 150, + }, + { + "label": _("Employee"), + "fieldname": "employee", + "fieldtype": "Link", + "options": "Employee", + "width": 120, + }, + { + "label": _("Employee Name"), + "fieldname": "employee_name", + "fieldtype": "Data", + "width": 140, + }, + { + "label": _("Date of Joining"), + "fieldname": "data_of_joining", + "fieldtype": "Date", + "width": 80, + }, + { + "label": _("Branch"), + "fieldname": "branch", + "fieldtype": "Link", + "options": "Branch", + "width": -1, + }, + { + "label": _("Department"), + "fieldname": "department", + "fieldtype": "Link", + "options": "Department", + "width": -1, + }, + { + "label": _("Designation"), + "fieldname": "designation", + "fieldtype": "Link", + "options": "Designation", + "width": 120, + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 120, + }, + { + "label": _("Start Date"), + "fieldname": "start_date", + "fieldtype": "Date", + "width": 80, + }, + { + "label": _("End Date"), + "fieldname": "end_date", + "fieldtype": "Date", + "width": 80, + }, + # { + # "label": _("Currency"), + # "fieldtype": "Link", + # "fieldname": "currency", + # "options": "Currency", + # "hidden": 1, + # }, + { + "label": _("Payment Days"), + "fieldname": "payment_days", + "fieldtype": "Int", + "width": 120, + }, + ] + return columns def get_salary_components(salary_slips): - return ( - frappe.qb.from_(salary_detail) - .where( - (salary_detail.amount != 0) - & (salary_detail.parent.isin([d.name for d in salary_slips])) - ) - .select(salary_detail.salary_component) - .distinct() - ).run(as_dict=True) + salary_detail = frappe.qb.DocType("Salary Detail") + return ( + frappe.qb.from_(salary_detail) + .where((salary_detail.amount != 0) & (salary_detail.parent.isin([d.name for d in salary_slips]))) + .select(salary_detail.salary_component) + .distinct() + ).run(as_dict=True) def get_salary_component_type(salary_component): - return frappe.db.get_value("Salary Component", salary_component, "type", cache=True) + return frappe.db.get_value("Salary Component", salary_component, "type", cache=True) def get_salary_slips(filters): - doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} + salary_slip = frappe.qb.DocType("Salary Slip") + doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} - query = frappe.qb.from_(salary_slip).select(salary_slip.star) + query = frappe.qb.from_(salary_slip).select(salary_slip.star) - if filters.get("docstatus"): - query = query.where( - salary_slip.docstatus == doc_status[filters.get("docstatus")] - ) + if filters.get("docstatus"): + query = query.where(salary_slip.docstatus == doc_status[filters.get("docstatus")]) - if filters.get("from_date"): - query = query.where(salary_slip.start_date >= filters.get("from_date")) + if filters.get("from_date"): + query = query.where(salary_slip.start_date >= filters.get("from_date")) - if filters.get("to_date"): - query = query.where(salary_slip.end_date <= filters.get("to_date")) + if filters.get("to_date"): + query = query.where(salary_slip.end_date <= filters.get("to_date")) - if filters.get("company"): - query = query.where(salary_slip.company == filters.get("company")) + if filters.get("company"): + query = query.where(salary_slip.company == filters.get("company")) - if filters.get("employee"): - query = query.where(salary_slip.employee == filters.get("employee")) + if filters.get("employee"): + query = query.where(salary_slip.employee == filters.get("employee")) - if filters.get("currency") and filters.get("currency"): - query = query.where(salary_slip.currency == filters.get("currency")) - if filters.get("department") and filters.get("company"): - department_list = get_departments( - filters.get("department"), filters.get("company") - ) - query = query.where(salary_slip.department.isin(department_list)) + if filters.get("currency") and filters.get("currency"): + query = query.where(salary_slip.currency == filters.get("currency")) + if filters.get("department") and filters.get("company"): + department_list = get_departments(filters.get("department"), filters.get("company")) + query = query.where(salary_slip.department.isin(department_list)) - salary_slips = query.run(as_dict=1) + salary_slips = query.run(as_dict=1) - return salary_slips or [] + return salary_slips or [] def get_employee_doj_map(): - employee = frappe.qb.DocType("Employee") + employee = frappe.qb.DocType("Employee") - result = ( - frappe.qb.from_(employee).select(employee.name, employee.date_of_joining) - ).run() + result = (frappe.qb.from_(employee).select(employee.name, employee.date_of_joining)).run() - return frappe._dict(result) + return frappe._dict(result) def get_salary_slip_details(salary_slips, currency, company_currency, component_type): - salary_slips = [ss.name for ss in salary_slips] - - result = ( - frappe.qb.from_(salary_slip) - .join(salary_detail) - .on(salary_slip.name == salary_detail.parent) - .where( - (salary_detail.parent.isin(salary_slips)) - & (salary_detail.parentfield == component_type) - ) - .select( - salary_detail.parent, - salary_detail.salary_component, - salary_detail.amount, - salary_slip.exchange_rate, - ) - ).run(as_dict=1) - - ss_map = {} - - for d in result: - ss_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0) - if currency == company_currency: - ss_map[d.parent][d.salary_component] += flt(d.amount) * flt( - d.exchange_rate if d.exchange_rate else 1 - ) - else: - ss_map[d.parent][d.salary_component] += flt(d.amount) - - return ss_map + salary_slip = frappe.qb.DocType("Salary Slip") + salary_detail = frappe.qb.DocType("Salary Detail") + salary_slips = [ss.name for ss in salary_slips] + + result = ( + frappe.qb.from_(salary_slip) + .join(salary_detail) + .on(salary_slip.name == salary_detail.parent) + .where((salary_detail.parent.isin(salary_slips)) & (salary_detail.parentfield == component_type)) + .select( + salary_detail.parent, + salary_detail.salary_component, + salary_detail.amount, + salary_slip.exchange_rate, + ) + ).run(as_dict=1) + + ss_map = {} + + for d in result: + ss_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, 0.0) + if currency == company_currency: + ss_map[d.parent][d.salary_component] += flt(d.amount) * flt( + d.exchange_rate if d.exchange_rate else 1 + ) + else: + ss_map[d.parent][d.salary_component] += flt(d.amount) + + return ss_map def get_departments(department, company): - departments_list = get_descendants_of("Department", department) - departments_list.append(department) - return departments_list + departments_list = get_descendants_of("Department", department) + departments_list.append(department) + return departments_list @frappe.whitelist() def approve(data): - from frappe.utils.background_jobs import enqueue - import json + import json + + from frappe.utils.background_jobs import enqueue - data = json.loads(data) - enqueue( - method=enqueue_approve, - queue="short", - timeout=10000, - job_name="approve_salary_slip", - is_async=True, - kwargs=data, - ) - return _("Start Processing") + data = json.loads(data) + enqueue( + method=enqueue_approve, + queue="short", + timeout=10000, + job_name="approve_salary_slip", + is_async=True, + kwargs=data, + ) + return _("Start Processing") def enqueue_approve(kwargs): - from frappe.model.workflow import apply_workflow - - data = kwargs - for i in data: - if not i.get("salary_slip_id") or i.get("salary_slip_id") == "Total": - continue - doc = frappe.get_doc("Salary Slip", i.get("salary_slip_id")) - if doc.workflow_state == "Pending": - try: - apply_workflow(doc, "Approve") - frappe.db.commit() - except Exception as e: - frappe.log_error(e) + from frappe.model.workflow import apply_workflow + + data = kwargs + for i in data: + if not i.get("salary_slip_id") or i.get("salary_slip_id") == "Total": + continue + doc = frappe.get_doc("Salary Slip", i.get("salary_slip_id")) + if doc.workflow_state == "Pending": + try: + apply_workflow(doc, "Approve") + frappe.db.commit() + except Exception as e: + frappe.log_error(e) diff --git a/csf_tz/csf_tz/report/salary_register_ctc/salary_register_ctc.py b/csf_tz/csf_tz/report/salary_register_ctc/salary_register_ctc.py index 5a7b4d06..87bfc163 100644 --- a/csf_tz/csf_tz/report/salary_register_ctc/salary_register_ctc.py +++ b/csf_tz/csf_tz/report/salary_register_ctc/salary_register_ctc.py @@ -1,310 +1,307 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -import frappe import erpnext +import frappe +from frappe import _ from frappe.utils import flt -from frappe import _, msgprint from frappe.utils.nestedset import get_descendants_of def execute(filters=None): - if not filters: - filters = {} - currency = None - if filters.get("currency"): - currency = filters.get("currency") - company_currency = erpnext.get_company_currency(filters.get("company")) - salary_slips = get_salary_slips(filters, company_currency) - if not salary_slips: - return [], [] - - columns, earning_types, ded_types, ded_types_ctc = get_columns(salary_slips) - ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) - ss_ded_map = get_ss_ded_map(salary_slips, currency, company_currency) - doj_map = get_employee_doj_map() - - data = [] - for ss in salary_slips: - CTC = ss.gross_pay - row = [ - ss.name, - ss.employee, - ss.employee_name, - doj_map.get(ss.employee), - ss.branch, - ss.department, - ss.designation, - ss.company, - ss.start_date, - ss.end_date, - ss.leave_without_pay, - ss.payment_days, - ] - - if ss.branch is not None: - columns[3] = columns[3].replace("-1", "120") - if ss.department is not None: - columns[4] = columns[4].replace("-1", "120") - if ss.designation is not None: - columns[5] = columns[5].replace("-1", "120") - if ss.leave_without_pay is not None: - columns[9] = columns[9].replace("-1", "130") - - for e in earning_types: - row.append(ss_earning_map.get(ss.name, {}).get(e)) - - if currency == company_currency: - row += [flt(ss.gross_pay) * flt(ss.exchange_rate)] - else: - row += [ss.gross_pay] - - for d in ded_types: - row.append(ss_ded_map.get(ss.name, {}).get(d)) - - row.append(ss.total_loan_repayment) - - if currency == company_currency: - row += [ - flt(ss.total_deduction) * flt(ss.exchange_rate), - flt(ss.net_pay) * flt(ss.exchange_rate), - ] - else: - row += [ss.total_deduction, ss.net_pay] - - for d_ctc in ded_types_ctc: - amount = flt(ss_ded_map.get(ss.name, {}).get(d_ctc)) - CTC = CTC + amount - row.append(amount) - - row.append(CTC) - row.append(currency or company_currency) - data.append(row) - - columns += [_("CTC") + ":Currency:120"] - - return columns, data + if not filters: + filters = {} + currency = None + if filters.get("currency"): + currency = filters.get("currency") + company_currency = erpnext.get_company_currency(filters.get("company")) + salary_slips = get_salary_slips(filters, company_currency) + if not salary_slips: + return [], [] + + columns, earning_types, ded_types, ded_types_ctc = get_columns(salary_slips) + ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) + ss_ded_map = get_ss_ded_map(salary_slips, currency, company_currency) + doj_map = get_employee_doj_map() + + data = [] + for ss in salary_slips: + CTC = ss.gross_pay + row = [ + ss.name, + ss.employee, + ss.employee_name, + doj_map.get(ss.employee), + ss.branch, + ss.department, + ss.designation, + ss.company, + ss.start_date, + ss.end_date, + ss.leave_without_pay, + ss.payment_days, + ] + + if ss.branch is not None: + columns[3] = columns[3].replace("-1", "120") + if ss.department is not None: + columns[4] = columns[4].replace("-1", "120") + if ss.designation is not None: + columns[5] = columns[5].replace("-1", "120") + if ss.leave_without_pay is not None: + columns[9] = columns[9].replace("-1", "130") + + for e in earning_types: + row.append(ss_earning_map.get(ss.name, {}).get(e)) + + if currency == company_currency: + row += [flt(ss.gross_pay) * flt(ss.exchange_rate)] + else: + row += [ss.gross_pay] + + for d in ded_types: + row.append(ss_ded_map.get(ss.name, {}).get(d)) + + row.append(ss.total_loan_repayment) + + if currency == company_currency: + row += [ + flt(ss.total_deduction) * flt(ss.exchange_rate), + flt(ss.net_pay) * flt(ss.exchange_rate), + ] + else: + row += [ss.total_deduction, ss.net_pay] + + for d_ctc in ded_types_ctc: + amount = flt(ss_ded_map.get(ss.name, {}).get(d_ctc)) + CTC = CTC + amount + row.append(amount) + + row.append(CTC) + row.append(currency or company_currency) + data.append(row) + + columns += [_("CTC") + ":Currency:120"] + + return columns, data def get_columns(salary_slips): - """ - columns = [ - _("Salary Slip ID") + ":Link/Salary Slip:150", - _("Employee") + ":Link/Employee:120", - _("Employee Name") + "::140", - _("Date of Joining") + "::80", - _("Branch") + ":Link/Branch:120", - _("Department") + ":Link/Department:120", - _("Designation") + ":Link/Designation:120", - _("Company") + ":Link/Company:120", - _("Start Date") + "::80", - _("End Date") + "::80", - _("Leave Without Pay") + ":Float:130", - _("Payment Days") + ":Float:120", - _("Currency") + ":Link/Currency:80" - ] - """ - columns = [ - _("Salary Slip ID") + ":Link/Salary Slip:150", - _("Employee") + ":Link/Employee:120", - _("Employee Name") + "::140", - _("Date of Joining") + "::80", - _("Branch") + ":Link/Branch:-1", - _("Department") + ":Link/Department:-1", - _("Designation") + ":Link/Designation:120", - _("Company") + ":Link/Company:120", - _("Start Date") + "::80", - _("End Date") + "::80", - _("Leave Without Pay") + ":Float:50", - _("Payment Days") + ":Float:120", - ] - - salary_components = {_("Earning"): [], _("Deduction"): []} - - for component in frappe.db.sql( - """select distinct sd.salary_component, sc.type + """ + columns = [ + _("Salary Slip ID") + ":Link/Salary Slip:150", + _("Employee") + ":Link/Employee:120", + _("Employee Name") + "::140", + _("Date of Joining") + "::80", + _("Branch") + ":Link/Branch:120", + _("Department") + ":Link/Department:120", + _("Designation") + ":Link/Designation:120", + _("Company") + ":Link/Company:120", + _("Start Date") + "::80", + _("End Date") + "::80", + _("Leave Without Pay") + ":Float:130", + _("Payment Days") + ":Float:120", + _("Currency") + ":Link/Currency:80" + ] + """ + columns = [ + _("Salary Slip ID") + ":Link/Salary Slip:150", + _("Employee") + ":Link/Employee:120", + _("Employee Name") + "::140", + _("Date of Joining") + "::80", + _("Branch") + ":Link/Branch:-1", + _("Department") + ":Link/Department:-1", + _("Designation") + ":Link/Designation:120", + _("Company") + ":Link/Company:120", + _("Start Date") + "::80", + _("End Date") + "::80", + _("Leave Without Pay") + ":Float:50", + _("Payment Days") + ":Float:120", + ] + + salary_components = {_("Earning"): [], _("Deduction"): []} + + for component in frappe.db.sql( + """select distinct sd.salary_component, sc.type from `tabSalary Detail` sd, `tabSalary Component` sc - where sc.do_not_include_in_total = 0 and sc.name=sd.salary_component and sd.amount != 0 and sd.parent in (%s)""" - % (", ".join(["%s"] * len(salary_slips))), - tuple([d.name for d in salary_slips]), - as_dict=1, - ): - salary_components[_(component.type)].append(component.salary_component) - - columns = ( - columns - + [(e + ":Currency:120") for e in salary_components[_("Earning")]] - + [_("Gross Pay") + ":Currency:120"] - + [(d + ":Currency:120") for d in salary_components[_("Deduction")]] - + [ - _("Loan Repayment") + ":Currency:120", - _("Total Deduction") + ":Currency:120", - _("Net Pay") + ":Currency:120", - ] - ) - components = {_("Deduction"): []} - company_expenses = frappe.db.sql(""" + where sc.do_not_include_in_total = 0 and sc.name=sd.salary_component and sd.amount != 0 and sd.parent in ({})""".format( + ", ".join(["%s"] * len(salary_slips)) + ), + tuple([d.name for d in salary_slips]), + as_dict=1, + ): + salary_components[_(component.type)].append(component.salary_component) + + columns = ( + columns + + [(e + ":Currency:120") for e in salary_components[_("Earning")]] + + [_("Gross Pay") + ":Currency:120"] + + [(d + ":Currency:120") for d in salary_components[_("Deduction")]] + + [ + _("Loan Repayment") + ":Currency:120", + _("Total Deduction") + ":Currency:120", + _("Net Pay") + ":Currency:120", + ] + ) + components = {_("Deduction"): []} + company_expenses = frappe.db.sql( + """ SELECT distinct sd.salary_component, sc.type FROM `tabSalary Detail` sd, `tabSalary Component` sc WHERE sc.do_not_include_in_total = 1 AND sc.type = "Deduction" AND sc.name=sd.salary_component AND sd.amount != 0 - AND sd.parent in (%s)""" - % (", ".join(["%s"] * len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1 - ) - for entry in company_expenses: - components[_(entry.type)].append(entry.salary_component) - - columns = (columns + [(d_ctc + ":Currency:120") for d_ctc in components[_("Deduction")]]) + AND sd.parent in ({})""".format(", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) + for entry in company_expenses: + components[_(entry.type)].append(entry.salary_component) + + columns = columns + [(d_ctc + ":Currency:120") for d_ctc in components[_("Deduction")]] - return columns, salary_components[_("Earning")], salary_components[_("Deduction")], components[_("Deduction")] + return ( + columns, + salary_components[_("Earning")], + salary_components[_("Deduction")], + components[_("Deduction")], + ) def get_salary_slips(filters, company_currency): - filters.update( - {"from_date": filters.get("from_date"), "to_date": filters.get("to_date")} - ) - conditions, filters = get_conditions(filters, company_currency) - salary_slips = frappe.db.sql( - """select * from `tabSalary Slip` where %s - order by employee""" - % conditions, - filters, - as_dict=1, - ) + filters.update({"from_date": filters.get("from_date"), "to_date": filters.get("to_date")}) + conditions, filters = get_conditions(filters, company_currency) + salary_slips = frappe.db.sql( + f"""select * from `tabSalary Slip` where {conditions} + order by employee""", + filters, + as_dict=1, + ) - return salary_slips or [] + return salary_slips or [] def get_conditions(filters, company_currency): - conditions = "" - doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} - - if filters.get("docstatus"): - conditions += "docstatus = {0}".format(doc_status[filters.get("docstatus")]) - - if filters.get("from_date"): - conditions += " and start_date >= %(from_date)s" - if filters.get("to_date"): - conditions += " and end_date <= %(to_date)s" - if filters.get("company"): - conditions += " and company = %(company)s" - if filters.get("employee"): - conditions += " and employee = %(employee)s" - if filters.get("currency") and filters.get("currency") != company_currency: - conditions += " and currency = %(currency)s" - if filters.get("department") and filters.get("company"): - department_list = get_departments( - filters.get("department"), filters.get("company") - ) - conditions += ( - "and department in (" - + ",".join(("'" + n + "'" for n in department_list)) - + ")" - ) - if filters.get("workflow_state"): - conditions += " and workflow_state = %(workflow_state)s" - - return conditions, filters + conditions = "" + doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} + + if filters.get("docstatus"): + conditions += "docstatus = {}".format(doc_status[filters.get("docstatus")]) + + if filters.get("from_date"): + conditions += " and start_date >= %(from_date)s" + if filters.get("to_date"): + conditions += " and end_date <= %(to_date)s" + if filters.get("company"): + conditions += " and company = %(company)s" + if filters.get("employee"): + conditions += " and employee = %(employee)s" + if filters.get("currency") and filters.get("currency") != company_currency: + conditions += " and currency = %(currency)s" + if filters.get("department") and filters.get("company"): + department_list = get_departments(filters.get("department"), filters.get("company")) + conditions += "and department in (" + ",".join("'" + n + "'" for n in department_list) + ")" + if filters.get("workflow_state"): + conditions += " and workflow_state = %(workflow_state)s" + + return conditions, filters def get_employee_doj_map(): - return frappe._dict( - frappe.db.sql( - """ + return frappe._dict( + frappe.db.sql( + """ SELECT employee, date_of_joining FROM `tabEmployee` """ - ) - ) + ) + ) def get_ss_earning_map(salary_slips, currency, company_currency): - ss_earnings = frappe.db.sql( - """select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name - from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" - % (", ".join(["%s"] * len(salary_slips))), - tuple([d.name for d in salary_slips]), - as_dict=1, - ) - - ss_earning_map = {} - for d in ss_earnings: - ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault( - d.salary_component, [] - ) - if currency == company_currency: - ss_earning_map[d.parent][d.salary_component] = flt(d.amount) * flt( - d.exchange_rate if d.exchange_rate else 1 - ) - else: - ss_earning_map[d.parent][d.salary_component] = flt(d.amount) - - return ss_earning_map + ss_earnings = frappe.db.sql( + """select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in ({})""".format( + ", ".join(["%s"] * len(salary_slips)) + ), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) + + ss_earning_map = {} + for d in ss_earnings: + ss_earning_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) + if currency == company_currency: + ss_earning_map[d.parent][d.salary_component] = flt(d.amount) * flt( + d.exchange_rate if d.exchange_rate else 1 + ) + else: + ss_earning_map[d.parent][d.salary_component] = flt(d.amount) + + return ss_earning_map def get_ss_ded_map(salary_slips, currency, company_currency): - ss_deductions = frappe.db.sql( - """select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name - from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in (%s)""" - % (", ".join(["%s"] * len(salary_slips))), - tuple([d.name for d in salary_slips]), - as_dict=1, - ) - - ss_ded_map = {} - for d in ss_deductions: - ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault( - d.salary_component, [] - ) - if currency == company_currency: - ss_ded_map[d.parent][d.salary_component] = flt(d.amount) * flt( - d.exchange_rate if d.exchange_rate else 1 - ) - else: - ss_ded_map[d.parent][d.salary_component] = flt(d.amount) - - return ss_ded_map + ss_deductions = frappe.db.sql( + """select sd.parent, sd.salary_component, sd.amount, ss.exchange_rate, ss.name + from `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name and sd.parent in ({})""".format( + ", ".join(["%s"] * len(salary_slips)) + ), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) + + ss_ded_map = {} + for d in ss_deductions: + ss_ded_map.setdefault(d.parent, frappe._dict()).setdefault(d.salary_component, []) + if currency == company_currency: + ss_ded_map[d.parent][d.salary_component] = flt(d.amount) * flt( + d.exchange_rate if d.exchange_rate else 1 + ) + else: + ss_ded_map[d.parent][d.salary_component] = flt(d.amount) + + return ss_ded_map def get_departments(department, company): - departments_list = get_descendants_of("Department", department) - departments_list.append(department) - return departments_list + departments_list = get_descendants_of("Department", department) + departments_list.append(department) + return departments_list @frappe.whitelist() def approve(data): - from frappe.utils.background_jobs import enqueue - import json + import json - data = json.loads(data) - enqueue( - method=enqueue_approve, - queue="short", - timeout=10000, - job_name="approve_salary_slip", - is_async=True, - kwargs=data, - ) - return _("Start Processing") + from frappe.utils.background_jobs import enqueue + data = json.loads(data) + enqueue( + method=enqueue_approve, + queue="short", + timeout=10000, + job_name="approve_salary_slip", + is_async=True, + kwargs=data, + ) + return _("Start Processing") -def enqueue_approve(kwargs): - from frappe.model.workflow import apply_workflow - - data = kwargs - for i in data: - if not i.get("salary_slip_id") or i.get("salary_slip_id") == "Total": - continue - doc = frappe.get_doc("Salary Slip", i.get("salary_slip_id")) - if doc.workflow_state == "Pending": - try: - apply_workflow(doc, "Approve") - frappe.db.commit() - except Exception as e: - frappe.log_error(e) +def enqueue_approve(kwargs): + from frappe.model.workflow import apply_workflow + + data = kwargs + for i in data: + if not i.get("salary_slip_id") or i.get("salary_slip_id") == "Total": + continue + doc = frappe.get_doc("Salary Slip", i.get("salary_slip_id")) + if doc.workflow_state == "Pending": + try: + apply_workflow(doc, "Approve") + frappe.db.commit() + except Exception as e: + frappe.log_error(e) diff --git a/csf_tz/csf_tz/report/salary_register_summary/salary_register_summary.py b/csf_tz/csf_tz/report/salary_register_summary/salary_register_summary.py index 30a3fe29..f7b595b1 100644 --- a/csf_tz/csf_tz/report/salary_register_summary/salary_register_summary.py +++ b/csf_tz/csf_tz/report/salary_register_summary/salary_register_summary.py @@ -1,171 +1,168 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import erpnext -from frappe.utils import flt -from frappe import _, msgprint +import frappe +from frappe import _ from frappe.utils.nestedset import get_descendants_of def execute(filters=None): - columns = [{ - "fieldname": "salary_component", - "label": _("Salary Component"), - "fieldtype": "Data", - "width": 300 - }, - { - "fieldname": "total", - "label": _("Total"), - "fieldtype": "Float", - "width": 300 - }] - data = get_data(filters) - return columns, data + columns = [ + {"fieldname": "salary_component", "label": _("Salary Component"), "fieldtype": "Data", "width": 300}, + {"fieldname": "total", "label": _("Total"), "fieldtype": "Float", "width": 300}, + ] + data = get_data(filters) + return columns, data def get_data(filters=None): - if not filters: - filters = {} - currency = None - data = [] - if filters.get('currency'): - currency = filters.get('currency') - company_currency = erpnext.get_company_currency(filters.get("company")) - salary_slips = get_salary_slips(filters, company_currency) - if not salary_slips: - return [] - - blank_line = {"salary_component": "", "total": ""} - total_employees = len(salary_slips) - #frappe.msgprint(str(total_employees)) - total_employee_record = { - "salary_component": "Total Employees", "total": total_employees} - data.append(total_employee_record) - data.append(blank_line) - - ss_basic_map = get_ss_basic_map(salary_slips, currency, company_currency) - data.extend(ss_basic_map) - t_basic = 0 - for basic in ss_basic_map: - t_basic = t_basic + basic["total"] - - ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) - #data.extend(ss_earning_map) - - #frappe.msgprint(str(ss_earning_map)) - total_earnings = 0 - for earning in ss_earning_map: - total_earnings = total_earnings + earning["total"] - te_record = {"salary_component": "Total Allowances", "total": total_earnings} - data.append(te_record) - - gross_pay = total_earnings + t_basic - gp_record = {"salary_component": "GROSS PAY", "total": gross_pay} - data.append(gp_record) - - ss_deduction_map = get_ss_ded_map(salary_slips, currency, company_currency) - #data.extend(ss_deduction_map) - - total_deduction = 0 - for deduction in ss_deduction_map: - total_deduction = total_deduction + deduction["total"] - - ded_record = {"salary_component": "Total Deductions", "total": total_deduction} - data.append(ded_record) - - netpay = gross_pay + total_deduction - np_record = {"salary_component": "NET PAY", "total": netpay} - data.append(np_record) - - # ss_ded_map = get_ss_ded_map(salary_slips, currency, company_currency) - # data.extend(ss_ded_map) - return data + if not filters: + filters = {} + currency = None + data = [] + if filters.get("currency"): + currency = filters.get("currency") + company_currency = erpnext.get_company_currency(filters.get("company")) + salary_slips = get_salary_slips(filters, company_currency) + if not salary_slips: + return [] + + blank_line = {"salary_component": "", "total": ""} + total_employees = len(salary_slips) + # frappe.msgprint(str(total_employees)) + total_employee_record = {"salary_component": "Total Employees", "total": total_employees} + data.append(total_employee_record) + data.append(blank_line) + + ss_basic_map = get_ss_basic_map(salary_slips, currency, company_currency) + data.extend(ss_basic_map) + t_basic = 0 + for basic in ss_basic_map: + t_basic = t_basic + basic["total"] + + ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) + # data.extend(ss_earning_map) + + # frappe.msgprint(str(ss_earning_map)) + total_earnings = 0 + for earning in ss_earning_map: + total_earnings = total_earnings + earning["total"] + te_record = {"salary_component": "Total Allowances", "total": total_earnings} + data.append(te_record) + + gross_pay = total_earnings + t_basic + gp_record = {"salary_component": "GROSS PAY", "total": gross_pay} + data.append(gp_record) + + ss_deduction_map = get_ss_ded_map(salary_slips, currency, company_currency) + # data.extend(ss_deduction_map) + + total_deduction = 0 + for deduction in ss_deduction_map: + total_deduction = total_deduction + deduction["total"] + + ded_record = {"salary_component": "Total Deductions", "total": total_deduction} + data.append(ded_record) + + netpay = gross_pay + total_deduction + np_record = {"salary_component": "NET PAY", "total": netpay} + data.append(np_record) + + # ss_ded_map = get_ss_ded_map(salary_slips, currency, company_currency) + # data.extend(ss_ded_map) + return data def get_salary_slips(filters, company_currency): - filters.update({"from_date": filters.get("from_date"), - "to_date": filters.get("to_date")}) - conditions, filters = get_conditions(filters, company_currency) - salary_slips = frappe.db.sql("""select * from `tabSalary Slip` where %s - order by employee""" % conditions, filters, as_dict=1) + filters.update({"from_date": filters.get("from_date"), "to_date": filters.get("to_date")}) + conditions, filters = get_conditions(filters, company_currency) + salary_slips = frappe.db.sql( + f"""select * from `tabSalary Slip` where {conditions} + order by employee""", + filters, + as_dict=1, + ) - return salary_slips or [] + return salary_slips or [] def get_ss_basic_map(salary_slips, currency, company_currency): - ss_basic = frappe.db.sql(""" - SELECT sd.salary_component, SUM(sd.amount) as total - FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name - AND sd.parent in (%s) - AND do_not_include_in_total = 0 - AND sd.parentfield = 'earnings' + ss_basic = frappe.db.sql( + """ + SELECT sd.salary_component, SUM(sd.amount) as total + FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name + AND sd.parent in ({}) + AND do_not_include_in_total = 0 + AND sd.parentfield = 'earnings' AND sd.salary_component = 'Basic' - GROUP BY sd.salary_component - ORDER BY sd.salary_component ASC""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) - - return ss_basic + GROUP BY sd.salary_component + ORDER BY sd.salary_component ASC""".format(", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) + return ss_basic def get_ss_earning_map(salary_slips, currency, company_currency): - ss_earnings = frappe.db.sql(""" - SELECT sd.salary_component, SUM(sd.amount) as total - FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name - AND sd.parent in (%s) - AND do_not_include_in_total = 0 - AND sd.parentfield = 'earnings' + ss_earnings = frappe.db.sql( + """ + SELECT sd.salary_component, SUM(sd.amount) as total + FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name + AND sd.parent in ({}) + AND do_not_include_in_total = 0 + AND sd.parentfield = 'earnings' AND sd.salary_component != 'Basic' - GROUP BY sd.salary_component - ORDER BY sd.salary_component ASC""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) + GROUP BY sd.salary_component + ORDER BY sd.salary_component ASC""".format(", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) - return ss_earnings + return ss_earnings def get_ss_ded_map(salary_slips, currency, company_currency): - ss_deductions = frappe.db.sql(""" - SELECT sd.salary_component, SUM(sd.amount) * -1 as total - FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name AND sd.parent in (%s) - AND do_not_include_in_total = 0 - AND sd.parentfield = 'deductions' - GROUP BY sd.salary_component - ORDER BY sd.salary_component ASC""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) - return ss_deductions + ss_deductions = frappe.db.sql( + """ + SELECT sd.salary_component, SUM(sd.amount) * -1 as total + FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name AND sd.parent in ({}) + AND do_not_include_in_total = 0 + AND sd.parentfield = 'deductions' + GROUP BY sd.salary_component + ORDER BY sd.salary_component ASC""".format(", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) + return ss_deductions def get_conditions(filters, company_currency): - conditions = "" - doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} - - if filters.get("docstatus"): - conditions += "docstatus = {0}".format( - doc_status[filters.get("docstatus")]) - - if filters.get("from_date"): - conditions += " and start_date >= %(from_date)s" - if filters.get("to_date"): - conditions += " and end_date <= %(to_date)s" - if filters.get("company"): - conditions += " and company = %(company)s" - if filters.get("employee"): - conditions += " and employee = %(employee)s" - if filters.get("currency") and filters.get("currency") != company_currency: - conditions += " and currency = %(currency)s" - if filters.get("department") and filters.get("company"): - department_list = get_departments( - filters.get("department"), filters.get("company")) - conditions += 'and department in (' + ','.join( - ("'"+n+"'" for n in department_list)) + ')' - - return conditions, filters - - -def get_departments(department,company): - departments_list = get_descendants_of("Department", department) - departments_list.append(department) - return departments_list + conditions = "" + doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} + + if filters.get("docstatus"): + conditions += "docstatus = {}".format(doc_status[filters.get("docstatus")]) + + if filters.get("from_date"): + conditions += " and start_date >= %(from_date)s" + if filters.get("to_date"): + conditions += " and end_date <= %(to_date)s" + if filters.get("company"): + conditions += " and company = %(company)s" + if filters.get("employee"): + conditions += " and employee = %(employee)s" + if filters.get("currency") and filters.get("currency") != company_currency: + conditions += " and currency = %(currency)s" + if filters.get("department") and filters.get("company"): + department_list = get_departments(filters.get("department"), filters.get("company")) + conditions += "and department in (" + ",".join("'" + n + "'" for n in department_list) + ")" + + return conditions, filters + + +def get_departments(department, company): + departments_list = get_descendants_of("Department", department) + departments_list.append(department) + return departments_list diff --git a/csf_tz/csf_tz/report/salary_register_summary_with_components/salary_register_summary_with_components.py b/csf_tz/csf_tz/report/salary_register_summary_with_components/salary_register_summary_with_components.py index b10347b3..dcef2d71 100644 --- a/csf_tz/csf_tz/report/salary_register_summary_with_components/salary_register_summary_with_components.py +++ b/csf_tz/csf_tz/report/salary_register_summary_with_components/salary_register_summary_with_components.py @@ -1,168 +1,160 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import erpnext -from frappe.utils import flt -from frappe import _, msgprint +import frappe +from frappe import _ from frappe.utils.nestedset import get_descendants_of def execute(filters=None): - columns = [{ - "fieldname": "salary_component", - "label": _("Salary Component"), - "fieldtype": "Data", - "width": 300 - }, - { - "fieldname": "total", - "label": _("Total"), - "fieldtype": "Float", - "width": 300, - "precision": 2 - }] - data = get_data(filters) - return columns, data + columns = [ + {"fieldname": "salary_component", "label": _("Salary Component"), "fieldtype": "Data", "width": 300}, + {"fieldname": "total", "label": _("Total"), "fieldtype": "Float", "width": 300, "precision": 2}, + ] + data = get_data(filters) + return columns, data def get_data(filters=None): - if not filters: - filters = {} - currency = None - data = [] - if filters.get('currency'): - currency = filters.get('currency') - company_currency = erpnext.get_company_currency(filters.get("company")) - salary_slips = get_salary_slips(filters, company_currency) - if not salary_slips: - return [] - - blank_line = {"salary_component": "", "total": ""} - total_employees = len(salary_slips) - #frappe.msgprint(str(total_employees)) - total_employee_record = {"salary_component": "Total Employees", "total": total_employees} - data.append(total_employee_record) - data.append(blank_line) - - ss_basic_map = get_ss_basic_map(salary_slips, currency, company_currency) - data.extend(ss_basic_map) - t_basic = 0 - for basic in ss_basic_map: - t_basic = t_basic + basic["total"] - - ss_earning_map = get_ss_earning_map( - salary_slips, currency, company_currency) - data.extend(ss_earning_map) - - #frappe.msgprint(str(ss_earning_map)) - total_earnings = 0 - for earning in ss_earning_map: - total_earnings = total_earnings + earning["total"] - te_record = {"salary_component": "Total Allowances", "total": total_earnings} - #data.append(te_record) - - gross_pay = total_earnings + t_basic - gp_record = {"salary_component": "GROSS PAY", "total": gross_pay} - data.append(gp_record) - - ss_deduction_map = get_ss_ded_map(salary_slips, currency, company_currency) - data.extend(ss_deduction_map) - - total_deduction = 0 - for deduction in ss_deduction_map: - total_deduction = total_deduction + deduction["total"] - - ded_record = {"salary_component": "Total Deductions", "total": total_deduction} - # data.append(ded_record) - - netpay = gross_pay + total_deduction - np_record = {"salary_component": "NET PAY BEFORE LOAN", "total": netpay} - data.append(np_record) - return data + if not filters: + filters = {} + currency = None + data = [] + if filters.get("currency"): + currency = filters.get("currency") + company_currency = erpnext.get_company_currency(filters.get("company")) + salary_slips = get_salary_slips(filters, company_currency) + if not salary_slips: + return [] + + blank_line = {"salary_component": "", "total": ""} + total_employees = len(salary_slips) + # frappe.msgprint(str(total_employees)) + total_employee_record = {"salary_component": "Total Employees", "total": total_employees} + data.append(total_employee_record) + data.append(blank_line) + + ss_basic_map = get_ss_basic_map(salary_slips, currency, company_currency) + data.extend(ss_basic_map) + t_basic = 0 + for basic in ss_basic_map: + t_basic = t_basic + basic["total"] + + ss_earning_map = get_ss_earning_map(salary_slips, currency, company_currency) + data.extend(ss_earning_map) + + # frappe.msgprint(str(ss_earning_map)) + total_earnings = 0 + for earning in ss_earning_map: + total_earnings = total_earnings + earning["total"] + + gross_pay = total_earnings + t_basic + gp_record = {"salary_component": "GROSS PAY", "total": gross_pay} + data.append(gp_record) + + ss_deduction_map = get_ss_ded_map(salary_slips, currency, company_currency) + data.extend(ss_deduction_map) + + total_deduction = 0 + for deduction in ss_deduction_map: + total_deduction = total_deduction + deduction["total"] + + netpay = gross_pay + total_deduction + np_record = {"salary_component": "NET PAY BEFORE LOAN", "total": netpay} + data.append(np_record) + return data def get_salary_slips(filters, company_currency): - filters.update({"from_date": filters.get("from_date"), - "to_date": filters.get("to_date")}) - conditions, filters = get_conditions(filters, company_currency) - salary_slips = frappe.db.sql("""select * from `tabSalary Slip` where %s - order by employee""" % conditions, filters, as_dict=1) + filters.update({"from_date": filters.get("from_date"), "to_date": filters.get("to_date")}) + conditions, filters = get_conditions(filters, company_currency) + salary_slips = frappe.db.sql( + f"""select * from `tabSalary Slip` where {conditions} + order by employee""", + filters, + as_dict=1, + ) - return salary_slips or [] + return salary_slips or [] def get_ss_basic_map(salary_slips, currency, company_currency): - ss_basic = frappe.db.sql(""" - SELECT sd.salary_component, SUM(sd.amount) as total - FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name - AND sd.parent in (%s) - AND do_not_include_in_total = 0 - AND sd.parentfield = 'earnings' + ss_basic = frappe.db.sql( + """ + SELECT sd.salary_component, SUM(sd.amount) as total + FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name + AND sd.parent in ({}) + AND do_not_include_in_total = 0 + AND sd.parentfield = 'earnings' AND sd.salary_component = 'Basic' - GROUP BY sd.salary_component - ORDER BY sd.salary_component ASC""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) + GROUP BY sd.salary_component + ORDER BY sd.salary_component ASC""".format(", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) - return ss_basic + return ss_basic def get_ss_earning_map(salary_slips, currency, company_currency): - ss_earnings = frappe.db.sql(""" - SELECT sd.salary_component, SUM(sd.amount) as total - FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name - AND sd.parent in (%s) - AND do_not_include_in_total = 0 - AND sd.parentfield = 'earnings' + ss_earnings = frappe.db.sql( + """ + SELECT sd.salary_component, SUM(sd.amount) as total + FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name + AND sd.parent in ({}) + AND do_not_include_in_total = 0 + AND sd.parentfield = 'earnings' AND sd.salary_component != 'Basic' - GROUP BY sd.salary_component - ORDER BY sd.salary_component ASC""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) + GROUP BY sd.salary_component + ORDER BY sd.salary_component ASC""".format(", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) - return ss_earnings + return ss_earnings def get_ss_ded_map(salary_slips, currency, company_currency): - ss_deductions = frappe.db.sql(""" - SELECT sd.salary_component, SUM(sd.amount) * -1 as total - FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name AND sd.parent in (%s) - AND do_not_include_in_total = 0 - AND sd.parentfield = 'deductions' - GROUP BY sd.salary_component - ORDER BY sd.salary_component ASC""" % - (', '.join(['%s']*len(salary_slips))), tuple([d.name for d in salary_slips]), as_dict=1) - return ss_deductions + ss_deductions = frappe.db.sql( + """ + SELECT sd.salary_component, SUM(sd.amount) * -1 as total + FROM `tabSalary Detail` sd, `tabSalary Slip` ss where sd.parent=ss.name AND sd.parent in ({}) + AND do_not_include_in_total = 0 + AND sd.parentfield = 'deductions' + GROUP BY sd.salary_component + ORDER BY sd.salary_component ASC""".format(", ".join(["%s"] * len(salary_slips))), + tuple([d.name for d in salary_slips]), + as_dict=1, + ) + return ss_deductions def get_conditions(filters, company_currency): - conditions = "" - doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} - - if filters.get("docstatus"): - conditions += "docstatus = {0}".format( - doc_status[filters.get("docstatus")]) - - if filters.get("from_date"): - conditions += " and start_date >= %(from_date)s" - if filters.get("to_date"): - conditions += " and end_date <= %(to_date)s" - if filters.get("company"): - conditions += " and company = %(company)s" - if filters.get("employee"): - conditions += " and employee = %(employee)s" - if filters.get("currency") and filters.get("currency") != company_currency: - conditions += " and currency = %(currency)s" - if filters.get("department") and filters.get("company"): - department_list = get_departments( - filters.get("department"), filters.get("company")) - conditions += 'and department in (' + ','.join( - ("'"+n+"'" for n in department_list)) + ')' - - return conditions, filters - - -def get_departments(department,company): - departments_list = get_descendants_of("Department", department) - departments_list.append(department) - return departments_list + conditions = "" + doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} + + if filters.get("docstatus"): + conditions += "docstatus = {}".format(doc_status[filters.get("docstatus")]) + + if filters.get("from_date"): + conditions += " and start_date >= %(from_date)s" + if filters.get("to_date"): + conditions += " and end_date <= %(to_date)s" + if filters.get("company"): + conditions += " and company = %(company)s" + if filters.get("employee"): + conditions += " and employee = %(employee)s" + if filters.get("currency") and filters.get("currency") != company_currency: + conditions += " and currency = %(currency)s" + if filters.get("department") and filters.get("company"): + department_list = get_departments(filters.get("department"), filters.get("company")) + conditions += "and department in (" + ",".join("'" + n + "'" for n in department_list) + ")" + + return conditions, filters + + +def get_departments(department, company): + departments_list = get_descendants_of("Department", department) + departments_list.append(department) + return departments_list diff --git a/csf_tz/csf_tz/report/salary_register_summary_with_monthly_comparison/salary_register_summary_with_monthly_comparison.py b/csf_tz/csf_tz/report/salary_register_summary_with_monthly_comparison/salary_register_summary_with_monthly_comparison.py index f9ff5b44..ca9c2891 100644 --- a/csf_tz/csf_tz/report/salary_register_summary_with_monthly_comparison/salary_register_summary_with_monthly_comparison.py +++ b/csf_tz/csf_tz/report/salary_register_summary_with_monthly_comparison/salary_register_summary_with_monthly_comparison.py @@ -1,930 +1,861 @@ -import frappe -import erpnext import calendar -from frappe.utils import flt, cstr, getdate + +import erpnext +import frappe from frappe import _, msgprint +from frappe.query_builder import DocType, Order +from frappe.query_builder import functions as fn +from frappe.utils import cstr, flt, getdate from frappe.utils.nestedset import get_descendants_of -from frappe.query_builder import DocType, functions as fn, Order ss = DocType("Salary Slip") sd = DocType("Salary Detail") def execute(filters=None): - company_currency = erpnext.get_company_currency(filters.get("company")) + company_currency = erpnext.get_company_currency(filters.get("company")) - prev_first_date, prev_last_date, prev_month, prev_year = get_prev_month_date( - filters - ) + prev_first_date, prev_last_date, prev_month, prev_year = get_prev_month_date(filters) - prev_salary_slips = get_prev_salary_slips( - filters, company_currency, prev_first_date, prev_last_date - ) - if len(prev_salary_slips) == 0: - msgprint( - _( - "No salary slip found for the previous month: {0} {1}".format( - frappe.bold(calendar.month_name[prev_month]), frappe.bold(prev_year) - ) - ) - ) - return [] + prev_salary_slips = get_prev_salary_slips(filters, company_currency, prev_first_date, prev_last_date) + if len(prev_salary_slips) == 0: + msgprint( + _( + f"No salary slip found for the previous month: {frappe.bold(calendar.month_name[prev_month])} {frappe.bold(prev_year)}" + ) + ) + return [] - columns = get_columns(filters, prev_month, prev_year) - data = get_data(filters, company_currency, prev_salary_slips) + columns = get_columns(filters, prev_month, prev_year) + data = get_data(filters, company_currency, prev_salary_slips) - return columns, data + return columns, data def get_columns(filters, prev_month, prev_year): - cur_month_name = calendar.month_name[getdate(filters.from_date).month] - cur_year = getdate(filters.from_date).year - - prev_month_name = calendar.month_name[prev_month] - - columns = [] - - if filters.get("based_on_department") == 1: - columns.append( - { - "fieldname": "department", - "label": _("Department"), - "fieldtype": "Data", - "width": 150, - } - ) - - if filters.get("based_on_cost_center") == 1: - columns.append( - { - "fieldname": "payroll_cost_center", - "label": _("Cost Center"), - "fieldtype": "Data", - "width": 150, - } - ) - - columns += [ - { - "fieldname": "salary_component", - "label": _("Salary Component"), - "fieldtype": "Data", - "width": 150, - }, - { - "fieldname": "total_prev_month", - "label": _("{0} {1}".format(prev_month_name, prev_year)), - "fieldtype": "Float", - "width": 150, - "precision": 2, - }, - { - "fieldname": "total_cur_month", - "label": _("{0} {1}".format(cur_month_name, cur_year)), - "fieldtype": "Float", - "width": 150, - "precision": 2, - }, - { - "fieldname": "difference_amount", - "label": _("Difference Amount"), - "fieldtype": "Data", - "width": 150, - }, - ] - return columns + cur_month_name = calendar.month_name[getdate(filters.from_date).month] + cur_year = getdate(filters.from_date).year + + prev_month_name = calendar.month_name[prev_month] + + columns = [] + + if filters.get("based_on_department") == 1: + columns.append( + { + "fieldname": "department", + "label": _("Department"), + "fieldtype": "Data", + "width": 150, + } + ) + + if filters.get("based_on_cost_center") == 1: + columns.append( + { + "fieldname": "payroll_cost_center", + "label": _("Cost Center"), + "fieldtype": "Data", + "width": 150, + } + ) + + columns += [ + { + "fieldname": "salary_component", + "label": _("Salary Component"), + "fieldtype": "Data", + "width": 150, + }, + { + "fieldname": "total_prev_month", + "label": _(f"{prev_month_name} {prev_year}"), + "fieldtype": "Float", + "width": 150, + "precision": 2, + }, + { + "fieldname": "total_cur_month", + "label": _(f"{cur_month_name} {cur_year}"), + "fieldtype": "Float", + "width": 150, + "precision": 2, + }, + { + "fieldname": "difference_amount", + "label": _("Difference Amount"), + "fieldtype": "Data", + "width": 150, + }, + ] + return columns def get_data(filters, company_currency, prev_salary_slips): - records = [] - currency = None - if filters.get("currency"): - currency = filters.get("currency") - - cur_salary_slips = get_cur_salary_slips(filters, company_currency) - if len(cur_salary_slips) == 0: - msgprint( - _( - "No salary slip found for the this month: {0} {1}".format( - frappe.bold(calendar.month_name[getdate(filters.from_date).month]), - frappe.bold(getdate(filters.from_date).year), - ) - ) - ) - return [] - - no_employee_diff = len(cur_salary_slips) - len(prev_salary_slips) - result = None - if no_employee_diff > 0: - result = "+" + cstr(no_employee_diff) - elif no_employee_diff < 0: - result = "-" + cstr(abs(no_employee_diff)) - else: - result = "0" - - records.append( - { - "salary_component": "TOTAL EMPLOYEES", - "total_prev_month": len(prev_salary_slips), - "total_cur_month": len(cur_salary_slips), - "difference_amount": result, - } - ) - records.append( - { - "salary_component": "", - "total_prev_month": "", - "total_cur_month": "", - "difference_amount": "", - } - ) - records.append( - { - "salary_component": "", - "total_prev_month": "", - "total_cur_month": "", - "difference_amount": "", - } - ) - - prev_ss_basic = get_prev_ss_basic_map(filters, prev_salary_slips) - prev_ss_earnings = get_prev_ss_earn_map(filters, prev_salary_slips) - prev_ss_deductions = get_prev_ss_ded_map(filters, prev_salary_slips) - - basic_data, total_prev_basic, total_cur_basic = get_basic_data( - filters, records, cur_salary_slips, prev_ss_basic - ) - basic_data.append( - { - "salary_component": "", - "total_prev_month": "", - "total_cur_month": "", - "difference_amount": "", - } - ) - basic_data.append( - { - "salary_component": "", - "total_prev_month": "", - "total_cur_month": "", - "difference_amount": "", - } - ) - earnings_data, total_prev_earning, total_cur_earning = get_earnings_data( - filters, basic_data, cur_salary_slips, prev_ss_earnings - ) - - cur_gross_pay = total_cur_earning + total_cur_basic - prev_gross_pay = total_prev_earning + total_prev_basic - - total_gross_amount_diff = flt(cur_gross_pay - prev_gross_pay, 2) - grs_diff = "" - if total_gross_amount_diff > 0: - grs_diff = "+" + cstr(total_gross_amount_diff) - elif total_gross_amount_diff < 0: - grs_diff = "-" + cstr(abs(total_gross_amount_diff)) - else: - grs_diff = "0" - - earnings_data.append( - { - "salary_component": "GROSS PAY", - "total_prev_month": prev_gross_pay, - "total_cur_month": cur_gross_pay, - "difference_amount": grs_diff, - } - ) - - earnings_data.append( - { - "salary_component": "", - "total_prev_month": "", - "total_cur_month": "", - "difference_amount": "", - } - ) - earnings_data.append( - { - "salary_component": "", - "total_prev_month": "", - "total_cur_month": "", - "difference_amount": "", - } - ) - - data = get_deduction_data( - filters, - earnings_data, - cur_salary_slips, - prev_ss_deductions, - cur_gross_pay, - prev_gross_pay, - ) - - return data + records = [] + cur_salary_slips = get_cur_salary_slips(filters, company_currency) + if len(cur_salary_slips) == 0: + msgprint( + _( + f"No salary slip found for the this month: {frappe.bold(calendar.month_name[getdate(filters.from_date).month])} {frappe.bold(getdate(filters.from_date).year)}" + ) + ) + return [] + + no_employee_diff = len(cur_salary_slips) - len(prev_salary_slips) + result = None + if no_employee_diff > 0: + result = "+" + cstr(no_employee_diff) + elif no_employee_diff < 0: + result = "-" + cstr(abs(no_employee_diff)) + else: + result = "0" + + records.append( + { + "salary_component": "TOTAL EMPLOYEES", + "total_prev_month": len(prev_salary_slips), + "total_cur_month": len(cur_salary_slips), + "difference_amount": result, + } + ) + records.append( + { + "salary_component": "", + "total_prev_month": "", + "total_cur_month": "", + "difference_amount": "", + } + ) + records.append( + { + "salary_component": "", + "total_prev_month": "", + "total_cur_month": "", + "difference_amount": "", + } + ) + + prev_ss_basic = get_prev_ss_basic_map(filters, prev_salary_slips) + prev_ss_earnings = get_prev_ss_earn_map(filters, prev_salary_slips) + prev_ss_deductions = get_prev_ss_ded_map(filters, prev_salary_slips) + + basic_data, total_prev_basic, total_cur_basic = get_basic_data( + filters, records, cur_salary_slips, prev_ss_basic + ) + basic_data.append( + { + "salary_component": "", + "total_prev_month": "", + "total_cur_month": "", + "difference_amount": "", + } + ) + basic_data.append( + { + "salary_component": "", + "total_prev_month": "", + "total_cur_month": "", + "difference_amount": "", + } + ) + earnings_data, total_prev_earning, total_cur_earning = get_earnings_data( + filters, basic_data, cur_salary_slips, prev_ss_earnings + ) + + cur_gross_pay = total_cur_earning + total_cur_basic + prev_gross_pay = total_prev_earning + total_prev_basic + + total_gross_amount_diff = flt(cur_gross_pay - prev_gross_pay, 2) + grs_diff = "" + if total_gross_amount_diff > 0: + grs_diff = "+" + cstr(total_gross_amount_diff) + elif total_gross_amount_diff < 0: + grs_diff = "-" + cstr(abs(total_gross_amount_diff)) + else: + grs_diff = "0" + + earnings_data.append( + { + "salary_component": "GROSS PAY", + "total_prev_month": prev_gross_pay, + "total_cur_month": cur_gross_pay, + "difference_amount": grs_diff, + } + ) + + earnings_data.append( + { + "salary_component": "", + "total_prev_month": "", + "total_cur_month": "", + "difference_amount": "", + } + ) + earnings_data.append( + { + "salary_component": "", + "total_prev_month": "", + "total_cur_month": "", + "difference_amount": "", + } + ) + + data = get_deduction_data( + filters, + earnings_data, + cur_salary_slips, + prev_ss_deductions, + cur_gross_pay, + prev_gross_pay, + ) + + return data def get_basic_data(filters, data, salary_slips, prev_ss_basic): - ss_basic_map = get_cur_ss_basic_map(filters, salary_slips) - - total_cur_basic = 0 - unique_cur_basic_salary_components = [] - unique_prev_basic_salary_components = [] - total_prev_basic = sum(flt(d.total_prev_month) for d in prev_ss_basic) - department_or_cost_center = "" - if filters.get("based_on_department") == 1: - department_or_cost_center = "department" - elif filters.get("based_on_cost_center") == 1: - department_or_cost_center = "payroll_cost_center" - - for cur_basic_row in ss_basic_map: - total_cur_basic += cur_basic_row["total_cur_month"] - - for prev_basic_row in prev_ss_basic: - if cur_basic_row.get(department_or_cost_center) == prev_basic_row.get( - department_or_cost_center - ) and cur_basic_row.get("salary_component") == prev_basic_row.get( - "salary_component" - ): - bsc_amount_diff = flt( - cur_basic_row.get("total_cur_month") - - prev_basic_row.get("total_prev_month"), - 2, - ) - result = "" - if bsc_amount_diff > 0: - result = "+" + cstr(bsc_amount_diff) - elif bsc_amount_diff < 0: - result = "-" + cstr(abs(bsc_amount_diff)) - else: - result = "0" - cur_basic_row.update( - { - "total_prev_month": prev_basic_row.get("total_prev_month"), - "difference_amount": result, - } - ) - data.append(cur_basic_row) - - unique_cur_basic_salary_components.append( - { - department_or_cost_center: cur_basic_row.get( - department_or_cost_center - ), - "salary_component": cur_basic_row.get("salary_component"), - } - ) - unique_prev_basic_salary_components.append( - { - department_or_cost_center: prev_basic_row.get( - department_or_cost_center - ), - "salary_component": prev_basic_row.get("salary_component"), - } - ) - - cur_row = { - department_or_cost_center: cur_basic_row.get(department_or_cost_center), - "salary_component": cur_basic_row.get("salary_component"), - } - if cur_row not in unique_cur_basic_salary_components: - unique_cur_basic_salary_components.append(cur_row) - - data.append( - { - department_or_cost_center: cur_basic_row.get( - department_or_cost_center - ), - "salary_component": cur_basic_row.get("salary_component"), - "total_prev_month": 0, - "total_cur_month": cur_basic_row.get("total_cur_month"), - "difference_amount": "+" - + cstr(cur_basic_row.get("total_cur_month")), - } - ) - - for row in prev_ss_basic: - prev_row = { - department_or_cost_center: row.get(department_or_cost_center), - "salary_component": row.get("salary_component"), - } - if prev_row not in unique_prev_basic_salary_components: - unique_prev_basic_salary_components.append(prev_row) - data.append( - { - department_or_cost_center: row.get(department_or_cost_center), - "salary_component": row.get("salary_component"), - "total_prev_month": row.get("total_prev_month") or 0, - "total_cur_month": 0, - "difference_amount": "-" + cstr(row.get("total_prev_month")), - } - ) - - total_bsc_amount_diff = flt(total_cur_basic - total_prev_basic, 2) - d = "" - if total_bsc_amount_diff > 0: - d = "+" + cstr(total_bsc_amount_diff) - elif total_bsc_amount_diff < 0: - d = "-" + cstr(abs(total_bsc_amount_diff)) - else: - d = "0" - - data.append( - { - department_or_cost_center: "", - "salary_component": "Total Basic", - "total_prev_month": total_prev_basic, - "total_cur_month": total_cur_basic, - "difference_amount": d, - } - ) - - return data, total_prev_basic, total_cur_basic + ss_basic_map = get_cur_ss_basic_map(filters, salary_slips) + + total_cur_basic = 0 + unique_cur_basic_salary_components = [] + unique_prev_basic_salary_components = [] + total_prev_basic = sum(flt(d.total_prev_month) for d in prev_ss_basic) + department_or_cost_center = "" + if filters.get("based_on_department") == 1: + department_or_cost_center = "department" + elif filters.get("based_on_cost_center") == 1: + department_or_cost_center = "payroll_cost_center" + + for cur_basic_row in ss_basic_map: + total_cur_basic += cur_basic_row["total_cur_month"] + + for prev_basic_row in prev_ss_basic: + if cur_basic_row.get(department_or_cost_center) == prev_basic_row.get( + department_or_cost_center + ) and cur_basic_row.get("salary_component") == prev_basic_row.get("salary_component"): + bsc_amount_diff = flt( + cur_basic_row.get("total_cur_month") - prev_basic_row.get("total_prev_month"), + 2, + ) + result = "" + if bsc_amount_diff > 0: + result = "+" + cstr(bsc_amount_diff) + elif bsc_amount_diff < 0: + result = "-" + cstr(abs(bsc_amount_diff)) + else: + result = "0" + cur_basic_row.update( + { + "total_prev_month": prev_basic_row.get("total_prev_month"), + "difference_amount": result, + } + ) + data.append(cur_basic_row) + + unique_cur_basic_salary_components.append( + { + department_or_cost_center: cur_basic_row.get(department_or_cost_center), + "salary_component": cur_basic_row.get("salary_component"), + } + ) + unique_prev_basic_salary_components.append( + { + department_or_cost_center: prev_basic_row.get(department_or_cost_center), + "salary_component": prev_basic_row.get("salary_component"), + } + ) + + cur_row = { + department_or_cost_center: cur_basic_row.get(department_or_cost_center), + "salary_component": cur_basic_row.get("salary_component"), + } + if cur_row not in unique_cur_basic_salary_components: + unique_cur_basic_salary_components.append(cur_row) + + data.append( + { + department_or_cost_center: cur_basic_row.get(department_or_cost_center), + "salary_component": cur_basic_row.get("salary_component"), + "total_prev_month": 0, + "total_cur_month": cur_basic_row.get("total_cur_month"), + "difference_amount": "+" + cstr(cur_basic_row.get("total_cur_month")), + } + ) + + for row in prev_ss_basic: + prev_row = { + department_or_cost_center: row.get(department_or_cost_center), + "salary_component": row.get("salary_component"), + } + if prev_row not in unique_prev_basic_salary_components: + unique_prev_basic_salary_components.append(prev_row) + data.append( + { + department_or_cost_center: row.get(department_or_cost_center), + "salary_component": row.get("salary_component"), + "total_prev_month": row.get("total_prev_month") or 0, + "total_cur_month": 0, + "difference_amount": "-" + cstr(row.get("total_prev_month")), + } + ) + + total_bsc_amount_diff = flt(total_cur_basic - total_prev_basic, 2) + d = "" + if total_bsc_amount_diff > 0: + d = "+" + cstr(total_bsc_amount_diff) + elif total_bsc_amount_diff < 0: + d = "-" + cstr(abs(total_bsc_amount_diff)) + else: + d = "0" + + data.append( + { + department_or_cost_center: "", + "salary_component": "Total Basic", + "total_prev_month": total_prev_basic, + "total_cur_month": total_cur_basic, + "difference_amount": d, + } + ) + + return data, total_prev_basic, total_cur_basic def get_earnings_data(filters, data, cur_salary_slips, prev_ss_earnings): - ss_earning_map = get_cur_ss_earning_map(filters, cur_salary_slips) - - total_cur_earning = 0 - unique_cur_earnings_salary_components = [] - unique_prev_earnings_salary_components = [] - total_prev_earning = sum(flt(d.total_prev_month) for d in prev_ss_earnings) - department_or_cost_center = "" - if filters.get("based_on_department") == 1: - department_or_cost_center = "department" - elif filters.get("based_on_cost_center") == 1: - department_or_cost_center = "payroll_cost_center" - - for cur_earning_row in ss_earning_map: - total_cur_earning += cur_earning_row["total_cur_month"] - - for prev_earning_row in prev_ss_earnings: - if cur_earning_row.get(department_or_cost_center) == prev_earning_row.get( - department_or_cost_center - ) and cur_earning_row.get("salary_component") == prev_earning_row.get( - "salary_component" - ): - earn_amount_diff = flt( - cur_earning_row.get("total_cur_month") - - prev_earning_row.get("total_prev_month"), - 2, - ) - result = "" - if earn_amount_diff > 0: - result = "+" + cstr(earn_amount_diff) - elif earn_amount_diff < 0: - result = "-" + cstr(abs(earn_amount_diff)) - else: - result = "0" - - cur_earning_row.update( - { - "total_prev_month": prev_earning_row.get("total_prev_month"), - "difference_amount": result, - } - ) - data.append(cur_earning_row) - - unique_cur_earnings_salary_components.append( - { - department_or_cost_center: cur_earning_row.get( - department_or_cost_center - ), - "salary_component": cur_earning_row.get("salary_component"), - } - ) - unique_prev_earnings_salary_components.append( - { - department_or_cost_center: prev_earning_row.get( - department_or_cost_center - ), - "salary_component": prev_earning_row.get("salary_component"), - } - ) - - cur_row = { - department_or_cost_center: cur_earning_row.get(department_or_cost_center), - "salary_component": cur_earning_row.get("salary_component"), - } - if cur_row not in unique_cur_earnings_salary_components: - unique_cur_earnings_salary_components.append(cur_row) - - data.append( - { - department_or_cost_center: cur_earning_row.get( - department_or_cost_center - ), - "salary_component": cur_earning_row.get("salary_component"), - "total_prev_month": 0, - "total_cur_month": cur_earning_row.get("total_cur_month"), - "difference_amount": "+" - + cstr(cur_earning_row.get("total_cur_month")), - } - ) - - for row in prev_ss_earnings: - prev_row = { - department_or_cost_center: row.get(department_or_cost_center), - "salary_component": row.get("salary_component"), - } - if prev_row not in unique_prev_earnings_salary_components: - unique_prev_earnings_salary_components.append(prev_row) - data.append( - { - department_or_cost_center: row.get(department_or_cost_center), - "salary_component": row.get("salary_component"), - "total_prev_month": row.get("total_prev_month") or 0, - "total_cur_month": 0, - "difference_amount": "-" + cstr(row.get("total_prev_month")), - } - ) - total_earn_amount_diff = flt(total_cur_earning - total_prev_earning, 2) - d = "" - if total_earn_amount_diff > 0: - d = "+" + cstr(total_earn_amount_diff) - elif total_earn_amount_diff < 0: - d = "-" + cstr(abs(total_earn_amount_diff)) - else: - d = "0" - - data.append( - { - department_or_cost_center: "", - "salary_component": "TOTAL ALLOWANCES", - "total_prev_month": total_prev_earning, - "total_cur_month": total_cur_earning, - "difference_amount": d, - } - ) - - return data, total_prev_earning, total_cur_earning + ss_earning_map = get_cur_ss_earning_map(filters, cur_salary_slips) + + total_cur_earning = 0 + unique_cur_earnings_salary_components = [] + unique_prev_earnings_salary_components = [] + total_prev_earning = sum(flt(d.total_prev_month) for d in prev_ss_earnings) + department_or_cost_center = "" + if filters.get("based_on_department") == 1: + department_or_cost_center = "department" + elif filters.get("based_on_cost_center") == 1: + department_or_cost_center = "payroll_cost_center" + + for cur_earning_row in ss_earning_map: + total_cur_earning += cur_earning_row["total_cur_month"] + + for prev_earning_row in prev_ss_earnings: + if cur_earning_row.get(department_or_cost_center) == prev_earning_row.get( + department_or_cost_center + ) and cur_earning_row.get("salary_component") == prev_earning_row.get("salary_component"): + earn_amount_diff = flt( + cur_earning_row.get("total_cur_month") - prev_earning_row.get("total_prev_month"), + 2, + ) + result = "" + if earn_amount_diff > 0: + result = "+" + cstr(earn_amount_diff) + elif earn_amount_diff < 0: + result = "-" + cstr(abs(earn_amount_diff)) + else: + result = "0" + + cur_earning_row.update( + { + "total_prev_month": prev_earning_row.get("total_prev_month"), + "difference_amount": result, + } + ) + data.append(cur_earning_row) + + unique_cur_earnings_salary_components.append( + { + department_or_cost_center: cur_earning_row.get(department_or_cost_center), + "salary_component": cur_earning_row.get("salary_component"), + } + ) + unique_prev_earnings_salary_components.append( + { + department_or_cost_center: prev_earning_row.get(department_or_cost_center), + "salary_component": prev_earning_row.get("salary_component"), + } + ) + + cur_row = { + department_or_cost_center: cur_earning_row.get(department_or_cost_center), + "salary_component": cur_earning_row.get("salary_component"), + } + if cur_row not in unique_cur_earnings_salary_components: + unique_cur_earnings_salary_components.append(cur_row) + + data.append( + { + department_or_cost_center: cur_earning_row.get(department_or_cost_center), + "salary_component": cur_earning_row.get("salary_component"), + "total_prev_month": 0, + "total_cur_month": cur_earning_row.get("total_cur_month"), + "difference_amount": "+" + cstr(cur_earning_row.get("total_cur_month")), + } + ) + + for row in prev_ss_earnings: + prev_row = { + department_or_cost_center: row.get(department_or_cost_center), + "salary_component": row.get("salary_component"), + } + if prev_row not in unique_prev_earnings_salary_components: + unique_prev_earnings_salary_components.append(prev_row) + data.append( + { + department_or_cost_center: row.get(department_or_cost_center), + "salary_component": row.get("salary_component"), + "total_prev_month": row.get("total_prev_month") or 0, + "total_cur_month": 0, + "difference_amount": "-" + cstr(row.get("total_prev_month")), + } + ) + total_earn_amount_diff = flt(total_cur_earning - total_prev_earning, 2) + d = "" + if total_earn_amount_diff > 0: + d = "+" + cstr(total_earn_amount_diff) + elif total_earn_amount_diff < 0: + d = "-" + cstr(abs(total_earn_amount_diff)) + else: + d = "0" + + data.append( + { + department_or_cost_center: "", + "salary_component": "TOTAL ALLOWANCES", + "total_prev_month": total_prev_earning, + "total_cur_month": total_cur_earning, + "difference_amount": d, + } + ) + + return data, total_prev_earning, total_cur_earning def get_deduction_data( - filters, - data, - salary_slips, - prev_ss_deductions, - cur_gross_pay, - prev_gross_pay, + filters, + data, + salary_slips, + prev_ss_deductions, + cur_gross_pay, + prev_gross_pay, ): - ss_deduction_map = get_cur_ss_ded_map(filters, salary_slips) - - total_cur_deduction = 0 - unique_cur_deduction_salary_components = [] - unique_prev_deduction_salary_components = [] - total_prev_deduction = sum(flt(d.total_prev_month) for d in prev_ss_deductions) - department_or_cost_center = "" - if filters.get("based_on_department") == 1: - department_or_cost_center = "department" - elif filters.get("based_on_cost_center") == 1: - department_or_cost_center = "payroll_cost_center" - - for cur_deduction_row in ss_deduction_map: - total_cur_deduction += cur_deduction_row["total_cur_month"] - - for prev_deduction_row in prev_ss_deductions: - if cur_deduction_row.get("salary_component") == prev_deduction_row.get( - "salary_component" - ) and cur_deduction_row.get( - department_or_cost_center - ) == prev_deduction_row.get( - department_or_cost_center - ): - ded_amount_diff = flt( - cur_deduction_row.get("total_cur_month") - - prev_deduction_row.get("total_prev_month"), - 2, - ) - result = "" - if ded_amount_diff > 0: - result = "+" + cstr(ded_amount_diff) - elif ded_amount_diff < 0: - result = "-" + cstr(abs(ded_amount_diff)) - else: - result = "0" - - cur_deduction_row.update( - { - "total_prev_month": prev_deduction_row.get("total_prev_month"), - "difference_amount": result, - } - ) - data.append(cur_deduction_row) - - unique_cur_deduction_salary_components.append( - { - department_or_cost_center: cur_deduction_row.get( - department_or_cost_center - ), - "salary_component": cur_deduction_row.get("salary_component"), - } - ) - - unique_prev_deduction_salary_components.append( - { - department_or_cost_center: prev_deduction_row.get( - department_or_cost_center - ), - "salary_component": prev_deduction_row.get("salary_component"), - } - ) - - cur_row = { - "salary_component": cur_deduction_row.get("salary_component"), - department_or_cost_center: cur_deduction_row.get(department_or_cost_center), - } - if cur_row not in unique_cur_deduction_salary_components: - unique_cur_deduction_salary_components.append(cur_row) - - data.append( - { - department_or_cost_center: cur_deduction_row.get( - department_or_cost_center - ), - "salary_component": cur_deduction_row.get("salary_component"), - "total_prev_month": 0, - "total_cur_month": cur_deduction_row.get("total_cur_month"), - "difference_amount": "+" - + cstr(cur_deduction_row.get("total_cur_month")), - } - ) - - for row in prev_ss_deductions: - prev_row = { - department_or_cost_center: row.get(department_or_cost_center), - "salary_component": row.get("salary_component"), - } - if prev_row not in unique_prev_deduction_salary_components: - unique_prev_deduction_salary_components.append(prev_row) - data.append( - { - department_or_cost_center: row.get(department_or_cost_center), - "salary_component": row.get("salary_component"), - "total_prev_month": row.get("total_prev_month"), - "total_cur_month": 0, - "difference_amount": "-" + cstr(row.get("total_prev_month")), - } - ) - total_ded_amount_diff = flt(total_cur_deduction - total_prev_deduction, 2) - d = "" - if total_ded_amount_diff > 0: - d = "+" + cstr(total_ded_amount_diff) - elif total_ded_amount_diff < 0: - d = "-" + cstr(abs(total_ded_amount_diff)) - else: - d = "0" - - data.append( - { - department_or_cost_center: "", - "salary_component": "TOTAL DEDUCTIONS", - "total_prev_month": total_prev_deduction, - "total_cur_month": total_cur_deduction, - "difference_amount": d, - } - ) - - net_pay_amount_diff = flt( - (cur_gross_pay - total_cur_deduction) - (prev_gross_pay - total_prev_deduction), - 2, - ) - h = "" - if net_pay_amount_diff > 0: - h = "+" + cstr(net_pay_amount_diff) - elif net_pay_amount_diff < 0: - h = "-" + cstr(abs(net_pay_amount_diff)) - else: - h = "0" - - data.append( - { - department_or_cost_center: "", - "salary_component": "NET PAY BEFORE LOAN", - "total_prev_month": prev_gross_pay - total_prev_deduction, - "total_cur_month": cur_gross_pay - total_cur_deduction, - "difference_amount": h, - } - ) - - return data + ss_deduction_map = get_cur_ss_ded_map(filters, salary_slips) + + total_cur_deduction = 0 + unique_cur_deduction_salary_components = [] + unique_prev_deduction_salary_components = [] + total_prev_deduction = sum(flt(d.total_prev_month) for d in prev_ss_deductions) + department_or_cost_center = "" + if filters.get("based_on_department") == 1: + department_or_cost_center = "department" + elif filters.get("based_on_cost_center") == 1: + department_or_cost_center = "payroll_cost_center" + + for cur_deduction_row in ss_deduction_map: + total_cur_deduction += cur_deduction_row["total_cur_month"] + + for prev_deduction_row in prev_ss_deductions: + if cur_deduction_row.get("salary_component") == prev_deduction_row.get( + "salary_component" + ) and cur_deduction_row.get(department_or_cost_center) == prev_deduction_row.get( + department_or_cost_center + ): + ded_amount_diff = flt( + cur_deduction_row.get("total_cur_month") - prev_deduction_row.get("total_prev_month"), + 2, + ) + result = "" + if ded_amount_diff > 0: + result = "+" + cstr(ded_amount_diff) + elif ded_amount_diff < 0: + result = "-" + cstr(abs(ded_amount_diff)) + else: + result = "0" + + cur_deduction_row.update( + { + "total_prev_month": prev_deduction_row.get("total_prev_month"), + "difference_amount": result, + } + ) + data.append(cur_deduction_row) + + unique_cur_deduction_salary_components.append( + { + department_or_cost_center: cur_deduction_row.get(department_or_cost_center), + "salary_component": cur_deduction_row.get("salary_component"), + } + ) + + unique_prev_deduction_salary_components.append( + { + department_or_cost_center: prev_deduction_row.get(department_or_cost_center), + "salary_component": prev_deduction_row.get("salary_component"), + } + ) + + cur_row = { + "salary_component": cur_deduction_row.get("salary_component"), + department_or_cost_center: cur_deduction_row.get(department_or_cost_center), + } + if cur_row not in unique_cur_deduction_salary_components: + unique_cur_deduction_salary_components.append(cur_row) + + data.append( + { + department_or_cost_center: cur_deduction_row.get(department_or_cost_center), + "salary_component": cur_deduction_row.get("salary_component"), + "total_prev_month": 0, + "total_cur_month": cur_deduction_row.get("total_cur_month"), + "difference_amount": "+" + cstr(cur_deduction_row.get("total_cur_month")), + } + ) + + for row in prev_ss_deductions: + prev_row = { + department_or_cost_center: row.get(department_or_cost_center), + "salary_component": row.get("salary_component"), + } + if prev_row not in unique_prev_deduction_salary_components: + unique_prev_deduction_salary_components.append(prev_row) + data.append( + { + department_or_cost_center: row.get(department_or_cost_center), + "salary_component": row.get("salary_component"), + "total_prev_month": row.get("total_prev_month"), + "total_cur_month": 0, + "difference_amount": "-" + cstr(row.get("total_prev_month")), + } + ) + total_ded_amount_diff = flt(total_cur_deduction - total_prev_deduction, 2) + d = "" + if total_ded_amount_diff > 0: + d = "+" + cstr(total_ded_amount_diff) + elif total_ded_amount_diff < 0: + d = "-" + cstr(abs(total_ded_amount_diff)) + else: + d = "0" + + data.append( + { + department_or_cost_center: "", + "salary_component": "TOTAL DEDUCTIONS", + "total_prev_month": total_prev_deduction, + "total_cur_month": total_cur_deduction, + "difference_amount": d, + } + ) + + net_pay_amount_diff = flt( + (cur_gross_pay - total_cur_deduction) - (prev_gross_pay - total_prev_deduction), + 2, + ) + h = "" + if net_pay_amount_diff > 0: + h = "+" + cstr(net_pay_amount_diff) + elif net_pay_amount_diff < 0: + h = "-" + cstr(abs(net_pay_amount_diff)) + else: + h = "0" + + data.append( + { + department_or_cost_center: "", + "salary_component": "NET PAY BEFORE LOAN", + "total_prev_month": prev_gross_pay - total_prev_deduction, + "total_cur_month": cur_gross_pay - total_cur_deduction, + "difference_amount": h, + } + ) + + return data def get_prev_month_date(filters): - prev_month = getdate(filters.from_date).month - 1 - prev_year = getdate(filters.from_date).year + prev_month = getdate(filters.from_date).month - 1 + prev_year = getdate(filters.from_date).year - if prev_month == 0: - prev_month = 12 - prev_year = prev_year - 1 + if prev_month == 0: + prev_month = 12 + prev_year = prev_year - 1 - prev_first_date = getdate(str(prev_year) + "-" + str(prev_month) + "-" + "01") - prev_last_date = getdate( - str(prev_year) - + "-" - + str(prev_month) - + "-" - + "{0}".format(calendar.monthrange(prev_year, prev_month)[1]) - ) + prev_first_date = getdate(str(prev_year) + "-" + str(prev_month) + "-" + "01") + prev_last_date = getdate( + str(prev_year) + "-" + str(prev_month) + "-" + f"{calendar.monthrange(prev_year, prev_month)[1]}" + ) - return prev_first_date, prev_last_date, prev_month, prev_year + return prev_first_date, prev_last_date, prev_month, prev_year def get_prev_ss_basic_map(filters, prev_salary_slips): - prev_ss_basic_query = ( - frappe.qb.from_(sd) - .inner_join(ss) - .on(sd.parent == ss.name) - .select(sd.salary_component, fn.Sum(sd.amount).as_("total_prev_month")) - .where( - (sd.parent.isin([d.name for d in prev_salary_slips])) - & (sd.do_not_include_in_total == 0) - & (sd.parentfield == "earnings") - & (sd.salary_component.like("Basic")) - ) - .groupby(sd.salary_component) - .orderby(sd.salary_component, Order.asc) - ) - if filters.get("based_on_department") == 1: - prev_ss_basic_query = prev_ss_basic_query.select(ss.department) - prev_ss_basic_query = prev_ss_basic_query.groupby(ss.department) - if filters.get("department"): - prev_ss_basic_query = prev_ss_basic_query.where( - ss.department == filters.get("department") - ) - - if filters.get("based_on_cost_center") == 1: - prev_ss_basic_query = prev_ss_basic_query.select(ss.payroll_cost_center) - prev_ss_basic_query = prev_ss_basic_query.groupby(ss.payroll_cost_center) - if filters.get("cost_center"): - prev_ss_basic_query = prev_ss_basic_query.where( - ss.payroll_cost_center == filters.get("cost_center") - ) - prev_ss_basic_query = prev_ss_basic_query.groupby(sd.salary_component) - prev_ss_basic_data = prev_ss_basic_query.run(as_dict=1) - - return prev_ss_basic_data or [] + prev_ss_basic_query = ( + frappe.qb.from_(sd) + .inner_join(ss) + .on(sd.parent == ss.name) + .select(sd.salary_component, fn.Sum(sd.amount).as_("total_prev_month")) + .where( + (sd.parent.isin([d.name for d in prev_salary_slips])) + & (sd.do_not_include_in_total == 0) + & (sd.parentfield == "earnings") + & (sd.salary_component.like("Basic")) + ) + .groupby(sd.salary_component) + .orderby(sd.salary_component, order=Order.asc) + ) + if filters.get("based_on_department") == 1: + prev_ss_basic_query = prev_ss_basic_query.select(ss.department) + prev_ss_basic_query = prev_ss_basic_query.groupby(ss.department) + if filters.get("department"): + prev_ss_basic_query = prev_ss_basic_query.where(ss.department == filters.get("department")) + + if filters.get("based_on_cost_center") == 1: + prev_ss_basic_query = prev_ss_basic_query.select(ss.payroll_cost_center) + prev_ss_basic_query = prev_ss_basic_query.groupby(ss.payroll_cost_center) + if filters.get("cost_center"): + prev_ss_basic_query = prev_ss_basic_query.where( + ss.payroll_cost_center == filters.get("cost_center") + ) + prev_ss_basic_query = prev_ss_basic_query.groupby(sd.salary_component) + prev_ss_basic_data = prev_ss_basic_query.run(as_dict=1) + + return prev_ss_basic_data or [] def get_prev_ss_earn_map(filters, prev_salary_slips): - prev_ss_earnings_query = ( - frappe.qb.from_(sd) - .inner_join(ss) - .on(sd.parent == ss.name) - .select(sd.salary_component, fn.Sum(sd.amount).as_("total_prev_month")) - .where( - (sd.parent.isin([d.name for d in prev_salary_slips])) - & (sd.do_not_include_in_total == 0) - & (sd.parentfield == "earnings") - & (sd.salary_component.not_like("Basic")) - ) - .groupby(sd.salary_component) - .orderby(sd.salary_component, Order.asc) - ) - if filters.get("based_on_department") == 1: - prev_ss_earnings_query = prev_ss_earnings_query.select(ss.department) - prev_ss_earnings_query = prev_ss_earnings_query.groupby(ss.department) - if filters.get("department"): - prev_ss_earnings_query = prev_ss_earnings_query.where( - ss.department == filters.get("department") - ) - - if filters.get("based_on_cost_center") == 1: - prev_ss_earnings_query = prev_ss_earnings_query.select(ss.payroll_cost_center) - prev_ss_earnings_query = prev_ss_earnings_query.groupby(ss.payroll_cost_center) - if filters.get("cost_center"): - prev_ss_earnings_query = prev_ss_earnings_query.where( - ss.payroll_cost_center == filters.get("cost_center") - ) - prev_ss_earnings_query = prev_ss_earnings_query.groupby(sd.salary_component) - prev_ss_earnings_data = prev_ss_earnings_query.run(as_dict=1) - - return prev_ss_earnings_data or [] + prev_ss_earnings_query = ( + frappe.qb.from_(sd) + .inner_join(ss) + .on(sd.parent == ss.name) + .select(sd.salary_component, fn.Sum(sd.amount).as_("total_prev_month")) + .where( + (sd.parent.isin([d.name for d in prev_salary_slips])) + & (sd.do_not_include_in_total == 0) + & (sd.parentfield == "earnings") + & (sd.salary_component.not_like("Basic")) + ) + .groupby(sd.salary_component) + .orderby(sd.salary_component, order=Order.asc) + ) + if filters.get("based_on_department") == 1: + prev_ss_earnings_query = prev_ss_earnings_query.select(ss.department) + prev_ss_earnings_query = prev_ss_earnings_query.groupby(ss.department) + if filters.get("department"): + prev_ss_earnings_query = prev_ss_earnings_query.where(ss.department == filters.get("department")) + + if filters.get("based_on_cost_center") == 1: + prev_ss_earnings_query = prev_ss_earnings_query.select(ss.payroll_cost_center) + prev_ss_earnings_query = prev_ss_earnings_query.groupby(ss.payroll_cost_center) + if filters.get("cost_center"): + prev_ss_earnings_query = prev_ss_earnings_query.where( + ss.payroll_cost_center == filters.get("cost_center") + ) + prev_ss_earnings_query = prev_ss_earnings_query.groupby(sd.salary_component) + prev_ss_earnings_data = prev_ss_earnings_query.run(as_dict=1) + + return prev_ss_earnings_data or [] def get_prev_ss_ded_map(filters, prev_salary_slips): - prev_ss_deductions_query = ( - frappe.qb.from_(sd) - .inner_join(ss) - .on(sd.parent == ss.name) - .select(sd.salary_component, fn.Sum(sd.amount).as_("total_prev_month")) - .where( - (sd.parent.isin([d.name for d in prev_salary_slips])) - & (sd.do_not_include_in_total == 0) - & (sd.parentfield == "deductions") - ) - .groupby(sd.salary_component) - .orderby(sd.salary_component, Order.asc) - ) - if filters.get("based_on_department") == 1: - prev_ss_deductions_query = prev_ss_deductions_query.select(ss.department) - prev_ss_deductions_query = prev_ss_deductions_query.groupby(ss.department) - if filters.get("department"): - prev_ss_deductions_query = prev_ss_deductions_query.where( - ss.department == filters.get("department") - ) - - if filters.get("based_on_cost_center") == 1: - prev_ss_deductions_query = prev_ss_deductions_query.select( - ss.payroll_cost_center - ) - prev_ss_deductions_query = prev_ss_deductions_query.groupby( - ss.payroll_cost_center - ) - if filters.get("cost_center"): - prev_ss_deductions_query = prev_ss_deductions_query.where( - ss.payroll_cost_center == filters.get("cost_center") - ) - prev_ss_deductions_query = prev_ss_deductions_query.groupby(sd.salary_component) - prev_ss_deductions_data = prev_ss_deductions_query.run(as_dict=1) - - return prev_ss_deductions_data or [] + prev_ss_deductions_query = ( + frappe.qb.from_(sd) + .inner_join(ss) + .on(sd.parent == ss.name) + .select(sd.salary_component, fn.Sum(sd.amount).as_("total_prev_month")) + .where( + (sd.parent.isin([d.name for d in prev_salary_slips])) + & (sd.do_not_include_in_total == 0) + & (sd.parentfield == "deductions") + ) + .groupby(sd.salary_component) + .orderby(sd.salary_component, order=Order.asc) + ) + if filters.get("based_on_department") == 1: + prev_ss_deductions_query = prev_ss_deductions_query.select(ss.department) + prev_ss_deductions_query = prev_ss_deductions_query.groupby(ss.department) + if filters.get("department"): + prev_ss_deductions_query = prev_ss_deductions_query.where( + ss.department == filters.get("department") + ) + + if filters.get("based_on_cost_center") == 1: + prev_ss_deductions_query = prev_ss_deductions_query.select(ss.payroll_cost_center) + prev_ss_deductions_query = prev_ss_deductions_query.groupby(ss.payroll_cost_center) + if filters.get("cost_center"): + prev_ss_deductions_query = prev_ss_deductions_query.where( + ss.payroll_cost_center == filters.get("cost_center") + ) + prev_ss_deductions_query = prev_ss_deductions_query.groupby(sd.salary_component) + prev_ss_deductions_data = prev_ss_deductions_query.run(as_dict=1) + + return prev_ss_deductions_data or [] def get_prev_salary_slips(filters, company_currency, prev_first_date, prev_last_date): - prev_ss_query = ( - frappe.qb.from_(ss) - .select(ss.name) - .where( - (ss.docstatus == 1) - & (ss.start_date >= prev_first_date) - & (ss.end_date <= prev_last_date) - & (ss.company == filters.get("company")) - ) - ) - if filters.get("employee"): - prev_ss_query = prev_ss_query.where(ss.employee == filters.get("employee")) - - if filters.get("currency") and filters.get("currency") != company_currency: - prev_ss_query = prev_ss_query.where(ss.currency == filters.get("currency")) - - if filters.get("department") and filters.get("company"): - department_list = get_departments( - filters.get("department"), filters.get("company") - ) - prev_ss_query = prev_ss_query.where(ss.department.isin(department_list)) - - if filters.get("cost_center") and filters.get("company"): - cost_center_list = get_cost_costs( - filters.get("cost_center"), filters.get("company") - ) - prev_ss_query = prev_ss_query.where( - ss.payroll_cost_center.isin(cost_center_list) - ) - - prev_salary_slips = prev_ss_query.run(as_dict=1) - - return prev_salary_slips or [] + prev_ss_query = ( + frappe.qb.from_(ss) + .select(ss.name) + .where( + (ss.docstatus == 1) + & (ss.start_date >= prev_first_date) + & (ss.end_date <= prev_last_date) + & (ss.company == filters.get("company")) + ) + ) + if filters.get("employee"): + prev_ss_query = prev_ss_query.where(ss.employee == filters.get("employee")) + + if filters.get("currency") and filters.get("currency") != company_currency: + prev_ss_query = prev_ss_query.where(ss.currency == filters.get("currency")) + + if filters.get("department") and filters.get("company"): + department_list = get_departments(filters.get("department"), filters.get("company")) + prev_ss_query = prev_ss_query.where(ss.department.isin(department_list)) + + if filters.get("cost_center") and filters.get("company"): + cost_center_list = get_cost_costs(filters.get("cost_center"), filters.get("company")) + prev_ss_query = prev_ss_query.where(ss.payroll_cost_center.isin(cost_center_list)) + + prev_salary_slips = prev_ss_query.run(as_dict=1) + + return prev_salary_slips or [] def get_cur_salary_slips(filters, company_currency): - doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} - cur_ss_query = ( - frappe.qb.from_(ss) - .select(ss.name) - .where( - (ss.docstatus == doc_status.get(filters.get("docstatus"))) - & (ss.start_date >= filters.get("from_date")) - & (ss.end_date <= filters.get("to_date")) - & (ss.company == filters.get("company")) - ) - ) - if filters.get("employee"): - cur_ss_query = cur_ss_query.where(ss.employee == filters.get("employee")) - - if filters.get("currency") and filters.get("currency") != company_currency: - cur_ss_query = cur_ss_query.where(ss.currency == filters.get("currency")) - - if filters.get("department") and filters.get("company"): - department_list = get_departments( - filters.get("department"), filters.get("company") - ) - cur_ss_query = cur_ss_query.where(ss.department.isin(department_list)) - - if filters.get("cost_center") and filters.get("company"): - cost_center_list = get_cost_costs( - filters.get("cost_center"), filters.get("company") - ) - cur_ss_query = cur_ss_query.where(ss.payroll_cost_center.isin(cost_center_list)) - - cur_salary_slips = cur_ss_query.run(as_dict=1) - - return cur_salary_slips or [] + doc_status = {"Draft": 0, "Submitted": 1, "Cancelled": 2} + cur_ss_query = ( + frappe.qb.from_(ss) + .select(ss.name) + .where( + (ss.docstatus == doc_status.get(filters.get("docstatus"))) + & (ss.start_date >= filters.get("from_date")) + & (ss.end_date <= filters.get("to_date")) + & (ss.company == filters.get("company")) + ) + ) + if filters.get("employee"): + cur_ss_query = cur_ss_query.where(ss.employee == filters.get("employee")) + + if filters.get("currency") and filters.get("currency") != company_currency: + cur_ss_query = cur_ss_query.where(ss.currency == filters.get("currency")) + + if filters.get("department") and filters.get("company"): + department_list = get_departments(filters.get("department"), filters.get("company")) + cur_ss_query = cur_ss_query.where(ss.department.isin(department_list)) + + if filters.get("cost_center") and filters.get("company"): + cost_center_list = get_cost_costs(filters.get("cost_center"), filters.get("company")) + cur_ss_query = cur_ss_query.where(ss.payroll_cost_center.isin(cost_center_list)) + + cur_salary_slips = cur_ss_query.run(as_dict=1) + + return cur_salary_slips or [] def get_cur_ss_basic_map(filters, cur_salary_slips): - cur_ss_basic_query = ( - frappe.qb.from_(sd) - .inner_join(ss) - .on(sd.parent == ss.name) - .select(sd.salary_component, fn.Sum(sd.amount).as_("total_cur_month")) - .where( - (sd.parent.isin([d.name for d in cur_salary_slips])) - & (sd.do_not_include_in_total == 0) - & (sd.parentfield == "earnings") - & (sd.salary_component.like("Basic")) - ) - .groupby(sd.salary_component) - .orderby(sd.salary_component, Order.asc) - ) - if filters.get("based_on_department") == 1: - cur_ss_basic_query = cur_ss_basic_query.select(ss.department) - cur_ss_basic_query = cur_ss_basic_query.groupby(ss.department) - if filters.get("department"): - cur_ss_basic_query = cur_ss_basic_query.where( - ss.department == filters.get("department") - ) - - if filters.get("based_on_cost_center") == 1: - cur_ss_basic_query = cur_ss_basic_query.select(ss.payroll_cost_center) - cur_ss_basic_query = cur_ss_basic_query.groupby(ss.payroll_cost_center) - if filters.get("cost_center"): - cur_ss_basic_query = cur_ss_basic_query.where( - ss.payroll_cost_center == filters.get("cost_center") - ) - cur_ss_basic_query = cur_ss_basic_query.groupby(sd.salary_component) - cur_ss_basic_data = cur_ss_basic_query.run(as_dict=1) - - return cur_ss_basic_data or [] + cur_ss_basic_query = ( + frappe.qb.from_(sd) + .inner_join(ss) + .on(sd.parent == ss.name) + .select(sd.salary_component, fn.Sum(sd.amount).as_("total_cur_month")) + .where( + (sd.parent.isin([d.name for d in cur_salary_slips])) + & (sd.do_not_include_in_total == 0) + & (sd.parentfield == "earnings") + & (sd.salary_component.like("Basic")) + ) + .groupby(sd.salary_component) + .orderby(sd.salary_component, order=Order.asc) + ) + if filters.get("based_on_department") == 1: + cur_ss_basic_query = cur_ss_basic_query.select(ss.department) + cur_ss_basic_query = cur_ss_basic_query.groupby(ss.department) + if filters.get("department"): + cur_ss_basic_query = cur_ss_basic_query.where(ss.department == filters.get("department")) + + if filters.get("based_on_cost_center") == 1: + cur_ss_basic_query = cur_ss_basic_query.select(ss.payroll_cost_center) + cur_ss_basic_query = cur_ss_basic_query.groupby(ss.payroll_cost_center) + if filters.get("cost_center"): + cur_ss_basic_query = cur_ss_basic_query.where( + ss.payroll_cost_center == filters.get("cost_center") + ) + cur_ss_basic_query = cur_ss_basic_query.groupby(sd.salary_component) + cur_ss_basic_data = cur_ss_basic_query.run(as_dict=1) + + return cur_ss_basic_data or [] def get_cur_ss_earning_map(filters, cur_salary_slips): - cur_ss_earnings_query = ( - frappe.qb.from_(sd) - .inner_join(ss) - .on(sd.parent == ss.name) - .select(sd.salary_component, fn.Sum(sd.amount).as_("total_cur_month")) - .where( - (sd.parent.isin([d.name for d in cur_salary_slips])) - & (sd.do_not_include_in_total == 0) - & (sd.parentfield == "earnings") - & (sd.salary_component.not_like("Basic")) - ) - .groupby(sd.salary_component) - .orderby(sd.salary_component, Order.asc) - ) - if filters.get("based_on_department") == 1: - cur_ss_earnings_query = cur_ss_earnings_query.select(ss.department) - cur_ss_earnings_query = cur_ss_earnings_query.groupby(ss.department) - if filters.get("department"): - cur_ss_earnings_query = cur_ss_earnings_query.where( - ss.department == filters.get("department") - ) - - if filters.get("based_on_cost_center") == 1: - cur_ss_earnings_query = cur_ss_earnings_query.select(ss.payroll_cost_center) - cur_ss_earnings_query = cur_ss_earnings_query.groupby(ss.payroll_cost_center) - if filters.get("cost_center"): - cur_ss_earnings_query = cur_ss_earnings_query.where( - ss.payroll_cost_center == filters.get("cost_center") - ) - - cur_ss_earnings_query = cur_ss_earnings_query.groupby(sd.salary_component) - cur_ss_earnings_data = cur_ss_earnings_query.run(as_dict=1) - - return cur_ss_earnings_data or [] + cur_ss_earnings_query = ( + frappe.qb.from_(sd) + .inner_join(ss) + .on(sd.parent == ss.name) + .select(sd.salary_component, fn.Sum(sd.amount).as_("total_cur_month")) + .where( + (sd.parent.isin([d.name for d in cur_salary_slips])) + & (sd.do_not_include_in_total == 0) + & (sd.parentfield == "earnings") + & (sd.salary_component.not_like("Basic")) + ) + .groupby(sd.salary_component) + .orderby(sd.salary_component, order=Order.asc) + ) + if filters.get("based_on_department") == 1: + cur_ss_earnings_query = cur_ss_earnings_query.select(ss.department) + cur_ss_earnings_query = cur_ss_earnings_query.groupby(ss.department) + if filters.get("department"): + cur_ss_earnings_query = cur_ss_earnings_query.where(ss.department == filters.get("department")) + + if filters.get("based_on_cost_center") == 1: + cur_ss_earnings_query = cur_ss_earnings_query.select(ss.payroll_cost_center) + cur_ss_earnings_query = cur_ss_earnings_query.groupby(ss.payroll_cost_center) + if filters.get("cost_center"): + cur_ss_earnings_query = cur_ss_earnings_query.where( + ss.payroll_cost_center == filters.get("cost_center") + ) + + cur_ss_earnings_query = cur_ss_earnings_query.groupby(sd.salary_component) + cur_ss_earnings_data = cur_ss_earnings_query.run(as_dict=1) + + return cur_ss_earnings_data or [] def get_cur_ss_ded_map(filters, cur_salary_slips): - cur_ss_deduction_query = ( - frappe.qb.from_(sd) - .inner_join(ss) - .on(sd.parent == ss.name) - .select(sd.salary_component, fn.Sum(sd.amount).as_("total_cur_month")) - .where( - (sd.parent.isin([d.name for d in cur_salary_slips])) - & (sd.do_not_include_in_total == 0) - & (sd.parentfield == "deductions") - ) - ) - if filters.get("based_on_department") == 1: - cur_ss_deduction_query = cur_ss_deduction_query.select(ss.department) - cur_ss_deduction_query = cur_ss_deduction_query.groupby(ss.department) - if filters.get("department"): - cur_ss_deduction_query = cur_ss_deduction_query.where( - ss.department == filters.get("department") - ) - - if filters.get("based_on_cost_center") == 1: - cur_ss_deduction_query = cur_ss_deduction_query.select(ss.payroll_cost_center) - cur_ss_deduction_query = cur_ss_deduction_query.groupby(ss.payroll_cost_center) - if filters.get("cost_center"): - cur_ss_deduction_query = cur_ss_deduction_query.where( - ss.payroll_cost_center == filters.get("cost_center") - ) - cur_ss_deduction_query = cur_ss_deduction_query.groupby(sd.salary_component) - cur_ss_deductions_data = cur_ss_deduction_query.run(as_dict=1) - - return cur_ss_deductions_data or [] + cur_ss_deduction_query = ( + frappe.qb.from_(sd) + .inner_join(ss) + .on(sd.parent == ss.name) + .select(sd.salary_component, fn.Sum(sd.amount).as_("total_cur_month")) + .where( + (sd.parent.isin([d.name for d in cur_salary_slips])) + & (sd.do_not_include_in_total == 0) + & (sd.parentfield == "deductions") + ) + ) + if filters.get("based_on_department") == 1: + cur_ss_deduction_query = cur_ss_deduction_query.select(ss.department) + cur_ss_deduction_query = cur_ss_deduction_query.groupby(ss.department) + if filters.get("department"): + cur_ss_deduction_query = cur_ss_deduction_query.where(ss.department == filters.get("department")) + + if filters.get("based_on_cost_center") == 1: + cur_ss_deduction_query = cur_ss_deduction_query.select(ss.payroll_cost_center) + cur_ss_deduction_query = cur_ss_deduction_query.groupby(ss.payroll_cost_center) + if filters.get("cost_center"): + cur_ss_deduction_query = cur_ss_deduction_query.where( + ss.payroll_cost_center == filters.get("cost_center") + ) + cur_ss_deduction_query = cur_ss_deduction_query.groupby(sd.salary_component) + cur_ss_deductions_data = cur_ss_deduction_query.run(as_dict=1) + + return cur_ss_deductions_data or [] def get_departments(department, company): - departments_list = get_descendants_of("Department", department) - departments_list.append(department) - return departments_list + departments_list = get_descendants_of("Department", department) + departments_list.append(department) + return departments_list def get_cost_costs(cost_center, company): - cost_center_list = get_descendants_of("Cost Center", cost_center) - cost_center_list.append(cost_center) - return cost_center_list + cost_center_list = get_descendants_of("Cost Center", cost_center) + cost_center_list.append(cost_center) + return cost_center_list diff --git a/csf_tz/csf_tz/report/stock_balance_pivot_warehouse/stock_balance_pivot_warehouse.py b/csf_tz/csf_tz/report/stock_balance_pivot_warehouse/stock_balance_pivot_warehouse.py index fee6c9ba..c3616118 100644 --- a/csf_tz/csf_tz/report/stock_balance_pivot_warehouse/stock_balance_pivot_warehouse.py +++ b/csf_tz/csf_tz/report/stock_balance_pivot_warehouse/stock_balance_pivot_warehouse.py @@ -1,87 +1,115 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe -from frappe.utils import flt from frappe import _ +from frappe.utils import flt + def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} stock_ledger_entry = get_stock_ledger_entries(filters) - if not stock_ledger_entry: return [], [] + if not stock_ledger_entry: + return [], [] columns, warehouses = get_columns(stock_ledger_entry) sle_warehouse_map = get_sle_warehouse_map(stock_ledger_entry) - data = [] for sle in stock_ledger_entry: row = [sle.item_code, sle.item_name, sle.brand, sle.item_group] for e in warehouses: row.append(sle_warehouse_map.get(sle.item_code, {}).get(e)) - #row += [total_qty] + # row += [total_qty] data.append(row) return columns, data + def get_columns(stock_ledger_entry): columns = [ - _("Item Code") + ":Link/Item:150", _("Item") + ":Data/Item Name:150", _("Brand") + ":Link/Brand:150", _("Item Group") + ":Link/Item Group:150" + _("Item Code") + ":Link/Item:150", + _("Item") + ":Data/Item Name:150", + _("Brand") + ":Link/Brand:150", + _("Item Group") + ":Link/Item Group:150", ] warehouses = {_("Warehouse"): []} - for warehouse in frappe.db.sql("""select distinct sle.warehouse + for warehouse in frappe.db.sql( + """select distinct sle.warehouse from `tabStock Ledger Entry` sle - where sle.actual_qty != 0 and sle.item_code in (%s)""" % - (', '.join(['%s']*len(stock_ledger_entry))), tuple([d.item_code for d in stock_ledger_entry]), as_dict=1): + where sle.actual_qty != 0 and sle.item_code in ({})""".format( + ", ".join(["%s"] * len(stock_ledger_entry)) + ), + tuple([d.item_code for d in stock_ledger_entry]), + as_dict=1, + ): warehouses[_("Warehouse")].append(warehouse.warehouse) - columns = columns + [(e + ":Float:120") for e in warehouses[_("Warehouse")]] + \ - [_("Total Stock") + ":Float:120"] - - #frappe.msgprint(warehouses[_("Warehouse")]) + columns = ( + columns + [(e + ":Float:120") for e in warehouses[_("Warehouse")]] + [_("Total Stock") + ":Float:120"] + ) + + # frappe.msgprint(warehouses[_("Warehouse")]) return columns, warehouses[_("Warehouse")] + def get_stock_ledger_entries(filters): - filters.update({"from_date": filters.get("from_date"), "to_date":filters.get("to_date")}) + filters.update({"from_date": filters.get("from_date"), "to_date": filters.get("to_date")}) conditions, filters = get_conditions(filters) - stock_ledger_entry = frappe.db.sql("""select sle.item_code, i.item_name, i.brand, i.item_group, sle.warehouse, sum(sle.actual_qty) + stock_ledger_entry = frappe.db.sql( + f"""select sle.item_code, i.item_name, i.brand, i.item_group, sle.warehouse, sum(sle.actual_qty) from `tabStock Ledger Entry` sle inner join `tabItem` i on sle.item_code = i.item_code - where %s + where {conditions} group by i.item_name, i.brand, i.item_group, sle.warehouse - order by i.brand, i.item_group, sle.item_code""" % conditions, filters, as_dict=1) + order by i.brand, i.item_group, sle.item_code""", + filters, + as_dict=1, + ) return stock_ledger_entry or [] + def get_conditions(filters): conditions = "" - if filters.get("from_date"): conditions += "posting_date >= %(from_date)s" - if filters.get("to_date"): conditions += " and posting_date <= %(to_date)s" - if filters.get("item_group"): conditions += " and i.item_group = %(item_group)s" - if filters.get("brand"): conditions += " and i.brand = %(brand)s" - if filters.get("warehouse"): conditions += " and sle.warehouse = %(warehouse)s" + if filters.get("from_date"): + conditions += "posting_date >= %(from_date)s" + if filters.get("to_date"): + conditions += " and posting_date <= %(to_date)s" + if filters.get("item_group"): + conditions += " and i.item_group = %(item_group)s" + if filters.get("brand"): + conditions += " and i.brand = %(brand)s" + if filters.get("warehouse"): + conditions += " and sle.warehouse = %(warehouse)s" return conditions, filters + def get_sle_warehouse_map(stock_ledger_entry): - sle_warehouses = frappe.db.sql("""select sle.item_code, sle.warehouse, sum(actual_qty) + sle_warehouses = frappe.db.sql( + """select sle.item_code, sle.warehouse, sum(actual_qty) from `tabStock Ledger Entry` sle inner join `tabItem` i on sle.item_code = i.item_code - where item_code in (%s) - group by i.item_name, i.brand, i.item_group, sle.warehouse""" % - (', '.join(['%s']*len(stock_ledger_entry))), tuple([d.item_code for d in stock_ledger_entry]), as_dict=1) + where item_code in ({}) + group by i.item_name, i.brand, i.item_group, sle.warehouse""".format( + ", ".join(["%s"] * len(stock_ledger_entry)) + ), + tuple([d.item_code for d in stock_ledger_entry]), + as_dict=1, + ) sle_warehouse_map = {} for d in sle_warehouses: sle_warehouse_map.setdefault(d.item_code, frappe._dict()).setdefault(d.warehouse, []) - #frappe.msgprint(d) + # frappe.msgprint(d) sle_warehouse_map[d.item_code][d.warehouse] = flt(d.actual_qty) return sle_warehouse_map diff --git a/csf_tz/csf_tz/report/stock_balance_pro/stock_balance_pro.py b/csf_tz/csf_tz/report/stock_balance_pro/stock_balance_pro.py index 4cbc3f64..171c1d8b 100644 --- a/csf_tz/csf_tz/report/stock_balance_pro/stock_balance_pro.py +++ b/csf_tz/csf_tz/report/stock_balance_pro/stock_balance_pro.py @@ -1,139 +1,175 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import erpnext -from frappe import _ -from frappe.utils import flt, cint, getdate -from erpnext.stock.utils import add_additional_uom_columns +import frappe from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition - - +from erpnext.stock.utils import add_additional_uom_columns +from frappe import _ +from frappe.utils import cint, flt, getdate from six import iteritems def execute(filters=None): - if not filters: - filters = {} + if not filters: + filters = {} - validate_filters(filters) + validate_filters(filters) - if filters.get("company"): - company_currency = erpnext.get_company_currency(filters.get("company")) - else: - company_currency = frappe.db.get_single_value( - "Global Defaults", "default_currency") + if filters.get("company"): + company_currency = erpnext.get_company_currency(filters.get("company")) + else: + company_currency = frappe.db.get_single_value("Global Defaults", "default_currency") - include_uom = filters.get("include_uom") - columns = get_columns(filters) - items = get_items(filters) - sle = get_stock_ledger_entries(filters, items) + include_uom = filters.get("include_uom") + columns = get_columns(filters) + items = get_items(filters) + sle = get_stock_ledger_entries(filters, items) - # if no stock ledger entry found return - if not sle: - return columns, [] + # if no stock ledger entry found return + if not sle: + return columns, [] - iwb_map = get_item_warehouse_map(filters, sle) - item_map = get_item_details(items, sle, filters) + iwb_map = get_item_warehouse_map(filters, sle) + item_map = get_item_details(items, sle, filters) - data = [] - conversion_factors = {} + data = [] + conversion_factors = {} - def _func(x): return x[1] + def _func(x): + return x[1] - for (company, item) in sorted(iwb_map): - if item_map.get(item): - qty_dict = iwb_map[(company, item)] + for company, item in sorted(iwb_map): + if item_map.get(item): + qty_dict = iwb_map[(company, item)] - report_data = { - 'currency': company_currency, - 'item_code': item, - 'company': company, - } - report_data.update(item_map[item]) - report_data.update(qty_dict) + report_data = { + "currency": company_currency, + "item_code": item, + "company": company, + } + report_data.update(item_map[item]) + report_data.update(qty_dict) - if include_uom: - conversion_factors.setdefault( - item, item_map[item].conversion_factor) + if include_uom: + conversion_factors.setdefault(item, item_map[item].conversion_factor) - data.append(report_data) + data.append(report_data) - add_additional_uom_columns(columns, data, include_uom, conversion_factors) - return columns, data + add_additional_uom_columns(columns, data, include_uom, conversion_factors) + return columns, data def get_columns(filters): - """return columns""" - columns = [ - {"label": _("Item"), "fieldname": "item_code", - "fieldtype": "Link", "options": "Item", "width": 100}, - {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, - {"label": _("Item Group"), "fieldname": "item_group", - "fieldtype": "Link", "options": "Item Group", "width": 100}, - {"label": _("Stock UOM"), "fieldname": "stock_uom", - "fieldtype": "Link", "options": "UOM", "width": 90}, - {"label": _("Balance Qty"), "fieldname": "bal_qty", - "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Opening Qty"), "fieldname": "opening_qty", - "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("In Qty"), "fieldname": "in_qty", - "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Out Qty"), "fieldname": "out_qty", - "fieldtype": "Float", "width": 80, "convertible": "qty"}, - {"label": _("Excise Qty"), "fieldname": "excise_stock", - "fieldtype": "Float", "width": 100, "convertible": "qty"}, - {"label": _("Company"), "fieldname": "company", - "fieldtype": "Link", "options": "Company", "width": 100} - ] - - if filters.get('show_variant_attributes'): - columns += [{'label': att_name, 'fieldname': att_name, 'width': 100} - for att_name in get_variants_attributes()] - - return columns + """return columns""" + columns = [ + {"label": _("Item"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", "width": 100}, + {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("Balance Qty"), + "fieldname": "bal_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Opening Qty"), + "fieldname": "opening_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Excise Qty"), + "fieldname": "excise_stock", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, + ] + + if filters.get("show_variant_attributes"): + columns += [ + {"label": att_name, "fieldname": att_name, "width": 100} for att_name in get_variants_attributes() + ] + + return columns def get_conditions(filters): - conditions = "" - if not filters.get("from_date"): - frappe.throw(_("'From Date' is required")) + conditions = "" + if not filters.get("from_date"): + frappe.throw(_("'From Date' is required")) - if filters.get("to_date"): - conditions += " and sle.posting_date <= %s" % frappe.db.escape( - filters.get("to_date")) - else: - frappe.throw(_("'To Date' is required")) + if filters.get("to_date"): + conditions += " and sle.posting_date <= {}".format(frappe.db.escape(filters.get("to_date"))) + else: + frappe.throw(_("'To Date' is required")) - if filters.get("company"): - conditions += " and sle.company = %s" % frappe.db.escape( - filters.get("company")) + if filters.get("company"): + conditions += " and sle.company = {}".format(frappe.db.escape(filters.get("company"))) - if filters.get("warehouse"): - warehouse_details = frappe.db.get_value("Warehouse", - filters.get("warehouse"), ["lft", "rgt"], as_dict=1) - if warehouse_details: - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and sle.warehouse = wh.name)" % (warehouse_details.lft, - warehouse_details.rgt) + if filters.get("warehouse"): + warehouse_details = frappe.db.get_value( + "Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1 + ) + if warehouse_details: + conditions += f" and exists (select name from `tabWarehouse` wh \ + where wh.lft >= {warehouse_details.lft} and wh.rgt <= {warehouse_details.rgt} and sle.warehouse = wh.name)" - if filters.get("warehouse_type") and not filters.get("warehouse"): - conditions += " and exists (select name from `tabWarehouse` wh \ - where wh.warehouse_type = '%s' and sle.warehouse = wh.name)" % (filters.get("warehouse_type")) + if filters.get("warehouse_type") and not filters.get("warehouse"): + conditions += " and exists (select name from `tabWarehouse` wh \ + where wh.warehouse_type = '{}' and sle.warehouse = wh.name)".format(filters.get("warehouse_type")) - return conditions + return conditions def get_stock_ledger_entries(filters, items): - item_conditions_sql = '' - if items: - item_conditions_sql = ' and sle.item_code in ({})'\ - .format(', '.join([frappe.db.escape(i, percent=False) for i in items])) + item_conditions_sql = "" + if items: + item_conditions_sql = " and sle.item_code in ({})".format( + ", ".join([frappe.db.escape(i, percent=False) for i in items]) + ) - conditions = get_conditions(filters) + conditions = get_conditions(filters) - return frappe.db.sql(""" + return frappe.db.sql( + f""" select sle.item_code, sle.warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, sle.company, sle.voucher_type, sle.qty_after_transaction, sle.stock_value_difference, @@ -145,7 +181,7 @@ def get_stock_ledger_entries(filters, items): where sle.is_cancelled = 0 and (se.purpose != "Material Transfer") and i.excisable_item = 1 - and sle.docstatus < 2 %s %s + and sle.docstatus < 2 {item_conditions_sql} {conditions} UNION ALL select sle.item_code, sle.warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, @@ -157,7 +193,7 @@ def get_stock_ledger_entries(filters, items): inner join `tabItem` i on sle.item_code = i.name where sle.is_cancelled = 0 and i.excisable_item = 1 - and sle.docstatus < 2 %s %s + and sle.docstatus < 2 {item_conditions_sql} {conditions} UNION ALL select sle.item_code, sle.warehouse, sle.posting_date, sle.actual_qty, sle.valuation_rate, @@ -169,142 +205,144 @@ def get_stock_ledger_entries(filters, items): where sle.is_cancelled = 0 and sle.voucher_type NOT IN ("Stock Entry", "Sales Invoice") and i.excisable_item = 1 - and sle.docstatus < 2 %s %s""" % # nosec - (item_conditions_sql, conditions, item_conditions_sql, conditions, item_conditions_sql, conditions), as_dict=1) + and sle.docstatus < 2 {item_conditions_sql} {conditions}""", + as_dict=1, + ) def get_item_warehouse_map(filters, sle): - iwb_map = {} - from_date = getdate(filters.get("from_date")) - to_date = getdate(filters.get("to_date")) + iwb_map = {} + from_date = getdate(filters.get("from_date")) + to_date = getdate(filters.get("to_date")) - float_precision = cint(frappe.db.get_default("float_precision")) or 3 + float_precision = cint(frappe.db.get_default("float_precision")) or 3 - for d in sle: - key = (d.company, d.item_code) - if key not in iwb_map: - iwb_map[key] = frappe._dict({ - "opening_qty": 0.0, - "in_qty": 0.0, - "out_qty": 0.0, - "excise_stock": 0.0, - "bal_qty": 0.0 - }) + for d in sle: + key = (d.company, d.item_code) + if key not in iwb_map: + iwb_map[key] = frappe._dict( + {"opening_qty": 0.0, "in_qty": 0.0, "out_qty": 0.0, "excise_stock": 0.0, "bal_qty": 0.0} + ) - qty_dict = iwb_map[(d.company, d.item_code)] + qty_dict = iwb_map[(d.company, d.item_code)] - if d.voucher_type == "Stock Reconciliation": - qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty) - else: - qty_diff = flt(d.actual_qty) + if d.voucher_type == "Stock Reconciliation": + qty_diff = flt(d.qty_after_transaction) - flt(qty_dict.bal_qty) + else: + qty_diff = flt(d.actual_qty) - if d.posting_date < from_date: - qty_dict.opening_qty += qty_diff + if d.posting_date < from_date: + qty_dict.opening_qty += qty_diff - elif d.posting_date >= from_date and d.posting_date <= to_date: - if flt(qty_diff, float_precision) >= 0: - qty_dict.in_qty += qty_diff - else: - qty_dict.out_qty += abs(qty_diff) - qty_dict.excise_stock += abs(d.excise_stock) or 0 + elif d.posting_date >= from_date and d.posting_date <= to_date: + if flt(qty_diff, float_precision) >= 0: + qty_dict.in_qty += qty_diff + else: + qty_dict.out_qty += abs(qty_diff) + qty_dict.excise_stock += abs(d.excise_stock) or 0 - qty_dict.bal_qty += qty_diff + qty_dict.bal_qty += qty_diff - iwb_map = filter_items_with_no_transactions(iwb_map, float_precision) + iwb_map = filter_items_with_no_transactions(iwb_map, float_precision) - return iwb_map + return iwb_map def filter_items_with_no_transactions(iwb_map, float_precision): - for (company, item) in sorted(iwb_map): - qty_dict = iwb_map[(company, item)] + for company, item in sorted(iwb_map): + qty_dict = iwb_map[(company, item)] - no_transactions = True - for key, val in iteritems(qty_dict): - val = flt(val, float_precision) - qty_dict[key] = val - if key != "val_rate" and val: - no_transactions = False + no_transactions = True + for key, val in iteritems(qty_dict): + val = flt(val, float_precision) + qty_dict[key] = val + if key != "val_rate" and val: + no_transactions = False - if no_transactions: - iwb_map.pop((company, item)) + if no_transactions: + iwb_map.pop((company, item)) - return iwb_map + return iwb_map def get_items(filters): - conditions = [] - if filters.get("item_code"): - conditions.append("item.name=%(item_code)s") - else: - if filters.get("item_group"): - conditions.append(get_item_group_condition( - filters.get("item_group"))) + conditions = [] + if filters.get("item_code"): + conditions.append("item.name=%(item_code)s") + else: + if filters.get("item_group"): + conditions.append(get_item_group_condition(filters.get("item_group"))) - items = [] - if conditions: - items = frappe.db.sql_list("""select name from `tabItem` item where {}""" - .format(" and ".join(conditions)), filters) - return items + items = [] + if conditions: + items = frappe.db.sql_list( + """select name from `tabItem` item where {}""".format(" and ".join(conditions)), filters + ) + return items def get_item_details(items, sle, filters): - item_details = {} - if not items: - items = list(set([d.item_code for d in sle])) - - if not items: - return item_details - - cf_field = cf_join = "" - if filters.get("include_uom"): - cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ - % frappe.db.escape(filters.get("include_uom")) - - res = frappe.db.sql(""" + item_details = {} + if not items: + items = list(set([d.item_code for d in sle])) + + if not items: + return item_details + + cf_field = cf_join = "" + if filters.get("include_uom"): + cf_field = ", ucd.conversion_factor" + cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom={}".format( + frappe.db.escape(filters.get("include_uom")) + ) + + res = frappe.db.sql( + """ select - item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom %s + item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom {} from `tabItem` item - %s + {} where - item.name in (%s) - """ % (cf_field, cf_join, ','.join(['%s'] * len(items))), items, as_dict=1) + item.name in ({}) + """.format(cf_field, cf_join, ",".join(["%s"] * len(items))), + items, + as_dict=1, + ) - for item in res: - item_details.setdefault(item.name, item) + for item in res: + item_details.setdefault(item.name, item) - if filters.get('show_variant_attributes', 0) == 1: - variant_values = get_variant_values_for(list(item_details)) - item_details = {k: v.update(variant_values.get(k, {})) - for k, v in iteritems(item_details)} + if filters.get("show_variant_attributes", 0) == 1: + variant_values = get_variant_values_for(list(item_details)) + item_details = {k: v.update(variant_values.get(k, {})) for k, v in iteritems(item_details)} - return item_details + return item_details def validate_filters(filters): - if not (filters.get("item_code") or filters.get("warehouse")): - sle_count = flt(frappe.db.sql( - """select count(name) from `tabStock Ledger Entry`""")[0][0]) - if sle_count > 500000: - frappe.throw( - _("Please set filter based on Item or Warehouse due to a large amount of entries.")) + if not (filters.get("item_code") or filters.get("warehouse")): + sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) + if sle_count > 500000: + frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries.")) def get_variants_attributes(): - '''Return all item variant attributes.''' - return [i.name for i in frappe.get_all('Item Attribute')] + """Return all item variant attributes.""" + return [i.name for i in frappe.get_all("Item Attribute")] def get_variant_values_for(items): - '''Returns variant values for items.''' - attribute_map = {} - for attr in frappe.db.sql('''select parent, attribute, attribute_value - from `tabItem Variant Attribute` where parent in (%s) - ''' % ", ".join(["%s"] * len(items)), tuple(items), as_dict=1): - attribute_map.setdefault(attr['parent'], {}) - attribute_map[attr['parent']].update( - {attr['attribute']: attr['attribute_value']}) - - return attribute_map + """Returns variant values for items.""" + attribute_map = {} + for attr in frappe.db.sql( + """select parent, attribute, attribute_value + from `tabItem Variant Attribute` where parent in ({}) + """.format(", ".join(["%s"] * len(items))), + tuple(items), + as_dict=1, + ): + attribute_map.setdefault(attr["parent"], {}) + attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]}) + + return attribute_map diff --git a/csf_tz/csf_tz/report/trial_balance_report_in_usd/trial_balance_report_in_usd.js b/csf_tz/csf_tz/report/trial_balance_report_in_usd/trial_balance_report_in_usd.js index ae5fa830..dd229939 100644 --- a/csf_tz/csf_tz/report/trial_balance_report_in_usd/trial_balance_report_in_usd.js +++ b/csf_tz/csf_tz/report/trial_balance_report_in_usd/trial_balance_report_in_usd.js @@ -103,4 +103,4 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "options": dimension["document_type"] }); }); -}); \ No newline at end of file +}); diff --git a/csf_tz/csf_tz/report/trial_balance_report_in_usd/trial_balance_report_in_usd.py b/csf_tz/csf_tz/report/trial_balance_report_in_usd/trial_balance_report_in_usd.py index d0a1d2ee..f422ee67 100644 --- a/csf_tz/csf_tz/report/trial_balance_report_in_usd/trial_balance_report_in_usd.py +++ b/csf_tz/csf_tz/report/trial_balance_report_in_usd/trial_balance_report_in_usd.py @@ -1,27 +1,33 @@ # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt -from __future__ import unicode_literals import frappe -from frappe import _ -from frappe.utils import flt, getdate, formatdate, cstr -from erpnext.accounts.report.financial_statements \ - import filter_accounts, set_gl_entries_by_account, filter_out_zero_value_rows from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions +from erpnext.accounts.report.financial_statements import ( + filter_accounts, + filter_out_zero_value_rows, + set_gl_entries_by_account, +) +from frappe import _ +from frappe.utils import cstr, flt, formatdate, getdate value_fields = ("opening_debit", "opening_credit", "debit", "credit", "closing_debit", "closing_credit") + def execute(filters=None): validate_filters(filters) data = get_data(filters) columns = get_columns() return columns, data + def validate_filters(filters): if not filters.fiscal_year: frappe.throw(_("Fiscal Year {0} is required").format(filters.fiscal_year)) - fiscal_year = frappe.db.get_value("Fiscal Year", filters.fiscal_year, ["year_start_date", "year_end_date"], as_dict=True) + fiscal_year = frappe.db.get_value( + "Fiscal Year", filters.fiscal_year, ["year_start_date", "year_end_date"], as_dict=True + ) if not fiscal_year: frappe.throw(_("Fiscal Year {0} does not exist").format(filters.fiscal_year)) else: @@ -41,21 +47,31 @@ def validate_filters(filters): frappe.throw(_("From Date cannot be greater than To Date")) if (filters.from_date < filters.year_start_date) or (filters.from_date > filters.year_end_date): - frappe.msgprint(_("From Date should be within the Fiscal Year. Assuming From Date = {0}")\ - .format(formatdate(filters.year_start_date))) + frappe.msgprint( + _("From Date should be within the Fiscal Year. Assuming From Date = {0}").format( + formatdate(filters.year_start_date) + ) + ) filters.from_date = filters.year_start_date if (filters.to_date < filters.year_start_date) or (filters.to_date > filters.year_end_date): - frappe.msgprint(_("To Date should be within the Fiscal Year. Assuming To Date = {0}")\ - .format(formatdate(filters.year_end_date))) + frappe.msgprint( + _("To Date should be within the Fiscal Year. Assuming To Date = {0}").format( + formatdate(filters.year_end_date) + ) + ) filters.to_date = filters.year_end_date -def get_data(filters): - accounts = frappe.db.sql("""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt +def get_data(filters): + accounts = frappe.db.sql( + """select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt - from `tabAccount` where company=%s order by lft""", filters.company, as_dict=True) + from `tabAccount` where company=%s order by lft""", + filters.company, + as_dict=True, + ) company_currency = "USD" if not accounts: @@ -63,23 +79,37 @@ def get_data(filters): accounts, accounts_by_name, parent_children_map = filter_accounts(accounts) - min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount` - where company=%s""", (filters.company,))[0] + min_lft, max_rgt = frappe.db.sql( + """select min(lft), max(rgt) from `tabAccount` + where company=%s""", + (filters.company,), + )[0] gl_entries_by_account = {} opening_balances = get_opening_balances(filters) - set_gl_entries_by_account(filters.company, filters.from_date, - filters.to_date, min_lft, max_rgt, filters, gl_entries_by_account, ignore_closing_entries=not flt(filters.with_period_closing_entry)) + set_gl_entries_by_account( + filters.company, + filters.from_date, + filters.to_date, + min_lft, + max_rgt, + filters, + gl_entries_by_account, + ignore_closing_entries=not flt(filters.with_period_closing_entry), + ) total_row = calculate_values(accounts, gl_entries_by_account, opening_balances, filters, company_currency) accumulate_values_into_parents(accounts, accounts_by_name) data = prepare_data(accounts, filters, total_row, parent_children_map, company_currency) - data = filter_out_zero_value_rows(data, parent_children_map, show_zero_values=filters.get("show_zero_values")) + data = filter_out_zero_value_rows( + data, parent_children_map, show_zero_values=filters.get("show_zero_values") + ) return data + def get_opening_balances(filters): balance_sheet_opening = get_rootwise_opening_balances(filters, "Balance Sheet") pl_opening = get_rootwise_opening_balances(filters, "Profit and Loss") @@ -91,16 +121,17 @@ def get_opening_balances(filters): def get_rootwise_opening_balances(filters, report_type): additional_conditions = "" if not filters.show_unclosed_fy_pl_balances: - additional_conditions = " and posting_date >= %(year_start_date)s" \ - if report_type == "Profit and Loss" else "" + additional_conditions = ( + " and posting_date >= %(year_start_date)s" if report_type == "Profit and Loss" else "" + ) if not flt(filters.with_period_closing_entry): additional_conditions += " and ifnull(voucher_type, '')!='Period Closing Voucher'" if filters.cost_center: - lft, rgt = frappe.db.get_value('Cost Center', filters.cost_center, ['lft', 'rgt']) - additional_conditions += """ and cost_center in (select name from `tabCost Center` - where lft >= %s and rgt <= %s)""" % (lft, rgt) + lft, rgt = frappe.db.get_value("Cost Center", filters.cost_center, ["lft", "rgt"]) + additional_conditions += f""" and cost_center in (select name from `tabCost Center` + where lft >= {lft} and rgt <= {rgt})""" if filters.finance_book: fb_conditions = " and finance_book = %(finance_book)s" @@ -117,19 +148,18 @@ def get_rootwise_opening_balances(filters, report_type): "report_type": report_type, "year_start_date": filters.year_start_date, "finance_book": filters.finance_book, - "company_fb": frappe.db.get_value("Company", filters.company, 'default_finance_book') + "company_fb": frappe.db.get_value("Company", filters.company, "default_finance_book"), } if accounting_dimensions: for dimension in accounting_dimensions: if filters.get(dimension): - additional_conditions += """ and {0} in (%({0})s) """.format(dimension) + additional_conditions += f""" and {dimension} in (%({dimension})s) """ - query_filters.update({ - dimension: filters.get(dimension) - }) + query_filters.update({dimension: filters.get(dimension)}) - gle = frappe.db.sql(""" + gle = frappe.db.sql( + f""" select account, sum(debit) as opening_debit, sum(credit) as opening_credit from `tabGL Entry` @@ -138,7 +168,10 @@ def get_rootwise_opening_balances(filters, report_type): {additional_conditions} and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') and account in (select name from `tabAccount` where report_type=%(report_type)s) - group by account""".format(additional_conditions=additional_conditions), query_filters , as_dict=True) + group by account""", + query_filters, + as_dict=True, + ) opening = frappe._dict() for d in gle: @@ -146,6 +179,7 @@ def get_rootwise_opening_balances(filters, report_type): return opening + def calculate_values(accounts, gl_entries_by_account, opening_balances, _filters, company_currency): init = { "opening_debit": 0.0, @@ -153,7 +187,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, _filters "debit": 0.0, "credit": 0.0, "closing_debit": 0.0, - "closing_credit": 0.0 + "closing_credit": 0.0, } total_row = { @@ -169,7 +203,7 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, _filters "parent_account": None, "indent": 0, "has_value": True, - "currency": company_currency + "currency": company_currency, } for d in accounts: @@ -194,15 +228,22 @@ def calculate_values(accounts, gl_entries_by_account, opening_balances, _filters return total_row + def accumulate_values_into_parents(accounts, accounts_by_name): for d in reversed(accounts): if d.parent_account: for key in value_fields: accounts_by_name[d.parent_account][key] += d[key] + def prepare_data(accounts, filters, total_row, parent_children_map, company_currency): data = [] - currency_value = frappe.get_list("Currency Exchange", filters={"from_currency": "USD", "to_currency": "TZS"}, fields=["exchange_rate"], order_by="date") + currency_value = frappe.get_list( + "Currency Exchange", + filters={"from_currency": "USD", "to_currency": "TZS"}, + fields=["exchange_rate"], + order_by="date", + ) if len(currency_value) == 0: frappe.throw("No Currency Exchange for USD") for d in accounts: @@ -218,8 +259,9 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr "from_date": filters.from_date, "to_date": filters.to_date, "currency": company_currency, - "account_name": ('{} - {}'.format(d.account_number, d.account_name) - if d.account_number else d.account_name) + "account_name": ( + f"{d.account_number} - {d.account_name}" if d.account_number else d.account_name + ), } for key in value_fields: @@ -232,10 +274,11 @@ def prepare_data(accounts, filters, total_row, parent_children_map, company_curr row["has_value"] = has_value data.append(row) - data.extend([{},total_row]) + data.extend([{}, total_row]) return data + def get_columns(): return [ { @@ -243,59 +286,60 @@ def get_columns(): "label": _("Account"), "fieldtype": "Link", "options": "Account", - "width": 300 + "width": 300, }, { "fieldname": "currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "hidden": 1 + "hidden": 1, }, { "fieldname": "opening_debit", "label": _("Opening (Dr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "opening_credit", "label": _("Opening (Cr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "closing_debit", "label": _("Closing (Dr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "fieldname": "closing_credit", "label": _("Closing (Cr)"), "fieldtype": "Currency", "options": "currency", - "width": 120 - } + "width": 120, + }, ] + def prepare_opening_closing(row): dr_or_cr = "debit" if row["root_type"] in ["Asset", "Equity", "Expense"] else "credit" reverse_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit" @@ -308,4 +352,4 @@ def prepare_opening_closing(row): row[reverse_col] = abs(row[valid_col]) row[valid_col] = 0.0 else: - row[reverse_col] = 0.0 \ No newline at end of file + row[reverse_col] = 0.0 diff --git a/csf_tz/csf_tz/report/vat_efiling_returns/vat_efiling_returns.py b/csf_tz/csf_tz/report/vat_efiling_returns/vat_efiling_returns.py index 7cef1701..59e19aa6 100644 --- a/csf_tz/csf_tz/report/vat_efiling_returns/vat_efiling_returns.py +++ b/csf_tz/csf_tz/report/vat_efiling_returns/vat_efiling_returns.py @@ -1,150 +1,110 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals + import frappe from frappe import _ -from erpnext.controllers.taxes_and_totals import get_itemised_taxable_amount -import json -from frappe.utils import flt -from csf_tz import console def execute(filters=None): - columns = get_columns() - data = get_data(filters) - return columns, data + columns = get_columns() + data = get_data(filters) + return columns, data def get_columns(): - columns = [ - { - "fieldname": "type", - "label": _("Type"), - "fieldtype": "Data", - "options": "", - "width": 150 - }, - { - "fieldname": "line", - "label": _("Line"), - "fieldtype": "Data", - "options": "", - "width": 100 - }, - { - "fieldname": "excl", - "label": _("Excl Amount"), - "fieldtype": "Float", - "options": "", - "width": 150 - }, - { - "fieldname": "vat", - "label": _("VAT Amount"), - "fieldtype": "Float", - "options": "", - "width": 150 - }, - { - "fieldname": "category", - "label": _("Category"), - "fieldtype": "Data", - "options": "", - "width": 300 - }, - ] - return columns + columns = [ + {"fieldname": "type", "label": _("Type"), "fieldtype": "Data", "options": "", "width": 150}, + {"fieldname": "line", "label": _("Line"), "fieldtype": "Data", "options": "", "width": 100}, + {"fieldname": "excl", "label": _("Excl Amount"), "fieldtype": "Float", "options": "", "width": 150}, + {"fieldname": "vat", "label": _("VAT Amount"), "fieldtype": "Float", "options": "", "width": 150}, + {"fieldname": "category", "label": _("Category"), "fieldtype": "Data", "options": "", "width": 300}, + ] + return columns def get_data(filters): + imported = { + "type": "Foreign Purchase", + "line": "Line 3", + "excl": 0, + "vat": 0, + "category": "Value of imported services", + } + non_creditable_purchases = { + "type": "Purchase", + "line": "Line 2", + "excl": 0, + "vat": 0, + "category": "Non-creditable purchases", + } + taxable_purchases = { + "type": "Purchase", + "line": "Line 1", + "excl": 0, + "vat": 0, + "category": "Taxable purchases", + } - imported = { - "type": "Foreign Purchase", - "line": "Line 3", - "excl": 0, - "vat": 0, - "category": "Value of imported services" - } - non_creditable_purchases = { - "type": "Purchase", - "line": "Line 2", - "excl": 0, - "vat": 0, - "category": "Non-creditable purchases" - } - taxable_purchases = { - "type": "Purchase", - "line": "Line 1", - "excl": 0, - "vat": 0, - "category": "Taxable purchases" - } - - purchase_list = frappe.get_all("Purchase Invoice", filters={ - "docstatus": 1, - "is_return": 0, - "company": filters.company, - "posting_date": ["between", [ - filters.from_date, - filters.to_date - ] - ] - }, - fields={"*"} - ) + purchase_list = frappe.get_all( + "Purchase Invoice", + filters={ + "docstatus": 1, + "is_return": 0, + "company": filters.company, + "posting_date": ["between", [filters.from_date, filters.to_date]], + }, + fields={"*"}, + ) - for element in purchase_list: - country = frappe.get_value( - "Supplier", element.supplier, "country") - if country and country != "Tanzania": - imported["excl"] += element.base_net_total - elif not element.base_total_taxes_and_charges: - non_creditable_purchases["excl"] += element.base_net_total - else: - taxable_purchases["vat"] += element.base_total_taxes_and_charges - taxable = element.base_total_taxes_and_charges / 0.18 - taxable_purchases["excl"] += taxable - non_creditable = element.base_net_total - taxable - non_creditable_purchases["excl"] += non_creditable + for element in purchase_list: + country = frappe.get_value("Supplier", element.supplier, "country") + if country and country != "Tanzania": + imported["excl"] += element.base_net_total + elif not element.base_total_taxes_and_charges: + non_creditable_purchases["excl"] += element.base_net_total + else: + taxable_purchases["vat"] += element.base_total_taxes_and_charges + taxable = element.base_total_taxes_and_charges / 0.18 + taxable_purchases["excl"] += taxable + non_creditable = element.base_net_total - taxable + non_creditable_purchases["excl"] += non_creditable - non_creditable_supplies = { - "type": "Sales", - "line": "Line 2", - "excl": 0, - "vat": 0, - "category": "Non-creditable supplies" - } - taxable_supplies = { - "type": "Sales", - "line": "Line 1", - "excl": 0, - "vat": 0, - "category": "Taxable supplies" - } + non_creditable_supplies = { + "type": "Sales", + "line": "Line 2", + "excl": 0, + "vat": 0, + "category": "Non-creditable supplies", + } + taxable_supplies = { + "type": "Sales", + "line": "Line 1", + "excl": 0, + "vat": 0, + "category": "Taxable supplies", + } - sales_list = frappe.get_all("Sales Invoice", filters={ - "docstatus": 1, - "is_return": 0, - "status": ["!=", "Credit Note Issued"], - "company": filters.company, - "posting_date": ["between", [ - filters.from_date, - filters.to_date - ] - ] - }, - fields={"*"} - ) + sales_list = frappe.get_all( + "Sales Invoice", + filters={ + "docstatus": 1, + "is_return": 0, + "status": ["!=", "Credit Note Issued"], + "company": filters.company, + "posting_date": ["between", [filters.from_date, filters.to_date]], + }, + fields={"*"}, + ) - for element in sales_list: - if not element.base_total_taxes_and_charges: - non_creditable_supplies["excl"] += element.base_net_total - else: - taxable_supplies["vat"] += element.base_total_taxes_and_charges - taxable = element.base_total_taxes_and_charges / 0.18 - taxable_supplies["excl"] += taxable - non_creditable = element.base_net_total - taxable - non_creditable_supplies["excl"] += non_creditable + for element in sales_list: + if not element.base_total_taxes_and_charges: + non_creditable_supplies["excl"] += element.base_net_total + else: + taxable_supplies["vat"] += element.base_total_taxes_and_charges + taxable = element.base_total_taxes_and_charges / 0.18 + taxable_supplies["excl"] += taxable + non_creditable = element.base_net_total - taxable + non_creditable_supplies["excl"] += non_creditable - return [taxable_purchases, non_creditable_purchases, taxable_supplies, non_creditable_supplies, imported] + return [taxable_purchases, non_creditable_purchases, taxable_supplies, non_creditable_supplies, imported] diff --git a/csf_tz/csf_tz/report/warehouse_wise_item_balance_and_value/warehouse_wise_item_balance_and_value.py b/csf_tz/csf_tz/report/warehouse_wise_item_balance_and_value/warehouse_wise_item_balance_and_value.py index 81ce0a8f..3dc91c5f 100644 --- a/csf_tz/csf_tz/report/warehouse_wise_item_balance_and_value/warehouse_wise_item_balance_and_value.py +++ b/csf_tz/csf_tz/report/warehouse_wise_item_balance_and_value/warehouse_wise_item_balance_and_value.py @@ -1,16 +1,22 @@ # Copyright (c) 2019, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe +from erpnext.stock.report.stock_ageing.stock_ageing import get_average_age, get_fifo_queue +from erpnext.stock.report.stock_balance.stock_balance import ( + get_item_details, + get_item_warehouse_map, + get_items, + get_stock_ledger_entries, +) from frappe import _ from frappe.utils import flt -from erpnext.stock.report.stock_balance.stock_balance import (get_item_details, get_item_warehouse_map, get_items, get_stock_ledger_entries) -from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age from six import iteritems + def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} validate_filters(filters) @@ -27,8 +33,9 @@ def execute(filters=None): item_balance = {} item_value = {} - for (company, item, warehouse) in sorted(iwb_map): - if not item_map.get(item): continue + for company, item, warehouse in sorted(iwb_map): + if not item_map.get(item): + continue row = [] qty_dict = iwb_map[(company, item, warehouse)] @@ -39,13 +46,13 @@ def execute(filters=None): total_stock_value += qty_dict.bal_val if wh.name in warehouse else 0.00 item_balance[(item, item_map[item]["item_group"])].append(row) - item_value.setdefault((item, item_map[item]["item_group"]),[]) + item_value.setdefault((item, item_map[item]["item_group"]), []) item_value[(item, item_map[item]["item_group"])].append(total_stock_value) - # sum bal_qty by item for (item, item_group), wh_balance in iteritems(item_balance): - if not item_ageing.get(item): continue + if not item_ageing.get(item): + continue total_stock_value = sum(item_value[(item, item_group)]) row = [item, item_group, total_stock_value] @@ -57,7 +64,7 @@ def execute(filters=None): row += [average_age] - bal_qty = [sum(bal_qty) for bal_qty in zip(*wh_balance)] + bal_qty = [sum(bal_qty) for bal_qty in zip(*wh_balance, strict=False)] total_qty = sum(bal_qty) if len(warehouse_list) > 1: row += [total_qty] @@ -74,17 +81,19 @@ def execute(filters=None): # frappe.msgprint(str(columns) + " " + str(data)) return columns, data + def get_columns(_filters): """return columns""" columns = [ - _("Item")+":Link/Item:100", - _("Item Group")+"::100", - _("Value")+":Currency:120", - _("Age")+":Float:60", + _("Item") + ":Link/Item:100", + _("Item Group") + "::100", + _("Value") + ":Currency:120", + _("Age") + ":Float:60", ] return columns + def validate_filters(filters): if not (filters.get("item_code") or filters.get("warehouse")): sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) @@ -93,11 +102,12 @@ def validate_filters(filters): if not filters.get("company"): filters["company"] = frappe.defaults.get_user_default("Company") + def get_warehouse_list(filters): from frappe.core.doctype.user_permission.user_permission import get_permitted_documents - condition = '' - user_permitted_warehouse = get_permitted_documents('Warehouse') + condition = "" + user_permitted_warehouse = get_permitted_documents("Warehouse") value = () if user_permitted_warehouse: condition = "and name in %s" @@ -106,16 +116,22 @@ def get_warehouse_list(filters): condition = "and name = %s" value = filters.get("warehouse") - return frappe.db.sql("""select name + return frappe.db.sql( + f"""select name from `tabWarehouse` where is_group = 0 - {condition}""".format(condition=condition), value, as_dict=1) + {condition}""", + value, + as_dict=1, + ) + def add_warehouse_column(columns, warehouse_list): if len(warehouse_list) > 1: - columns += [_("Total Qty")+":Int:80"] + columns += [_("Total Qty") + ":Int:80"] for wh in warehouse_list: - columns += [_(wh.name)+":Int:100"] + columns += [_(wh.name) + ":Int:100"] + def check_zero_total_qty(columns, data): zero_qty_columns = [] diff --git a/csf_tz/csf_tz/report/withholding_tax_payment_summary/withholding_tax_payment_summary.js b/csf_tz/csf_tz/report/withholding_tax_payment_summary/withholding_tax_payment_summary.js index 9ac06721..d673eb90 100644 --- a/csf_tz/csf_tz/report/withholding_tax_payment_summary/withholding_tax_payment_summary.js +++ b/csf_tz/csf_tz/report/withholding_tax_payment_summary/withholding_tax_payment_summary.js @@ -12,4 +12,4 @@ frappe.query_reports["Withholding Tax Payment Summary"] = { "default": "Commercial Rent" } ] -}; \ No newline at end of file +}; diff --git a/csf_tz/csf_tz/report/withholding_tax_payment_summary/withholding_tax_payment_summary.py b/csf_tz/csf_tz/report/withholding_tax_payment_summary/withholding_tax_payment_summary.py index 9f52308c..b7594a86 100644 --- a/csf_tz/csf_tz/report/withholding_tax_payment_summary/withholding_tax_payment_summary.py +++ b/csf_tz/csf_tz/report/withholding_tax_payment_summary/withholding_tax_payment_summary.py @@ -1,9 +1,10 @@ # Copyright (c) 2013, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ + + def execute(filters=None): rental = filters.get("rental") columns, data = get_columns(rental), [] @@ -12,33 +13,69 @@ def execute(filters=None): for i in sales_invoice_data: tin_number = frappe.get_value("Customer", i.customer, "tax_id") lease = frappe.get_value("Lease", i.lease, "property") - data.append({ - "date": i.posting_date, - "particulars": i.customer, - "voucher_ref": i.name, - "office_no" if rental == "Commercial Rent" else "apt": lease, - "period": str(i.from_date) + " to " + str(i.to_date) if i.from_date and i.to_date else "", - "rental_income": "$ " + str(i.total), - "withholding_tax_usd": "$ " + str(i.total / 10), - "withholding_tax_tzs": i.base_total / 10, - "control_sheet_no": i.tra_control_number or "", - "tax_certificate": i.witholding_tax_certificate_number, - "tin_number": tin_number or "", - - }) + data.append( + { + "date": i.posting_date, + "particulars": i.customer, + "voucher_ref": i.name, + "office_no" if rental == "Commercial Rent" else "apt": lease, + "period": str(i.from_date) + " to " + str(i.to_date) if i.from_date and i.to_date else "", + "rental_income": "$ " + str(i.total), + "withholding_tax_usd": "$ " + str(i.total / 10), + "withholding_tax_tzs": i.base_total / 10, + "control_sheet_no": i.tra_control_number or "", + "tax_certificate": i.witholding_tax_certificate_number, + "tin_number": tin_number or "", + } + ) return columns, data + def get_columns(rental): return [ - {"fieldname": "date","label": _("Date"),"fieldtype": "Date","width": 150}, - {"fieldname": "particulars","label": _("Particulars"),"fieldtype": "Link","options": "Customer","width": 300}, - {"fieldname": "voucher_ref","label": _("Voucher Ref."),"fieldtype": "Data","width": 180}, - {"fieldname": "office_no" if rental == "Commercial Rent" else "apt","label": _("Shop/Office No") if rental == "Commercial Rent" else "APT","fieldtype": "Data","width": 200}, - {"fieldname": "period","label": _("Period"),"fieldtype": "Data","width": 220}, - {"fieldname": "rental_income","label": _("Rental Income"),"fieldtype": "Data","options": "currency","width": 120}, - {"fieldname": "withholding_tax_usd","label": _("Withholding Tax USD"),"fieldtype": "Data","options": "currency","width": 180}, - {"fieldname": "withholding_tax_tzs","label": _("Withholding Tax TZS"),"fieldtype": "Currency","options": "currency","width": 180}, - {"fieldname": "control_sheet_no","label": _("Control Sheet No"),"fieldtype": "Data","width": 120}, - {"fieldname": "tax_certificate","label": _("Withholding Tax Certificate Number"),"fieldtype": "Data","width": 250 }, - {"fieldname": "tin_number","label": _("Tin Number"),"fieldtype": "Data","width": 300} - ] \ No newline at end of file + {"fieldname": "date", "label": _("Date"), "fieldtype": "Date", "width": 150}, + { + "fieldname": "particulars", + "label": _("Particulars"), + "fieldtype": "Link", + "options": "Customer", + "width": 300, + }, + {"fieldname": "voucher_ref", "label": _("Voucher Ref."), "fieldtype": "Data", "width": 180}, + { + "fieldname": "office_no" if rental == "Commercial Rent" else "apt", + "label": _("Shop/Office No") if rental == "Commercial Rent" else "APT", + "fieldtype": "Data", + "width": 200, + }, + {"fieldname": "period", "label": _("Period"), "fieldtype": "Data", "width": 220}, + { + "fieldname": "rental_income", + "label": _("Rental Income"), + "fieldtype": "Data", + "options": "currency", + "width": 120, + }, + { + "fieldname": "withholding_tax_usd", + "label": _("Withholding Tax USD"), + "fieldtype": "Data", + "options": "currency", + "width": 180, + }, + { + "fieldname": "withholding_tax_tzs", + "label": _("Withholding Tax TZS"), + "fieldtype": "Currency", + "options": "currency", + "width": 180, + }, + {"fieldname": "control_sheet_no", "label": _("Control Sheet No"), "fieldtype": "Data", "width": 120}, + { + "fieldname": "tax_certificate", + "label": _("Withholding Tax Certificate Number"), + "fieldtype": "Data", + "width": 250, + }, + {"fieldname": "tin_number", "label": _("Tin Number"), "fieldtype": "Data", "width": 300}, + ] diff --git a/csf_tz/csf_tz/report/withholding_tax_summary_on_sales/withholding_tax_summary_on_sales.js b/csf_tz/csf_tz/report/withholding_tax_summary_on_sales/withholding_tax_summary_on_sales.js index 9d18b6e4..61209a22 100644 --- a/csf_tz/csf_tz/report/withholding_tax_summary_on_sales/withholding_tax_summary_on_sales.js +++ b/csf_tz/csf_tz/report/withholding_tax_summary_on_sales/withholding_tax_summary_on_sales.js @@ -17,4 +17,4 @@ frappe.query_reports["Withholding Tax Summary on Sales"] = { "default": frappe.defaults.get_user_default("year_end_date"), }, ] -}; \ No newline at end of file +}; diff --git a/csf_tz/csf_tz/salary_slip.js b/csf_tz/csf_tz/salary_slip.js index 5d475729..74cd8d0a 100644 --- a/csf_tz/csf_tz/salary_slip.js +++ b/csf_tz/csf_tz/salary_slip.js @@ -6,7 +6,7 @@ frappe.ui.form.on("Salary Slip", { $('[data-label="Reject"]').parent().hide(); $('[data-label="Cancel"]').parent().hide(); } - + }, refresh:function(frm) { if (frm.doc.has_payroll_approval == 1) { @@ -48,7 +48,7 @@ frappe.ui.form.on("Salary Slip", { frm.trigger("create_update_slip_btn"); } }, - + create_update_slip_btn: function (frm) { if (frm.doc.docstatus != 0 || frm.is_new()) { return @@ -67,4 +67,4 @@ frappe.ui.form.on("Salary Slip", { }); }); }, -}); \ No newline at end of file +}); diff --git a/csf_tz/csf_tz/sales_order.js b/csf_tz/csf_tz/sales_order.js index 2eb46411..bf3ec9db 100644 --- a/csf_tz/csf_tz/sales_order.js +++ b/csf_tz/csf_tz/sales_order.js @@ -114,4 +114,4 @@ frappe.ui.keys.add_shortcut({ page: this.page, description: __('Select Item Price'), ignore_inputs: true, -}); \ No newline at end of file +}); diff --git a/csf_tz/csf_tz/stock_reconciliation.js b/csf_tz/csf_tz/stock_reconciliation.js index 0522fce3..754da934 100644 --- a/csf_tz/csf_tz/stock_reconciliation.js +++ b/csf_tz/csf_tz/stock_reconciliation.js @@ -7,5 +7,5 @@ frappe.ui.form.on('Stock Reconciliation', { }); refresh_field("items"); }, - + }) diff --git a/csf_tz/csf_tz/student_applicant.js b/csf_tz/csf_tz/student_applicant.js index a21b9709..d941087e 100644 --- a/csf_tz/csf_tz/student_applicant.js +++ b/csf_tz/csf_tz/student_applicant.js @@ -29,9 +29,9 @@ frappe.ui.form.on('Student Applicant', { frappe.db.get_value('Fee Structure', frm.doc.fee_structure, ["company"], function(value1) { frappe.db.get_value('Company', value1.company, ["send_fee_details_to_bank"], function(value2) { frm.send_fee_details_to_bank = value2.send_fee_details_to_bank || 0; - + }); }); }, - -}); \ No newline at end of file + +}); diff --git a/csf_tz/csf_tz/supplier.js b/csf_tz/csf_tz/supplier.js index cb89dc17..6054b8b6 100644 --- a/csf_tz/csf_tz/supplier.js +++ b/csf_tz/csf_tz/supplier.js @@ -4,7 +4,7 @@ frappe.ui.form.on("Supplier", { - + refresh: function(frm) { @@ -15,7 +15,7 @@ frappe.ui.form.on("Supplier", { {party_type:'Supplier', party:frm.doc.name}); }); - } + } }, - -}); \ No newline at end of file + +}); diff --git a/csf_tz/csf_tz/warehouse.js b/csf_tz/csf_tz/warehouse.js index 4835eb88..342a4c34 100644 --- a/csf_tz/csf_tz/warehouse.js +++ b/csf_tz/csf_tz/warehouse.js @@ -1,14 +1,14 @@ frappe.ui.form.on("Warehouse", { - + refresh: function(frm, dt, dn) { - + frm.add_custom_button(__('Create Stock Reconciliation'), function() { frappe.call({ method: 'csf_tz.custom_api.make_stock_reconciliation_for_all_pending_material_request', - }); + }); }); }, -}); \ No newline at end of file +}); diff --git a/csf_tz/csftz_hooks/additional_salary.py b/csf_tz/csftz_hooks/additional_salary.py index 6c68c4b5..d4b497e1 100644 --- a/csf_tz/csftz_hooks/additional_salary.py +++ b/csf_tz/csftz_hooks/additional_salary.py @@ -1,201 +1,179 @@ import frappe from frappe import _ -from frappe.utils import flt, add_days, getdate, add_months, today +from frappe.utils import add_days, add_months, flt, getdate, today @frappe.whitelist() def create_additional_salary_journal(doc, method): - if frappe.get_value( - "Salary Component", doc.salary_component, "create_cash_journal" - ): - cash_account = frappe.db.get_single_value( - "CSF TZ Settings", "default_account_for_additional_component_cash_journal" - ) - if not cash_account: - frappe.throw( - _( - "Default Account for Additional Salary Cash Journal not found. Please set it in CSF TZ Settings." - ) - ) - component_account = frappe.db.get_value( - "Salary Component Account", - {"parent": doc.salary_component, "company": doc.company}, - "account", - ) - if not component_account: - frappe.throw( - _( - f"Salary Component Account not found for {doc.salary_component} in {doc.company}. Please set it in Salary Component." - ) - ) + if frappe.get_value("Salary Component", doc.salary_component, "create_cash_journal"): + cash_account = frappe.db.get_single_value( + "CSF TZ Settings", "default_account_for_additional_component_cash_journal" + ) + if not cash_account: + frappe.throw( + _( + "Default Account for Additional Salary Cash Journal not found. Please set it in CSF TZ Settings." + ) + ) + component_account = frappe.db.get_value( + "Salary Component Account", + {"parent": doc.salary_component, "company": doc.company}, + "account", + ) + if not component_account: + frappe.throw( + _( + f"Salary Component Account not found for {doc.salary_component} in {doc.company}. Please set it in Salary Component." + ) + ) - if method == "on_submit": - dr_account = component_account - cr_account = cash_account - else: - frappe.msgprint("Unknown method on create_additional_salary_journal") - return + if method == "on_submit": + dr_account = component_account + cr_account = cash_account + else: + frappe.msgprint("Unknown method on create_additional_salary_journal") + return - precision = frappe.get_precision( - "Journal Entry Account", "debit_in_account_currency" - ) - journal_entry = frappe.new_doc("Journal Entry") - journal_entry.voucher_type = "Cash Entry" - journal_entry.user_remark = _( - f"{doc.doctype} - {doc.name} by {doc.employee_name} for {doc.salary_component}" - ) - journal_entry.company = doc.company - journal_entry.posting_date = doc.payroll_date - journal_entry.referance_doctype = doc.doctype - journal_entry.referance_docname = doc.name + precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency") + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Cash Entry" + journal_entry.user_remark = _( + f"{doc.doctype} - {doc.name} by {doc.employee_name} for {doc.salary_component}" + ) + journal_entry.company = doc.company + journal_entry.posting_date = doc.payroll_date + journal_entry.referance_doctype = doc.doctype + journal_entry.referance_docname = doc.name - payment_amount = flt(doc.amount, precision) + payment_amount = flt(doc.amount, precision) - journal_entry.set( - "accounts", - [ - {"account": dr_account, "debit_in_account_currency": payment_amount}, - {"account": cr_account, "credit_in_account_currency": payment_amount}, - ], - ) - journal_entry.save(ignore_permissions=True) + journal_entry.set( + "accounts", + [ + {"account": dr_account, "debit_in_account_currency": payment_amount}, + {"account": cr_account, "credit_in_account_currency": payment_amount}, + ], + ) + journal_entry.save(ignore_permissions=True) - if method == "on_submit": - # remark this since the field of 'journal_name' is not available in Additional Salary - # frappe.set_value(doc.doctype, doc.name, "journal_name", journal_entry.name) - msg_to_print = ( - doc.doctype + " journal " + journal_entry.name + " has been created." - ) - elif method == "on_cancel": - msg_to_print = ( - doc.doctype - + " reverse journal " - + journal_entry.name - + " has been created." - ) - frappe.msgprint(msg_to_print) - if doc.auto_created_based_on: - frappe.set_value( - "Additional Salary", - doc.auto_created_based_on, - "last_transaction_amount", - doc.amount, - ) + if method == "on_submit": + # remark this since the field of 'journal_name' is not available in Additional Salary + # frappe.set_value(doc.doctype, doc.name, "journal_name", journal_entry.name) + msg_to_print = doc.doctype + " journal " + journal_entry.name + " has been created." + elif method == "on_cancel": + msg_to_print = doc.doctype + " reverse journal " + journal_entry.name + " has been created." + frappe.msgprint(msg_to_print) + if doc.auto_created_based_on: + frappe.set_value( + "Additional Salary", + doc.auto_created_based_on, + "last_transaction_amount", + doc.amount, + ) @frappe.whitelist() def generate_additional_salary_records(): - today_date = today() - auto_repeat_frequency = {"Monthly": 1, "Annually": 12} - additional_salary_list = frappe.get_all( - "Additional Salary", - filters={ - "docstatus": "1", - "auto_repeat_frequency": ("!=", "None"), - "auto_repeat_end_date": ("!=", ""), - "auto_repeat_end_date": (">=", today_date), - }, - fields={ - "name", - "auto_repeat_end_date", - "last_transaction_date", - "last_transaction_amount", - "auto_repeat_frequency", - "payroll_date", - "employee", - "employee_name", - "salary_component", - "type", - "overwrite_salary_structure_amount", - "amount", - "company", - }, - ) - if len(additional_salary_list) > 0: - for entry in additional_salary_list: - if entry.last_transaction_date == None: - entry.last_transaction_date = entry.payroll_date - if entry.last_transaction_amount == 0: - entry.last_transaction_amount = entry.amount - if entry.auto_repeat_frequency == "Weekly": - next_date = add_days(getdate(entry.last_transaction_date), 7) - else: - frequency_factor = auto_repeat_frequency.get( - entry.auto_repeat_frequency, "Invalid frequency" - ) - if frequency_factor == "Invalid frequency": - frappe.throw( - f"Invalid frequency: {entry.auto_repeat_frequency} for {entry.name} not found. Contact the developers!" - ) - next_date = add_months( - getdate(entry.last_transaction_date), - frequency_factor, - ) - # Create 13 days in advance - specificaly to allow mid salary advance. - if next_date <= add_days(getdate(today_date), 13): - additional_salary = frappe.new_doc("Additional Salary") - additional_salary.employee = entry.employee - additional_salary.payroll_date = next_date - additional_salary.salary_component = entry.salary_component - additional_salary.employee_name = entry.employee_name - additional_salary.amount = entry.last_transaction_amount - additional_salary.company = entry.company - additional_salary.overwrite_salary_structure_amount = ( - entry.overwrite_salary_structure_amount - ) - additional_salary.type = entry.type - additional_salary.auto_repeat_frequency = "None" - additional_salary.auto_created_based_on = entry.name - additional_salary.auto_repeat_end_date = None - additional_salary.last_transaction_date = None - additional_salary.save(ignore_permissions=True) - frappe.set_value( - "Additional Salary", - entry.name, - "last_transaction_date", - next_date, - ) - frappe.msgprint( - "New additional salary created for " - + entry.auto_repeat_frequency - + " dated " - + str(next_date) - ) + today_date = today() + auto_repeat_frequency = {"Monthly": 1, "Annually": 12} + additional_salary_list = frappe.get_all( + "Additional Salary", + filters={ + "docstatus": "1", + "auto_repeat_frequency": ("!=", "None"), + "auto_repeat_end_date": (">=", today_date), + }, + fields={ + "name", + "auto_repeat_end_date", + "last_transaction_date", + "last_transaction_amount", + "auto_repeat_frequency", + "payroll_date", + "employee", + "employee_name", + "salary_component", + "type", + "overwrite_salary_structure_amount", + "amount", + "company", + }, + ) + if len(additional_salary_list) > 0: + for entry in additional_salary_list: + if entry.last_transaction_date is None: + entry.last_transaction_date = entry.payroll_date + if entry.last_transaction_amount == 0: + entry.last_transaction_amount = entry.amount + if entry.auto_repeat_frequency == "Weekly": + next_date = add_days(getdate(entry.last_transaction_date), 7) + else: + frequency_factor = auto_repeat_frequency.get(entry.auto_repeat_frequency, "Invalid frequency") + if frequency_factor == "Invalid frequency": + frappe.throw( + f"Invalid frequency: {entry.auto_repeat_frequency} for {entry.name} not found. Contact the developers!" + ) + next_date = add_months( + getdate(entry.last_transaction_date), + frequency_factor, + ) + # Create 13 days in advance - specificaly to allow mid salary advance. + if next_date <= add_days(getdate(today_date), 13): + additional_salary = frappe.new_doc("Additional Salary") + additional_salary.employee = entry.employee + additional_salary.payroll_date = next_date + additional_salary.salary_component = entry.salary_component + additional_salary.employee_name = entry.employee_name + additional_salary.amount = entry.last_transaction_amount + additional_salary.company = entry.company + additional_salary.overwrite_salary_structure_amount = entry.overwrite_salary_structure_amount + additional_salary.type = entry.type + additional_salary.auto_repeat_frequency = "None" + additional_salary.auto_created_based_on = entry.name + additional_salary.auto_repeat_end_date = None + additional_salary.last_transaction_date = None + additional_salary.save(ignore_permissions=True) + frappe.set_value( + "Additional Salary", + entry.name, + "last_transaction_date", + next_date, + ) + frappe.msgprint( + "New additional salary created for " + + entry.auto_repeat_frequency + + " dated " + + str(next_date) + ) @frappe.whitelist() def get_employee_base_salary_in_hours(employee, payroll_date): - """ - Returns the base salary in hours of the employee for this month - """ - last_salary_assignment = frappe.get_all( - "Salary Structure Assignment", - filters={"employee": employee, "from_date": ["<=", payroll_date], "docstatus": 1}, - fields=["name", "base"], - order_by="`from_date` DESC", - limit=1, - ) - last_salary_assignment = ( - last_salary_assignment[0] if last_salary_assignment else None - ) + """ + Returns the base salary in hours of the employee for this month + """ + last_salary_assignment = frappe.get_all( + "Salary Structure Assignment", + filters={"employee": employee, "from_date": ["<=", payroll_date], "docstatus": 1}, + fields=["name", "base"], + order_by="`from_date` DESC", + limit=1, + ) + last_salary_assignment = last_salary_assignment[0] if last_salary_assignment else None - working_hours_per_month = frappe.db.get_single_value( - "CSF TZ Settings", "working_hours_per_month" - ) - if not working_hours_per_month: - frappe.throw( - _( - "Working Hours per Month not defind in CSF TZ Settings. Define it there and try again." - ) - ) - base_salary_in_hours = (last_salary_assignment.base or 0) / working_hours_per_month - return {"base_salary_in_hours": base_salary_in_hours} + working_hours_per_month = frappe.db.get_single_value("CSF TZ Settings", "working_hours_per_month") + if not working_hours_per_month: + frappe.throw( + _("Working Hours per Month not defind in CSF TZ Settings. Define it there and try again.") + ) + base_salary_in_hours = (last_salary_assignment.base or 0) / working_hours_per_month + return {"base_salary_in_hours": base_salary_in_hours} def set_employee_base_salary_in_hours(doc, method): - if doc.based_on_hourly_rate: - doc.payroll_date = str(doc.payroll_date) - base_salary_in_hours = get_employee_base_salary_in_hours( - doc.employee, doc.payroll_date - )["base_salary_in_hours"] - doc.amount = doc.hourly_rate / 100 * doc.no_of_hours * base_salary_in_hours + if doc.based_on_hourly_rate: + doc.payroll_date = str(doc.payroll_date) + base_salary_in_hours = get_employee_base_salary_in_hours(doc.employee, doc.payroll_date)[ + "base_salary_in_hours" + ] + doc.amount = doc.hourly_rate / 100 * doc.no_of_hours * base_salary_in_hours diff --git a/csf_tz/csftz_hooks/attendance.py b/csf_tz/csftz_hooks/attendance.py index 62fd11c0..cf862548 100644 --- a/csf_tz/csftz_hooks/attendance.py +++ b/csf_tz/csftz_hooks/attendance.py @@ -1,267 +1,247 @@ +from datetime import datetime, timedelta + import frappe from frappe.utils import ( - get_time, - time_diff, - get_datetime, - get_weekday, + get_datetime, + get_time, + get_weekday, + time_diff, ) -from datetime import datetime, timedelta def process_overtime(doc, method): - if not frappe.db.get_single_value("CSF TZ Settings", "enable_overtime_calculation"): - return - - if ( - doc.status != "Present" - or not doc.overtime_applicable - or not doc.in_time - or not doc.out_time - ): - doc.eligible_working_hours = "00:00:00" - doc.eligible_overtime_normal = "00:00:00" - doc.eligible_overtime_holiday = "00:00:00" - doc.excess_overtime_normal = "00:00:00" - doc.excess_overtime_holiday = "00:00:00" - - return - - shift_type = frappe.get_doc("Shift Type", doc.shift) - if not shift_type.overtime_holiday: - frappe.throw(f"Please set overtime holiday in shift type {shift_type.name}") - - late_entry_grace_period = None - early_exit_grace_period = None - if ( - shift_type.enable_entry_grace_period == 1 - and shift_type.late_entry_grace_period != None - ): - late_entry_grace_period = shift_type.late_entry_grace_period - - if ( - shift_type.enable_exit_grace_period == 1 - and shift_type.early_exit_grace_period != None - ): - early_exit_grace_period = shift_type.early_exit_grace_period - - in_time = doc.in_time or "00:00:00" - out_time = doc.out_time or "00:00:00" - checkin_time = get_time(str(in_time)) - checkout_time = get_time(str(out_time)) - start_time = doc.start_time or "00:00:00" - end_time = doc.end_time or "00:00:00" - - shift_start_time, excess_in_time = calculate_shift_start_time( - checkin_time, start_time, late_entry_grace_period - ) - shift_end_time, excess_out_time = calculate_shift_end_time( - checkout_time, end_time, early_exit_grace_period - ) - - eligible_working_hours = None - if get_time(str(shift_end_time)) > get_time(str(shift_start_time)): - eligible_working_hours = time_diff(str(shift_end_time), str(shift_start_time)) - else: - eligible_working_hours = time_diff(str(shift_start_time), str(shift_end_time)) - - threshold = get_weekday_threshold(shift_type, doc.attendance_date) - - is_holiday = get_holiday_status(shift_type.overtime_holiday, doc.attendance_date) - - set_eligible_and_excess_overtime( - doc, - excess_in_time, - excess_out_time, - eligible_working_hours, - threshold, - is_holiday, - ) + if not frappe.db.get_single_value("CSF TZ Settings", "enable_overtime_calculation"): + return + + if doc.status != "Present" or not doc.overtime_applicable or not doc.in_time or not doc.out_time: + doc.eligible_working_hours = "00:00:00" + doc.eligible_overtime_normal = "00:00:00" + doc.eligible_overtime_holiday = "00:00:00" + doc.excess_overtime_normal = "00:00:00" + doc.excess_overtime_holiday = "00:00:00" + + return + + shift_type = frappe.get_doc("Shift Type", doc.shift) + if not shift_type.overtime_holiday: + frappe.throw(f"Please set overtime holiday in shift type {shift_type.name}") + + late_entry_grace_period = None + early_exit_grace_period = None + if shift_type.enable_entry_grace_period == 1 and shift_type.late_entry_grace_period is not None: + late_entry_grace_period = shift_type.late_entry_grace_period + + if shift_type.enable_exit_grace_period == 1 and shift_type.early_exit_grace_period is not None: + early_exit_grace_period = shift_type.early_exit_grace_period + + in_time = doc.in_time or "00:00:00" + out_time = doc.out_time or "00:00:00" + checkin_time = get_time(str(in_time)) + checkout_time = get_time(str(out_time)) + start_time = doc.start_time or "00:00:00" + end_time = doc.end_time or "00:00:00" + + shift_start_time, excess_in_time = calculate_shift_start_time( + checkin_time, start_time, late_entry_grace_period + ) + shift_end_time, excess_out_time = calculate_shift_end_time( + checkout_time, end_time, early_exit_grace_period + ) + + eligible_working_hours = None + if get_time(str(shift_end_time)) > get_time(str(shift_start_time)): + eligible_working_hours = time_diff(str(shift_end_time), str(shift_start_time)) + else: + eligible_working_hours = time_diff(str(shift_start_time), str(shift_end_time)) + + threshold = get_weekday_threshold(shift_type, doc.attendance_date) + + is_holiday = get_holiday_status(shift_type.overtime_holiday, doc.attendance_date) + + set_eligible_and_excess_overtime( + doc, + excess_in_time, + excess_out_time, + eligible_working_hours, + threshold, + is_holiday, + ) def calculate_shift_start_time(checkin_time, start_time, late_entry_grace_period): - _shift_start_time = None - _excess_in_time = None - if checkin_time < get_time(str(start_time)): - _shift_start_time = get_time(str(start_time)) - _excess_in_time = time_diff(str(start_time), str(checkin_time)) - else: - if late_entry_grace_period != None: - late_entry_time = timedelta(minutes=late_entry_grace_period) - start_time_ = datetime.strptime(str(start_time), "%H:%M:%S") - late_entry_datetime = start_time_ + late_entry_time - - if checkin_time <= get_time(late_entry_datetime): - _shift_start_time = get_time(str(start_time)) - else: - _shift_start_time = checkin_time - else: - _shift_start_time = checkin_time - - if _excess_in_time: - excess_in_time = _excess_in_time - else: - excess_in_time = "00:00:00" - - if _shift_start_time: - shift_start_time = _shift_start_time - else: - shift_start_time = "00:00:00" - return shift_start_time, excess_in_time + _shift_start_time = None + _excess_in_time = None + if checkin_time < get_time(str(start_time)): + _shift_start_time = get_time(str(start_time)) + _excess_in_time = time_diff(str(start_time), str(checkin_time)) + else: + if late_entry_grace_period is not None: + late_entry_time = timedelta(minutes=late_entry_grace_period) + start_time_ = datetime.strptime(str(start_time), "%H:%M:%S") + late_entry_datetime = start_time_ + late_entry_time + + if checkin_time <= get_time(late_entry_datetime): + _shift_start_time = get_time(str(start_time)) + else: + _shift_start_time = checkin_time + else: + _shift_start_time = checkin_time + + if _excess_in_time: + excess_in_time = _excess_in_time + else: + excess_in_time = "00:00:00" + + if _shift_start_time: + shift_start_time = _shift_start_time + else: + shift_start_time = "00:00:00" + return shift_start_time, excess_in_time def calculate_shift_end_time(checkout_time, end_time, early_exit_grace_period): - _shift_end_time = None - _excess_out_time = None - if checkout_time > get_time(str(end_time)): - _shift_end_time = get_time(str(end_time)) - _excess_out_time = time_diff(str(checkout_time), str(end_time)) - else: - if early_exit_grace_period != None: - early_exit_time = timedelta(minutes=early_exit_grace_period) - end_time_ = datetime.strptime(str(end_time), "%H:%M:%S") - early_exit_datetime = end_time_ - early_exit_time - - if checkout_time >= get_time(early_exit_datetime): - _shift_end_time = get_time(str(end_time)) - else: - _shift_end_time = checkout_time - else: - _shift_end_time = checkout_time - - if _excess_out_time: - excess_out_time = _excess_out_time - else: - excess_out_time = "00:00:00" - - if _shift_end_time: - shift_end_time = _shift_end_time - else: - shift_start_time = "00:00:00" - return shift_end_time, excess_out_time + _shift_end_time = None + _excess_out_time = None + if checkout_time > get_time(str(end_time)): + _shift_end_time = get_time(str(end_time)) + _excess_out_time = time_diff(str(checkout_time), str(end_time)) + else: + if early_exit_grace_period is not None: + early_exit_time = timedelta(minutes=early_exit_grace_period) + end_time_ = datetime.strptime(str(end_time), "%H:%M:%S") + early_exit_datetime = end_time_ - early_exit_time + + if checkout_time >= get_time(early_exit_datetime): + _shift_end_time = get_time(str(end_time)) + else: + _shift_end_time = checkout_time + else: + _shift_end_time = checkout_time + + if _excess_out_time: + excess_out_time = _excess_out_time + else: + excess_out_time = "00:00:00" + + if _shift_end_time: + shift_end_time = _shift_end_time + else: + shift_end_time = "00:00:00" + return shift_end_time, excess_out_time def get_weekday_threshold(shift_type, attendance_date): - day = get_weekday(get_datetime(attendance_date)) - threshold = None - if day == "Monday": - threshold = shift_type.monday_threshold + day = get_weekday(get_datetime(attendance_date)) + threshold = None + if day == "Monday": + threshold = shift_type.monday_threshold - if day == "Tuesday": - threshold = shift_type.tuesday_threshold + if day == "Tuesday": + threshold = shift_type.tuesday_threshold - if day == "Wednesday": - threshold = shift_type.wednesday_threshold + if day == "Wednesday": + threshold = shift_type.wednesday_threshold - if day == "Thursday": - threshold = shift_type.thursday_threshold + if day == "Thursday": + threshold = shift_type.thursday_threshold - if day == "Friday": - threshold = shift_type.friday_threshold + if day == "Friday": + threshold = shift_type.friday_threshold - if day == "Saturday": - threshold = shift_type.saturday_threshold + if day == "Saturday": + threshold = shift_type.saturday_threshold - if day == "Sunday": - threshold = shift_type.sunday_threshold + if day == "Sunday": + threshold = shift_type.sunday_threshold - return threshold + return threshold def get_holiday_status(overtime_holiday, attendance_date): - if frappe.db.exists( - "Holiday", - { - "parent": overtime_holiday, - "parentfield": "holidays", - "holiday_date": attendance_date, - }, - ): - return True + if frappe.db.exists( + "Holiday", + { + "parent": overtime_holiday, + "parentfield": "holidays", + "holiday_date": attendance_date, + }, + ): + return True - return False + return False def set_eligible_and_excess_overtime( - doc, excess_in_time, excess_out_time, eligible_working_hours, threshold, is_holiday + doc, excess_in_time, excess_out_time, eligible_working_hours, threshold, is_holiday ): - eligible_overtime_normal = None - excess_overtime_normal = None - eligible_overtime_holiday = None - excess_overtime_holiday = None - - excess_in_ = get_time(str(excess_in_time)) - excess_out_ = get_time(str(excess_out_time)) - excess_in_timedelta = timedelta( - hours=excess_in_.hour, minutes=excess_in_.minute, seconds=excess_in_.second - ) - excess_out_timedelta = timedelta( - hours=excess_out_.hour, minutes=excess_out_.minute, seconds=excess_out_.second - ) - - if is_holiday == False: - if eligible_working_hours > threshold: - eligible_overtime_normal = time_diff( - str(eligible_working_hours), str(threshold) - ) - - excess_overtime_normal = excess_in_timedelta + excess_out_timedelta - else: - eligible_overtime_holiday = eligible_working_hours - excess_overtime_holiday = excess_in_timedelta + excess_out_timedelta - - doc.eligible_working_hours = eligible_working_hours or "00:00:00" - - if doc.on_approval_overtime == 1: - if eligible_overtime_normal: - overtime_normal = datetime.strptime( - str(eligible_overtime_normal), "%H:%M:%S" - ) - else: - overtime_normal = datetime.strptime(str("00:00:00"), "%H:%M:%S") - if excess_overtime_normal: - excess_normal = datetime.strptime(str(excess_overtime_normal), "%H:%M:%S") - else: - excess_normal = datetime.strptime(str("00:00:00"), "%H:%M:%S") - - excess_overtime = overtime_normal + timedelta( - hours=excess_normal.hour, - minutes=excess_normal.minute, - seconds=excess_normal.second, - ) - doc.eligible_overtime_normal = "00:00:00" - doc.excess_overtime_normal = excess_overtime or "00:00:00" - - if eligible_overtime_holiday: - overtime_holiday = datetime.strptime( - str(eligible_overtime_holiday), "%H:%M:%S" - ) - else: - overtime_holiday = datetime.strptime(str("00:00:00"), "%H:%M:%S") - if excess_overtime_holiday: - excess_holiday = datetime.strptime(str(excess_overtime_holiday), "%H:%M:%S") - else: - excess_holiday = datetime.strptime(str("00:00:00"), "%H:%M:%S") - - excess_overtime_holiday = overtime_holiday + timedelta( - hours=excess_holiday.hour, - minutes=excess_holiday.minute, - seconds=excess_holiday.second, - ) - doc.eligible_overtime_holiday = "00:00:00" - doc.excess_overtime_holiday = excess_overtime_holiday or "00:00:00" - - else: - eligible_overtime_normal___ = ( - "00:00:00" - if "day" in str(eligible_overtime_normal) - else eligible_overtime_normal - ) - eligible_overtime_holiday___ = ( - "00:00:00" - if "day" in str(eligible_overtime_holiday) - else eligible_overtime_holiday - ) - doc.eligible_overtime_normal = eligible_overtime_normal___ or "00:00:00" - doc.eligible_overtime_holiday = eligible_overtime_holiday___ or "00:00:00" - doc.excess_overtime_normal = excess_overtime_normal or "00:00:00" - doc.excess_overtime_holiday = excess_overtime_holiday or "00:00:00" + eligible_overtime_normal = None + excess_overtime_normal = None + eligible_overtime_holiday = None + excess_overtime_holiday = None + + excess_in_ = get_time(str(excess_in_time)) + excess_out_ = get_time(str(excess_out_time)) + excess_in_timedelta = timedelta( + hours=excess_in_.hour, minutes=excess_in_.minute, seconds=excess_in_.second + ) + excess_out_timedelta = timedelta( + hours=excess_out_.hour, minutes=excess_out_.minute, seconds=excess_out_.second + ) + + if is_holiday is False: + if eligible_working_hours > threshold: + eligible_overtime_normal = time_diff(str(eligible_working_hours), str(threshold)) + + excess_overtime_normal = excess_in_timedelta + excess_out_timedelta + else: + eligible_overtime_holiday = eligible_working_hours + excess_overtime_holiday = excess_in_timedelta + excess_out_timedelta + + doc.eligible_working_hours = eligible_working_hours or "00:00:00" + + if doc.on_approval_overtime == 1: + if eligible_overtime_normal: + overtime_normal = datetime.strptime(str(eligible_overtime_normal), "%H:%M:%S") + else: + overtime_normal = datetime.strptime("00:00:00", "%H:%M:%S") + if excess_overtime_normal: + excess_normal = datetime.strptime(str(excess_overtime_normal), "%H:%M:%S") + else: + excess_normal = datetime.strptime("00:00:00", "%H:%M:%S") + + excess_overtime = overtime_normal + timedelta( + hours=excess_normal.hour, + minutes=excess_normal.minute, + seconds=excess_normal.second, + ) + doc.eligible_overtime_normal = "00:00:00" + doc.excess_overtime_normal = excess_overtime or "00:00:00" + + if eligible_overtime_holiday: + overtime_holiday = datetime.strptime(str(eligible_overtime_holiday), "%H:%M:%S") + else: + overtime_holiday = datetime.strptime("00:00:00", "%H:%M:%S") + if excess_overtime_holiday: + excess_holiday = datetime.strptime(str(excess_overtime_holiday), "%H:%M:%S") + else: + excess_holiday = datetime.strptime("00:00:00", "%H:%M:%S") + + excess_overtime_holiday = overtime_holiday + timedelta( + hours=excess_holiday.hour, + minutes=excess_holiday.minute, + seconds=excess_holiday.second, + ) + doc.eligible_overtime_holiday = "00:00:00" + doc.excess_overtime_holiday = excess_overtime_holiday or "00:00:00" + + else: + eligible_overtime_normal___ = ( + "00:00:00" if "day" in str(eligible_overtime_normal) else eligible_overtime_normal + ) + eligible_overtime_holiday___ = ( + "00:00:00" if "day" in str(eligible_overtime_holiday) else eligible_overtime_holiday + ) + doc.eligible_overtime_normal = eligible_overtime_normal___ or "00:00:00" + doc.eligible_overtime_holiday = eligible_overtime_holiday___ or "00:00:00" + doc.excess_overtime_normal = excess_overtime_normal or "00:00:00" + doc.excess_overtime_holiday = excess_overtime_holiday or "00:00:00" diff --git a/csf_tz/csftz_hooks/bank_charges_payment_entry.py b/csf_tz/csftz_hooks/bank_charges_payment_entry.py index 5fd5c114..34383176 100644 --- a/csf_tz/csftz_hooks/bank_charges_payment_entry.py +++ b/csf_tz/csftz_hooks/bank_charges_payment_entry.py @@ -1,54 +1,60 @@ import frappe from frappe.utils import nowdate + def validate_bank_charges_account(payment_entry, method): - """Ensure the Default Bank Charges Account is set before submitting Payment Entry""" + """Ensure the Default Bank Charges Account is set before submitting Payment Entry""" - if payment_entry.bank_charges and payment_entry.bank_charges > 0: + if payment_entry.bank_charges and payment_entry.bank_charges > 0: + company = payment_entry.company + bank_charges_account = frappe.db.get_value("Company", company, "default_bank_charges_account") - company = payment_entry.company - bank_charges_account = frappe.db.get_value("Company", company, "default_bank_charges_account") + if not bank_charges_account: + frappe.throw( + "Default Bank Charges Account is not set in Company settings. Please set it before submitting." + ) - if not bank_charges_account: - frappe.throw("Default Bank Charges Account is not set in Company settings. Please set it before submitting.") def create_bank_charges_journal(payment_entry, method): - """Creates a journal entry if bank charges are greater than 0""" - - if payment_entry.bank_charges and payment_entry.bank_charges > 0: - - company = payment_entry.company - bank_account = payment_entry.paid_from - bank_charges_account = frappe.db.get_value("Company", company, "default_bank_charges_account") - - if not bank_charges_account: - frappe.throw("Default Bank Charges Account is not set in Company settings. Please set it before submitting.") - - # Create Journal Entry - journal_entry = frappe.get_doc({ - "doctype": "Journal Entry", - "voucher_type": "Bank Entry", - "company": company, - "posting_date": payment_entry.posting_date, - "accounts": [ - { - "account": bank_charges_account, - "debit_in_account_currency": payment_entry.bank_charges, - "credit_in_account_currency": 0 - }, - { - "account": bank_account, - "debit_in_account_currency": 0, - "credit_in_account_currency": payment_entry.bank_charges - } - ], - "user_remark": f"Bank charges for Payment Entry {payment_entry.name}", - "reference_doctype": "Payment Entry", - "reference_name": payment_entry.name, - "cheque_no": payment_entry.name, - "cheque_date": nowdate() - }) - journal_entry.insert() - journal_entry.submit() - - payment_entry.db_set("bank_charges_journal_entry", journal_entry.name) + """Creates a journal entry if bank charges are greater than 0""" + + if payment_entry.bank_charges and payment_entry.bank_charges > 0: + company = payment_entry.company + bank_account = payment_entry.paid_from + bank_charges_account = frappe.db.get_value("Company", company, "default_bank_charges_account") + + if not bank_charges_account: + frappe.throw( + "Default Bank Charges Account is not set in Company settings. Please set it before submitting." + ) + + # Create Journal Entry + journal_entry = frappe.get_doc( + { + "doctype": "Journal Entry", + "voucher_type": "Bank Entry", + "company": company, + "posting_date": payment_entry.posting_date, + "accounts": [ + { + "account": bank_charges_account, + "debit_in_account_currency": payment_entry.bank_charges, + "credit_in_account_currency": 0, + }, + { + "account": bank_account, + "debit_in_account_currency": 0, + "credit_in_account_currency": payment_entry.bank_charges, + }, + ], + "user_remark": f"Bank charges for Payment Entry {payment_entry.name}", + "reference_doctype": "Payment Entry", + "reference_name": payment_entry.name, + "cheque_no": payment_entry.name, + "cheque_date": nowdate(), + } + ) + journal_entry.insert() + journal_entry.submit() + + payment_entry.db_set("bank_charges_journal_entry", journal_entry.name) diff --git a/csf_tz/csftz_hooks/budget.py b/csf_tz/csftz_hooks/budget.py index 4d7ea404..32a63258 100644 --- a/csf_tz/csftz_hooks/budget.py +++ b/csf_tz/csftz_hooks/budget.py @@ -1,12 +1,9 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2025, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe -from frappe import _ -from frappe.utils import flt from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget +from frappe.utils import flt def check_budget_for_journal_entry(doc, method=None): @@ -16,9 +13,7 @@ def check_budget_for_journal_entry(doc, method=None): For Journal Entry, budget is checked against each account entry when making GL entries. """ - if frappe.db.get_single_value( - "CSF TZ Settings", "check_budget_in_je" - ): + if frappe.db.get_single_value("CSF TZ Settings", "check_budget_in_je"): for account in doc.get("accounts") or []: # Check if account has at least one budget dimension (cost_center or project) # ERPNext's budget validation can work with either dimension independently @@ -55,9 +50,7 @@ def check_budget_for_material_request(doc, method=None): Material Request has items and the budget is checked against each item. """ - if frappe.db.get_single_value( - "CSF TZ Settings", "check_budget_in_mr" - ): + if frappe.db.get_single_value("CSF TZ Settings", "check_budget_in_mr"): for item in doc.get("items") or []: # Prepare args for budget validation args = item.as_dict() @@ -90,9 +83,7 @@ def check_budget_for_purchase_order(doc, method=None): Purchase Order has items and the budget is checked against each item. """ - if frappe.db.get_single_value( - "CSF TZ Settings", "check_budget_in_po" - ): + if frappe.db.get_single_value("CSF TZ Settings", "check_budget_in_po"): for item in doc.get("items") or []: # Prepare args for budget validation args = item.as_dict() @@ -118,6 +109,7 @@ def check_budget_for_purchase_order(doc, method=None): # Pass expense_amount explicitly validate_expense_against_budget(args, expense_amount=expense_amount) + def check_budget_for_purchase_invoice(doc, method=None): """ Check budget for Purchase Invoice document. diff --git a/csf_tz/csftz_hooks/customer.py b/csf_tz/csftz_hooks/customer.py index 6208896a..fec4d49a 100644 --- a/csf_tz/csftz_hooks/customer.py +++ b/csf_tz/csftz_hooks/customer.py @@ -1,31 +1,32 @@ import frappe from frappe import _ + @frappe.whitelist() def get_customer_total_unpaid_amount(customer, company=None): - if not customer: - return 0 - company_condition = "" - if company: - company_condition = " and company = '{0}'".format(company) - company_wise_total_unpaid = frappe._dict( - frappe.db.sql( - """ + if not customer: + return 0 + company_condition = "" + if company: + company_condition = f" and company = '{company}'" + company_wise_total_unpaid = frappe._dict( + frappe.db.sql( + f""" select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where party_type = %s and party=%s - and is_cancelled = 0 {0} - group by company""".format(company_condition), - ("Customer", customer), - ) - ) - total_unpaid = 0 - if company: - total_unpaid = company_wise_total_unpaid.get(company, 0) - else: - total_unpaid = sum(company_wise_total_unpaid.values()) + and is_cancelled = 0 {company_condition} + group by company""", + ("Customer", customer), + ) + ) + total_unpaid = 0 + if company: + total_unpaid = company_wise_total_unpaid.get(company, 0) + else: + total_unpaid = sum(company_wise_total_unpaid.values()) - total_unpaid = frappe.format_value(total_unpaid, "Float") + total_unpaid = frappe.format_value(total_unpaid, "Float") - frappe.msgprint(_("Total Unpaid Amount is {0}").format(total_unpaid)) - return total_unpaid \ No newline at end of file + frappe.msgprint(_("Total Unpaid Amount is {0}").format(total_unpaid)) + return total_unpaid diff --git a/csf_tz/csftz_hooks/employee_advance_payment_and_expense.py b/csf_tz/csftz_hooks/employee_advance_payment_and_expense.py index 6fb62491..3efd7b6f 100644 --- a/csf_tz/csftz_hooks/employee_advance_payment_and_expense.py +++ b/csf_tz/csftz_hooks/employee_advance_payment_and_expense.py @@ -4,45 +4,43 @@ def execute(doc, method): - """Main execution function""" - if doc.docstatus != 1 or not doc.travel_request_ref: - return + """Main execution function""" + if doc.docstatus != 1 or not doc.travel_request_ref: + return - if frappe.db.exists( - "Payment Entry", {"reference_no": doc.name, "docstatus": ["!=", 2]} - ): - frappe.msgprint("Payment Entry already exists for this advance") - return + if frappe.db.exists("Payment Entry", {"reference_no": doc.name, "docstatus": ["!=", 2]}): + frappe.msgprint("Payment Entry already exists for this advance") + return - try: - payment_entry = create_payment_entry(doc) - if payment_entry: - doc.reload() - except Exception as e: - frappe.throw(f"Error creating payment entry: {str(e)}") + try: + payment_entry = create_payment_entry(doc) + if payment_entry: + doc.reload() + except Exception as e: + frappe.throw(f"Error creating payment entry: {str(e)}") def create_payment_entry(doc): - """Create payment entry with permission bypass""" - # Set permission bypass flags globally for this operation - frappe.flags.ignore_account_permission = True - frappe.flags.ignore_permissions = True - - payment_entry = get_payment_entry_for_employee("Employee Advance", doc.name) - - # Set reference details - payment_entry.update( - { - "reference_no": doc.name, - "reference_date": nowdate(), - } - ) - - # Apply permission bypass flags - payment_entry.flags.ignore_permissions = True - payment_entry.flags.ignore_validate = True - payment_entry.flags.ignore_mandatory = True - payment_entry.insert(ignore_permissions=True) - - frappe.msgprint(f"Payment Entry {payment_entry.name} created successfully") - return payment_entry + """Create payment entry with permission bypass""" + # Set permission bypass flags globally for this operation + frappe.flags.ignore_account_permission = True + frappe.flags.ignore_permissions = True + + payment_entry = get_payment_entry_for_employee("Employee Advance", doc.name) + + # Set reference details + payment_entry.update( + { + "reference_no": doc.name, + "reference_date": nowdate(), + } + ) + + # Apply permission bypass flags + payment_entry.flags.ignore_permissions = True + payment_entry.flags.ignore_validate = True + payment_entry.flags.ignore_mandatory = True + payment_entry.insert(ignore_permissions=True) + + frappe.msgprint(f"Payment Entry {payment_entry.name} created successfully") + return payment_entry diff --git a/csf_tz/csftz_hooks/employee_checkin.py b/csf_tz/csftz_hooks/employee_checkin.py index ebc8252c..4a056b6c 100644 --- a/csf_tz/csftz_hooks/employee_checkin.py +++ b/csf_tz/csftz_hooks/employee_checkin.py @@ -1,169 +1,162 @@ -import frappe -from typing import Dict, List from datetime import datetime, timedelta + +import frappe +from frappe import _ from frappe.query_builder import Criterion -from hrms.hr.utils import validate_active_employee -from frappe.utils import cint, get_datetime, now_datetime, add_days +from frappe.utils import get_datetime, now_datetime from hrms.hr.doctype.shift_assignment.shift_assignment import ( - get_actual_start_end_datetime_of_shift, - get_exact_shift, - get_shift_for_time, - get_shift_details, - get_prev_or_next_shift, + get_exact_shift, + get_prev_or_next_shift, + get_shift_details, + get_shift_for_time, ) +from hrms.hr.utils import validate_active_employee def validate(doc, method): - override_fetch_shift_details = frappe.db.get_single_value( - "CSF TZ Settings", "override_fetch_shift_details" - ) - if override_fetch_shift_details == 0: - return - - validate_active_employee(doc.employee) - doc.validate_duplicate_log() - shift_timings = get_employee_shift_timings( - doc.employee, get_datetime(doc.time), True - ) - shift_actual_timings = get_exact_shift(shift_timings, get_datetime(doc.time)) - if shift_actual_timings: - if ( - shift_actual_timings.shift_type.determine_check_in_and_check_out - == "Strictly based on Log Type in Employee Checkin" - and not doc.log_type - and not doc.skip_auto_attendance - ): - frappe.throw( - _( - "Log Type is required for check-ins falling in the shift: {0}." - ).format(shift_actual_timings.shift_type.name) - ) - if not doc.attendance: - doc.shift = shift_actual_timings.shift_type.name - doc.shift_actual_start = shift_actual_timings.actual_start - doc.shift_actual_end = shift_actual_timings.actual_end - doc.shift_start = shift_actual_timings.start_datetime - doc.shift_end = shift_actual_timings.end_datetime - else: - doc.shift = None + override_fetch_shift_details = frappe.db.get_single_value( + "CSF TZ Settings", "override_fetch_shift_details" + ) + if override_fetch_shift_details == 0: + return + + validate_active_employee(doc.employee) + doc.validate_duplicate_log() + shift_timings = get_employee_shift_timings(doc.employee, get_datetime(doc.time), True) + shift_actual_timings = get_exact_shift(shift_timings, get_datetime(doc.time)) + if shift_actual_timings: + if ( + shift_actual_timings.shift_type.determine_check_in_and_check_out + == "Strictly based on Log Type in Employee Checkin" + and not doc.log_type + and not doc.skip_auto_attendance + ): + frappe.throw( + _("Log Type is required for check-ins falling in the shift: {0}.").format( + shift_actual_timings.shift_type.name + ) + ) + if not doc.attendance: + doc.shift = shift_actual_timings.shift_type.name + doc.shift_actual_start = shift_actual_timings.actual_start + doc.shift_actual_end = shift_actual_timings.actual_end + doc.shift_start = shift_actual_timings.start_datetime + doc.shift_end = shift_actual_timings.end_datetime + else: + doc.shift = None def get_employee_shift_timings( - employee: str, for_timestamp: datetime = None, consider_default_shift: bool = False -) -> List[Dict]: - """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee""" - if for_timestamp is None: - for_timestamp = now_datetime() - - # write and verify a test case for midnight shift. - prev_shift = curr_shift = next_shift = None - curr_shift = get_employee_shift( - employee, for_timestamp, consider_default_shift, "forward" - ) - if curr_shift: - next_shift = get_employee_shift( - employee, - curr_shift.start_datetime + timedelta(days=1), - consider_default_shift, - "forward", - ) - prev_shift = get_employee_shift( - employee, - (curr_shift.start_datetime if curr_shift else for_timestamp) - + timedelta(days=-1), - consider_default_shift, - "reverse", - ) - - if curr_shift: - # adjust actual start and end times if they are overlapping with grace period (before start and after end) - if prev_shift: - curr_shift.actual_start = ( - prev_shift.end_datetime - if curr_shift.actual_start < prev_shift.end_datetime - else curr_shift.actual_start - ) - prev_shift.actual_end = ( - curr_shift.actual_start - if prev_shift.actual_end > curr_shift.actual_start - else prev_shift.actual_end - ) - if next_shift: - next_shift.actual_start = ( - curr_shift.end_datetime - if next_shift.actual_start < curr_shift.end_datetime - else next_shift.actual_start - ) - curr_shift.actual_end = ( - next_shift.actual_start - if curr_shift.actual_end > next_shift.actual_start - else curr_shift.actual_end - ) - - return prev_shift, curr_shift, next_shift + employee: str, for_timestamp: datetime = None, consider_default_shift: bool = False +) -> list[dict]: + """Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee""" + if for_timestamp is None: + for_timestamp = now_datetime() + + # write and verify a test case for midnight shift. + prev_shift = curr_shift = next_shift = None + curr_shift = get_employee_shift(employee, for_timestamp, consider_default_shift, "forward") + if curr_shift: + next_shift = get_employee_shift( + employee, + curr_shift.start_datetime + timedelta(days=1), + consider_default_shift, + "forward", + ) + prev_shift = get_employee_shift( + employee, + (curr_shift.start_datetime if curr_shift else for_timestamp) + timedelta(days=-1), + consider_default_shift, + "reverse", + ) + + if curr_shift: + # adjust actual start and end times if they are overlapping with grace period (before start and after end) + if prev_shift: + curr_shift.actual_start = ( + prev_shift.end_datetime + if curr_shift.actual_start < prev_shift.end_datetime + else curr_shift.actual_start + ) + prev_shift.actual_end = ( + curr_shift.actual_start + if prev_shift.actual_end > curr_shift.actual_start + else prev_shift.actual_end + ) + if next_shift: + next_shift.actual_start = ( + curr_shift.end_datetime + if next_shift.actual_start < curr_shift.end_datetime + else next_shift.actual_start + ) + curr_shift.actual_end = ( + next_shift.actual_start + if curr_shift.actual_end > next_shift.actual_start + else curr_shift.actual_end + ) + + return prev_shift, curr_shift, next_shift def get_employee_shift( - employee: str, - for_timestamp: datetime = None, - consider_default_shift: bool = False, - next_shift_direction: str = None, -) -> Dict: - shift_details = {} - shifts_for_date = get_shifts_for_date(employee, for_timestamp) - if shifts_for_date: - shift_details = get_shift_for_time(shifts_for_date, for_timestamp) - - # if shift assignment is not found, consider default shift - default_shift = frappe.db.get_value( - "Employee", employee, "default_shift", cache=True - ) - if not shift_details and consider_default_shift: - shift_details = get_shift_details(default_shift, for_timestamp) - - # if no shift is found, find next or prev shift assignment based on direction - if not shift_details and next_shift_direction: - shift_details = get_prev_or_next_shift( - employee, - for_timestamp, - consider_default_shift, - default_shift, - next_shift_direction, - ) - - return shift_details or {} - - -def get_shifts_for_date(employee: str, for_timestamp: datetime) -> List[Dict[str, str]]: - """Returns list of shifts with details for given date""" - for_date = for_timestamp.date() - - assignment = frappe.qb.DocType("Shift Assignment") - shift_assignments = ( - frappe.qb.from_(assignment) - .select( - assignment.name, - assignment.shift_type, - assignment.start_date, - assignment.end_date, - ) - .where( - (assignment.employee == employee) - & (assignment.docstatus == 1) - & (assignment.status == "Active") - & (assignment.start_date <= for_date) - & ( - Criterion.any( - [ - assignment.end_date.isnull(), - ( - assignment.end_date.isnotnull() - # for midnight shifts, valid assignments are upto 1 day prior - & (for_date <= assignment.end_date) - ), - ] - ) - ) - ) - ).run(as_dict=True) - return shift_assignments + employee: str, + for_timestamp: datetime = None, + consider_default_shift: bool = False, + next_shift_direction: str = None, +) -> dict: + shift_details = {} + shifts_for_date = get_shifts_for_date(employee, for_timestamp) + if shifts_for_date: + shift_details = get_shift_for_time(shifts_for_date, for_timestamp) + + # if shift assignment is not found, consider default shift + default_shift = frappe.db.get_value("Employee", employee, "default_shift", cache=True) + if not shift_details and consider_default_shift: + shift_details = get_shift_details(default_shift, for_timestamp) + + # if no shift is found, find next or prev shift assignment based on direction + if not shift_details and next_shift_direction: + shift_details = get_prev_or_next_shift( + employee, + for_timestamp, + consider_default_shift, + default_shift, + next_shift_direction, + ) + + return shift_details or {} + + +def get_shifts_for_date(employee: str, for_timestamp: datetime) -> list[dict[str, str]]: + """Returns list of shifts with details for given date""" + for_date = for_timestamp.date() + + assignment = frappe.qb.DocType("Shift Assignment") + shift_assignments = ( + frappe.qb.from_(assignment) + .select( + assignment.name, + assignment.shift_type, + assignment.start_date, + assignment.end_date, + ) + .where( + (assignment.employee == employee) + & (assignment.docstatus == 1) + & (assignment.status == "Active") + & (assignment.start_date <= for_date) + & ( + Criterion.any( + [ + assignment.end_date.isnull(), + ( + assignment.end_date.isnotnull() + # for midnight shifts, valid assignments are upto 1 day prior + & (for_date <= assignment.end_date) + ), + ] + ) + ) + ) + ).run(as_dict=True) + return shift_assignments diff --git a/csf_tz/csftz_hooks/employee_contact_qr.py b/csf_tz/csftz_hooks/employee_contact_qr.py index f8f22f79..9c62e180 100644 --- a/csf_tz/csftz_hooks/employee_contact_qr.py +++ b/csf_tz/csftz_hooks/employee_contact_qr.py @@ -1,49 +1,47 @@ +import base64 +import io + import frappe -from frappe import _ import qrcode -import io -import base64 +from frappe import _ + @frappe.whitelist() def generate_contact_qr(employee): - employee_doc = frappe.get_doc("Employee", employee) + employee_doc = frappe.get_doc("Employee", employee) - # Retrieve contact details - phone = employee_doc.cell_number - email = employee_doc.personal_email or employee_doc.company_email + # Retrieve contact details + phone = employee_doc.cell_number + email = employee_doc.personal_email or employee_doc.company_email - # Check if both phone and email are missing - if not phone and not email: - frappe.throw(_("Employee must have at least a phone number or an email to generate a QR Code.")) + # Check if both phone and email are missing + if not phone and not email: + frappe.throw(_("Employee must have at least a phone number or an email to generate a QR Code.")) - # Create vCard format string - vcard = f"""BEGIN:VCARD + # Create vCard format string + vcard = f"""BEGIN:VCARD VERSION:3.0 N:{employee_doc.last_name};{employee_doc.first_name};;; FN:{employee_doc.employee_name} -ORG:{frappe.defaults.get_global_default('company')} +ORG:{frappe.defaults.get_global_default("company")} TITLE:{employee_doc.designation} -TEL;TYPE=WORK,VOICE:{phone or ''} -EMAIL;TYPE=WORK:{email or ''} +TEL;TYPE=WORK,VOICE:{phone or ""} +EMAIL;TYPE=WORK:{email or ""} END:VCARD""" - # Generate QR code - qr = qrcode.QRCode( - version=1, - error_correction=qrcode.constants.ERROR_CORRECT_L, - box_size=10, - border=4, - mask_pattern=7 - ) - qr.add_data(vcard) - qr.make(fit=True) - - # Create QR code image - img = qr.make_image(fill_color="black", back_color="white") - - # Convert to base64 for displaying in the frontend - buffer = io.BytesIO() - img.save(buffer, format='PNG') - qr_base64 = base64.b64encode(buffer.getvalue()).decode() - - return qr_base64 + # Generate QR code + qr = qrcode.QRCode( + version=1, error_correction=qrcode.constants.ERROR_CORRECT_L, box_size=10, border=4, mask_pattern=7 + ) + qr.add_data(vcard) + qr.make(fit=True) + + # Create QR code image + img = qr.make_image(fill_color="black", back_color="white") + + # Convert to base64 for displaying in the frontend + buffer = io.BytesIO() + img.save(buffer, format="PNG") + qr_base64 = base64.b64encode(buffer.getvalue()).decode() + + return qr_base64 diff --git a/csf_tz/csftz_hooks/exchange_calculations.py b/csf_tz/csftz_hooks/exchange_calculations.py index 57e899f5..cb2c6425 100644 --- a/csf_tz/csftz_hooks/exchange_calculations.py +++ b/csf_tz/csftz_hooks/exchange_calculations.py @@ -1,723 +1,655 @@ import frappe -from frappe.utils import flt, getdate, nowdate, add_days from frappe import _ -import json +from frappe.utils import flt, nowdate def create_import_tracker(doc, method): - """Create Foreign Import Transaction when Purchase Invoice is submitted""" - if not doc.currency: - return - - company_currency = frappe.get_cached_value( - "Company", doc.company, "default_currency" - ) - - # Only create tracker for foreign currency invoices - if doc.currency == company_currency: - return - - # Check if tracker already exists - existing = frappe.db.exists( - "Foreign Import Transaction", {"purchase_invoice": doc.name} - ) - if existing: - return - - try: - import_tracker = frappe.new_doc("Foreign Import Transaction") - import_tracker.purchase_invoice = doc.name - import_tracker.supplier = doc.supplier - import_tracker.transaction_date = doc.posting_date - import_tracker.currency = doc.currency - import_tracker.original_exchange_rate = doc.conversion_rate - import_tracker.invoice_amount_foreign = doc.grand_total - import_tracker.invoice_amount_base = doc.base_grand_total - import_tracker.company = doc.company - import_tracker.status = "Draft" - - import_tracker.insert() - import_tracker.submit() - - # Add custom field reference - frappe.db.set_value( - "Purchase Invoice", doc.name, "foreign_import_tracker", import_tracker.name - ) - - frappe.msgprint( - _("Foreign Import Transaction {0} created successfully").format( - import_tracker.name - ), - alert=True, - ) - - except Exception as e: - frappe.log_error( - f"Error creating Foreign Import Transaction for {doc.name}: {str(e)}" - ) + """Create Foreign Import Transaction when Purchase Invoice is submitted""" + if not doc.currency: + return + + company_currency = frappe.get_cached_value("Company", doc.company, "default_currency") + + # Only create tracker for foreign currency invoices + if doc.currency == company_currency: + return + + # Check if tracker already exists + existing = frappe.db.exists("Foreign Import Transaction", {"purchase_invoice": doc.name}) + if existing: + return + + try: + import_tracker = frappe.new_doc("Foreign Import Transaction") + import_tracker.purchase_invoice = doc.name + import_tracker.supplier = doc.supplier + import_tracker.transaction_date = doc.posting_date + import_tracker.currency = doc.currency + import_tracker.original_exchange_rate = doc.conversion_rate + import_tracker.invoice_amount_foreign = doc.grand_total + import_tracker.invoice_amount_base = doc.base_grand_total + import_tracker.company = doc.company + import_tracker.status = "Draft" + + import_tracker.insert() + import_tracker.submit() + + # Add custom field reference + frappe.db.set_value("Purchase Invoice", doc.name, "foreign_import_tracker", import_tracker.name) + + frappe.msgprint( + _("Foreign Import Transaction {0} created successfully").format(import_tracker.name), + alert=True, + ) + + except Exception as e: + frappe.log_error(f"Error creating Foreign Import Transaction for {doc.name}: {str(e)}") def cancel_import_tracker(doc, method): - """Cancel Foreign Import Transaction when Purchase Invoice is cancelled""" - tracker_name = frappe.db.get_value( - "Foreign Import Transaction", {"purchase_invoice": doc.name}, "name" - ) - - if tracker_name: - try: - tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) - if tracker.docstatus == 1: - tracker.cancel() - except Exception as e: - frappe.log_error( - "Error on Cancelling Foreign Import Transaction", - f"Error cancelling Foreign Import Transaction {tracker_name}: {str(e)}", - ) + """Cancel Foreign Import Transaction when Purchase Invoice is cancelled""" + tracker_name = frappe.db.get_value("Foreign Import Transaction", {"purchase_invoice": doc.name}, "name") + + if tracker_name: + try: + tracker = frappe.get_doc("Foreign Import Transaction", tracker_name) + if tracker.docstatus == 1: + tracker.cancel() + except Exception as e: + frappe.log_error( + "Error on Cancelling Foreign Import Transaction", + f"Error cancelling Foreign Import Transaction {tracker_name}: {str(e)}", + ) def link_lcv_to_import_tracker(doc, method): - """Link Landed Cost Voucher to Foreign Import Transaction""" - if not doc.purchase_receipts: - return - - for pr_row in doc.purchase_receipts: - receipt_doc = pr_row.get("receipt_document") or pr_row.get("purchase_receipt") - if not receipt_doc: - continue - - # Find related purchase invoice through purchase receipt - if pr_row.get("receipt_document_type") == "Purchase Invoice": - pi_items = [{"purchase_invoice": receipt_doc}] - else: - pi_items = frappe.db.sql( - """ + """Link Landed Cost Voucher to Foreign Import Transaction""" + if not doc.purchase_receipts: + return + + for pr_row in doc.purchase_receipts: + receipt_doc = pr_row.get("receipt_document") or pr_row.get("purchase_receipt") + if not receipt_doc: + continue + + # Find related purchase invoice through purchase receipt + if pr_row.get("receipt_document_type") == "Purchase Invoice": + pi_items = [{"purchase_invoice": receipt_doc}] + else: + pi_items = frappe.db.sql( + """ SELECT DISTINCT pii.parent as purchase_invoice FROM `tabPurchase Invoice Item` pii WHERE pii.purchase_receipt = %s """, - receipt_doc, - as_dict=True, - ) - - for pi_item in pi_items: - purchase_invoice = pi_item.get("purchase_invoice") - if not purchase_invoice: - continue - tracker_name = frappe.db.get_value( - "Foreign Import Transaction", - {"purchase_invoice": purchase_invoice}, - "name", - ) - - if tracker_name: - try: - tracker_doc = frappe.get_doc( - "Foreign Import Transaction", tracker_name - ) - - # Check if LCV already added - existing_lcv = any( - row.landed_cost_voucher == doc.name - for row in tracker_doc.landed_cost_vouchers - ) - if not existing_lcv: - tracker_doc.add_lcv_detail(doc.name) - - # Calculate LCV exchange difference - calculate_lcv_exchange_difference(tracker_doc, doc) - - except Exception as e: - frappe.log_error( - f"Error linking LCV {doc.name} to tracker {tracker_name}: {str(e)}" - ) + receipt_doc, + as_dict=True, + ) + + for pi_item in pi_items: + purchase_invoice = pi_item.get("purchase_invoice") + if not purchase_invoice: + continue + tracker_name = frappe.db.get_value( + "Foreign Import Transaction", + {"purchase_invoice": purchase_invoice}, + "name", + ) + + if tracker_name: + try: + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Check if LCV already added + existing_lcv = any( + row.landed_cost_voucher == doc.name for row in tracker_doc.landed_cost_vouchers + ) + if not existing_lcv: + tracker_doc.add_lcv_detail(doc.name) + + # Calculate LCV exchange difference + calculate_lcv_exchange_difference(tracker_doc, doc) + + except Exception as e: + frappe.log_error(f"Error linking LCV {doc.name} to tracker {tracker_name}: {str(e)}") def unlink_lcv_from_import_tracker(doc, method): - """Remove LCV from Foreign Import Transaction when cancelled""" - trackers = frappe.db.sql( - """ + """Remove LCV from Foreign Import Transaction when cancelled""" + trackers = frappe.db.sql( + """ SELECT DISTINCT fit.name FROM `tabForeign Import Transaction` fit JOIN `tabForeign Import LCV Details` lcv ON lcv.parent = fit.name WHERE lcv.landed_cost_voucher = %s """, - doc.name, - as_dict=True, - ) - - for tracker in trackers: - try: - tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) - - # Remove LCV rows - tracker_doc.landed_cost_vouchers = [ - row - for row in tracker_doc.landed_cost_vouchers - if row.landed_cost_voucher != doc.name - ] - - # Remove related exchange difference entries - tracker_doc.exchange_differences = [ - row - for row in tracker_doc.exchange_differences - if not ( - row.reference_type == "Landed Cost Voucher" - and row.reference_name == doc.name - ) - ] - - tracker_doc.save() - - except Exception as e: - frappe.log_error( - f"Error unlinking LCV {doc.name} from tracker {tracker.name}: {str(e)}" - ) + doc.name, + as_dict=True, + ) + + for tracker in trackers: + try: + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) + + # Remove LCV rows + tracker_doc.landed_cost_vouchers = [ + row for row in tracker_doc.landed_cost_vouchers if row.landed_cost_voucher != doc.name + ] + + # Remove related exchange difference entries + tracker_doc.exchange_differences = [ + row + for row in tracker_doc.exchange_differences + if not (row.reference_type == "Landed Cost Voucher" and row.reference_name == doc.name) + ] + + tracker_doc.save() + + except Exception as e: + frappe.log_error(f"Error unlinking LCV {doc.name} from tracker {tracker.name}: {str(e)}") def link_payment_to_import_tracker(doc, method): - """Link Payment Entry to Foreign Import Transaction""" - if doc.payment_type != "Pay" or doc.party_type != "Supplier": - return + """Link Payment Entry to Foreign Import Transaction""" + if doc.payment_type != "Pay" or doc.party_type != "Supplier": + return - # Find active import trackers for this supplier - trackers = frappe.db.sql( - """ + # Find active import trackers for this supplier + trackers = frappe.db.sql( + """ SELECT name, purchase_invoice, currency, original_exchange_rate, invoice_amount_foreign FROM `tabForeign Import Transaction` WHERE supplier = %s AND status IN ('Active', 'Draft') AND docstatus = 1 ORDER BY transaction_date DESC """, - doc.party, - as_dict=True, - ) - - for tracker_data in trackers: - # Check if payment currency matches tracker currency - if doc.paid_to_account_currency == tracker_data.currency: - try: - tracker_doc = frappe.get_doc( - "Foreign Import Transaction", tracker_data.name - ) - - # Check if payment already added - existing_payment = any( - row.payment_entry == doc.name for row in tracker_doc.payments - ) - if not existing_payment: - payment_row = tracker_doc.add_payment_detail(doc.name) - - # Calculate and create exchange difference entry - calculate_payment_exchange_difference(tracker_doc, doc, payment_row) - - # Add custom field reference - frappe.db.set_value( - "Payment Entry", - doc.name, - "foreign_import_tracker", - tracker_doc.name, - ) - - break # Link to first matching tracker only - - except Exception as e: - # Instead of passing the full error as title: - frappe.log_error( - title=f"Error linking payment {doc.name} to tracker {tracker_doc.name}", # keep short, <140 chars - message=frappe.get_traceback(), - ) + doc.party, + as_dict=True, + ) + + for tracker_data in trackers: + # Check if payment currency matches tracker currency + if doc.paid_to_account_currency == tracker_data.currency: + try: + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_data.name) + + # Check if payment already added + existing_payment = any(row.payment_entry == doc.name for row in tracker_doc.payments) + if not existing_payment: + payment_row = tracker_doc.add_payment_detail(doc.name) + + # Calculate and create exchange difference entry + calculate_payment_exchange_difference(tracker_doc, doc, payment_row) + + # Add custom field reference + frappe.db.set_value( + "Payment Entry", + doc.name, + "foreign_import_tracker", + tracker_doc.name, + ) + + break # Link to first matching tracker only + + except Exception: + # Instead of passing the full error as title: + frappe.log_error( + title=f"Error linking payment {doc.name} to tracker {tracker_doc.name}", # keep short, <140 chars + message=frappe.get_traceback(), + ) def unlink_payment_from_import_tracker(doc, method): - """Remove payment from Foreign Import Transaction when cancelled""" - tracker_name = frappe.db.get_value( - "Payment Entry", doc.name, "foreign_import_tracker" - ) - - if tracker_name: - try: - tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Remove payment rows - tracker_doc.payments = [ - row for row in tracker_doc.payments if row.payment_entry != doc.name - ] - - # Remove related exchange difference entries and cancel JEs - for diff_row in tracker_doc.exchange_differences: - if ( - diff_row.reference_type == "Payment Entry" - and diff_row.reference_name == doc.name - ): - if diff_row.journal_entry: - try: - je = frappe.get_doc("Journal Entry", diff_row.journal_entry) - if je.docstatus == 1: - je.cancel() - except: - pass - - tracker_doc.exchange_differences = [ - row - for row in tracker_doc.exchange_differences - if not ( - row.reference_type == "Payment Entry" - and row.reference_name == doc.name - ) - ] - - tracker_doc.save() - - except Exception as e: - frappe.log_error( - f"Error unlinking payment {doc.name} from tracker {tracker_name}: {str(e)}" - ) - - -def calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row): - """Calculate exchange difference for payment and create Journal Entry""" - settings = get_import_settings(tracker_doc.company) - - original_rate = flt(tracker_doc.original_exchange_rate) - payment_rate = flt(payment_doc.source_exchange_rate) - paid_amount = flt(payment_doc.paid_amount) - - if abs(original_rate - payment_rate) < 0.000001: # No significant difference - return - - # Calculate exchange difference - exchange_diff = paid_amount * (payment_rate - original_rate) - - if abs(exchange_diff) < flt(settings.exchange_difference_threshold): - return # Below threshold - - difference_type = "Gain" if exchange_diff > 0 else "Loss" - - # Create Journal Entry if auto-creation is enabled - journal_entry = None - if settings.auto_create_journal_entries: - journal_entry = create_exchange_difference_je( - tracker_doc, - abs(exchange_diff), - difference_type, - payment_doc, - f"Payment Exchange {difference_type}", - ) - - # Add exchange difference entry - tracker_doc.add_exchange_difference( - "Payment Entry", - payment_doc.name, - difference_type, - abs(exchange_diff), - payment_doc.posting_date, - f"Exchange {difference_type.lower()} on payment against PI {tracker_doc.purchase_invoice}", - journal_entry.name if journal_entry else None, - ) - - # Update payment row - payment_row.exchange_difference = exchange_diff - payment_row.journal_entry_created = 1 if journal_entry else 0 - + """Remove payment from Foreign Import Transaction when cancelled""" + tracker_name = frappe.db.get_value("Payment Entry", doc.name, "foreign_import_tracker") -def calculate_lcv_exchange_difference(tracker_doc, lcv_doc): - """Calculate exchange difference for LCV and create Journal Entry""" - settings = get_import_settings(tracker_doc.company) - - if not settings.enable_lcv_exchange_tracking: - return - - # For LCV, we calculate the impact on inventory valuation - original_rate = flt(tracker_doc.original_exchange_rate) - - # Get LCV conversion rate (if available) - lcv_rate = flt(lcv_doc.get("conversion_rate", original_rate)) - - if abs(original_rate - lcv_rate) < 0.000001: - return + if tracker_name: + try: + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) - # Calculate LCV amount in foreign currency - lcv_base_amount = flt(lcv_doc.total_taxes_and_charges) - lcv_foreign_amount = lcv_base_amount / lcv_rate + # Remove payment rows + tracker_doc.payments = [row for row in tracker_doc.payments if row.payment_entry != doc.name] - # Calculate what it would have been at original rate - original_base_amount = lcv_foreign_amount * original_rate + # Remove related exchange difference entries and cancel JEs + for diff_row in tracker_doc.exchange_differences: + if diff_row.reference_type == "Payment Entry" and diff_row.reference_name == doc.name: + if diff_row.journal_entry: + try: + je = frappe.get_doc("Journal Entry", diff_row.journal_entry) + if je.docstatus == 1: + je.cancel() + except Exception: + pass - # Exchange difference - exchange_diff = lcv_base_amount - original_base_amount + tracker_doc.exchange_differences = [ + row + for row in tracker_doc.exchange_differences + if not (row.reference_type == "Payment Entry" and row.reference_name == doc.name) + ] - if abs(exchange_diff) < flt(settings.exchange_difference_threshold): - return + tracker_doc.save() - difference_type = "Loss" if exchange_diff > 0 else "Gain" # Reversed for LCV + except Exception as e: + frappe.log_error(f"Error unlinking payment {doc.name} from tracker {tracker_name}: {str(e)}") - # Create Journal Entry - journal_entry = None - if settings.auto_create_journal_entries: - journal_entry = create_exchange_difference_je( - tracker_doc, - abs(exchange_diff), - difference_type, - lcv_doc, - f"LCV Exchange {difference_type}", - ) - # Add exchange difference entry - tracker_doc.add_exchange_difference( - "Landed Cost Voucher", - lcv_doc.name, - difference_type, - abs(exchange_diff), - lcv_doc.posting_date, - f"Exchange {difference_type.lower()} on importation costs - LCV {lcv_doc.name}", - journal_entry.name if journal_entry else None, - ) +def calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row): + """Calculate exchange difference for payment and create Journal Entry""" + settings = get_import_settings(tracker_doc.company) + + original_rate = flt(tracker_doc.original_exchange_rate) + payment_rate = flt(payment_doc.source_exchange_rate) + paid_amount = flt(payment_doc.paid_amount) + + if abs(original_rate - payment_rate) < 0.000001: # No significant difference + return + + # Calculate exchange difference + exchange_diff = paid_amount * (payment_rate - original_rate) + + if abs(exchange_diff) < flt(settings.exchange_difference_threshold): + return # Below threshold + + difference_type = "Gain" if exchange_diff > 0 else "Loss" + + # Create Journal Entry if auto-creation is enabled + journal_entry = None + if settings.auto_create_journal_entries: + journal_entry = create_exchange_difference_je( + tracker_doc, + abs(exchange_diff), + difference_type, + payment_doc, + f"Payment Exchange {difference_type}", + ) + + # Add exchange difference entry + tracker_doc.add_exchange_difference( + "Payment Entry", + payment_doc.name, + difference_type, + abs(exchange_diff), + payment_doc.posting_date, + f"Exchange {difference_type.lower()} on payment against PI {tracker_doc.purchase_invoice}", + journal_entry.name if journal_entry else None, + ) + + # Update payment row + payment_row.exchange_difference = exchange_diff + payment_row.journal_entry_created = 1 if journal_entry else 0 -def create_exchange_difference_je( - tracker_doc, amount, gain_loss_type, reference_doc, description -): - """Create Journal Entry for exchange gain/loss""" - settings = get_import_settings(tracker_doc.company) - - # Determine accounts - if gain_loss_type == "Gain": - debit_account = get_supplier_payable_account( - tracker_doc.supplier, tracker_doc.company - ) - credit_account = ( - settings.default_exchange_gain_account - or get_exchange_gain_loss_account(tracker_doc.company) - ) - else: - debit_account = ( - settings.default_exchange_loss_account - or get_exchange_gain_loss_account(tracker_doc.company) - ) - credit_account = get_supplier_payable_account( - tracker_doc.supplier, tracker_doc.company - ) - - if not debit_account or not credit_account: - frappe.throw(_("Exchange Gain/Loss accounts not configured")) - - # Create Journal Entry - je = frappe.new_doc("Journal Entry") - je.company = tracker_doc.company - je.posting_date = reference_doc.posting_date - je.voucher_type = "Exchange Gain Or Loss" - je.user_remark = f"{description} - {tracker_doc.purchase_invoice}" - je.multi_currency = 1 - - # Debit entry - je.append( - "accounts", - { - "account": debit_account, - "debit_in_account_currency": amount, - "party_type": "Supplier" if is_payable_account(debit_account) else "", - "party": tracker_doc.supplier if is_payable_account(debit_account) else "", - }, - ) - - # Credit entry - je.append( - "accounts", - { - "account": credit_account, - "credit_in_account_currency": amount, - "party_type": "Supplier" if is_payable_account(credit_account) else "", - "party": tracker_doc.supplier if is_payable_account(credit_account) else "", - }, - ) - - je.insert() - je.submit() - - return je +def calculate_lcv_exchange_difference(tracker_doc, lcv_doc): + """Calculate exchange difference for LCV and create Journal Entry""" + settings = get_import_settings(tracker_doc.company) + + if not settings.enable_lcv_exchange_tracking: + return + + # For LCV, we calculate the impact on inventory valuation + original_rate = flt(tracker_doc.original_exchange_rate) + + # Get LCV conversion rate (if available) + lcv_rate = flt(lcv_doc.get("conversion_rate", original_rate)) + + if abs(original_rate - lcv_rate) < 0.000001: + return + + # Calculate LCV amount in foreign currency + lcv_base_amount = flt(lcv_doc.total_taxes_and_charges) + lcv_foreign_amount = lcv_base_amount / lcv_rate + + # Calculate what it would have been at original rate + original_base_amount = lcv_foreign_amount * original_rate + + # Exchange difference + exchange_diff = lcv_base_amount - original_base_amount + + if abs(exchange_diff) < flt(settings.exchange_difference_threshold): + return + + difference_type = "Loss" if exchange_diff > 0 else "Gain" # Reversed for LCV + + # Create Journal Entry + journal_entry = None + if settings.auto_create_journal_entries: + journal_entry = create_exchange_difference_je( + tracker_doc, + abs(exchange_diff), + difference_type, + lcv_doc, + f"LCV Exchange {difference_type}", + ) + + # Add exchange difference entry + tracker_doc.add_exchange_difference( + "Landed Cost Voucher", + lcv_doc.name, + difference_type, + abs(exchange_diff), + lcv_doc.posting_date, + f"Exchange {difference_type.lower()} on importation costs - LCV {lcv_doc.name}", + journal_entry.name if journal_entry else None, + ) + + +def create_exchange_difference_je(tracker_doc, amount, gain_loss_type, reference_doc, description): + """Create Journal Entry for exchange gain/loss""" + settings = get_import_settings(tracker_doc.company) + + # Determine accounts + if gain_loss_type == "Gain": + debit_account = get_supplier_payable_account(tracker_doc.supplier, tracker_doc.company) + credit_account = settings.default_exchange_gain_account or get_exchange_gain_loss_account( + tracker_doc.company + ) + else: + debit_account = settings.default_exchange_loss_account or get_exchange_gain_loss_account( + tracker_doc.company + ) + credit_account = get_supplier_payable_account(tracker_doc.supplier, tracker_doc.company) + + if not debit_account or not credit_account: + frappe.throw(_("Exchange Gain/Loss accounts not configured")) + + # Create Journal Entry + je = frappe.new_doc("Journal Entry") + je.company = tracker_doc.company + je.posting_date = reference_doc.posting_date + je.voucher_type = "Exchange Gain Or Loss" + je.user_remark = f"{description} - {tracker_doc.purchase_invoice}" + je.multi_currency = 1 + + # Debit entry + je.append( + "accounts", + { + "account": debit_account, + "debit_in_account_currency": amount, + "party_type": "Supplier" if is_payable_account(debit_account) else "", + "party": tracker_doc.supplier if is_payable_account(debit_account) else "", + }, + ) + + # Credit entry + je.append( + "accounts", + { + "account": credit_account, + "credit_in_account_currency": amount, + "party_type": "Supplier" if is_payable_account(credit_account) else "", + "party": tracker_doc.supplier if is_payable_account(credit_account) else "", + }, + ) + + je.insert() + je.submit() + + return je def get_import_settings(company): - """Get Foreign Import Settings for company""" - settings = frappe.get_single("Foreign Import Settings") + """Get Foreign Import Settings for company""" + settings = frappe.get_single("Foreign Import Settings") - if not settings.company: - settings.company = company - settings.save() + if not settings.company: + settings.company = company + settings.save() - return settings + return settings def get_supplier_payable_account(supplier, company): - # Try the current ERPNext field name first - payable_account = frappe.db.get_value( - "Party Account", - {"parenttype": "Supplier", "parent": supplier, "company": company}, - "account", - ) + # Try the current ERPNext field name first + payable_account = frappe.db.get_value( + "Party Account", + {"parenttype": "Supplier", "parent": supplier, "company": company}, + "account", + ) - if not payable_account: - # Fallback: try the supplier-level field (field name varies by version) - payable_account = frappe.db.get_value( - "Supplier", supplier, "payable_account" - ) or frappe.db.get_value("Supplier", supplier, "default_payable_account") + if not payable_account: + # Fallback: try the supplier-level field (field name varies by version) + payable_account = frappe.db.get_value("Supplier", supplier, "payable_account") or frappe.db.get_value( + "Supplier", supplier, "default_payable_account" + ) - if not payable_account: - # Final fallback: get default payable account from Company - payable_account = frappe.db.get_value( - "Company", company, "default_payable_account" - ) + if not payable_account: + # Final fallback: get default payable account from Company + payable_account = frappe.db.get_value("Company", company, "default_payable_account") - return payable_account + return payable_account def get_exchange_gain_loss_account(company): - """Get exchange gain/loss account for company""" - return frappe.get_cached_value("Company", company, "exchange_gain_loss_account") + """Get exchange gain/loss account for company""" + return frappe.get_cached_value("Company", company, "exchange_gain_loss_account") def is_payable_account(account): - """Check if account is a payable account""" - account_type = frappe.get_cached_value("Account", account, "account_type") - return account_type == "Payable" + """Check if account is a payable account""" + account_type = frappe.get_cached_value("Account", account, "account_type") + return account_type == "Payable" def recalculate_import_differences(tracker_name): - """Recalculate all exchange differences for an import transaction""" - tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) + """Recalculate all exchange differences for an import transaction""" + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) - # Clear existing differences (but don't cancel JEs) - tracker_doc.exchange_differences = [] + # Clear existing differences (but don't cancel JEs) + tracker_doc.exchange_differences = [] - # Recalculate payment differences - for payment_row in tracker_doc.payments: - if payment_row.payment_entry: - payment_doc = frappe.get_doc("Payment Entry", payment_row.payment_entry) - calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row) + # Recalculate payment differences + for payment_row in tracker_doc.payments: + if payment_row.payment_entry: + payment_doc = frappe.get_doc("Payment Entry", payment_row.payment_entry) + calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row) - # Recalculate LCV differences - for lcv_row in tracker_doc.landed_cost_vouchers: - if lcv_row.landed_cost_voucher: - lcv_doc = frappe.get_doc("Landed Cost Voucher", lcv_row.landed_cost_voucher) - calculate_lcv_exchange_difference(tracker_doc, lcv_doc) + # Recalculate LCV differences + for lcv_row in tracker_doc.landed_cost_vouchers: + if lcv_row.landed_cost_voucher: + lcv_doc = frappe.get_doc("Landed Cost Voucher", lcv_row.landed_cost_voucher) + calculate_lcv_exchange_difference(tracker_doc, lcv_doc) - tracker_doc.save() - return True + tracker_doc.save() + return True def update_pending_transactions(): - """Scheduled task to update pending import transactions""" - pending_trackers = frappe.db.sql( - """ + """Scheduled task to update pending import transactions""" + pending_trackers = frappe.db.sql( + """ SELECT name FROM `tabForeign Import Transaction` WHERE status = 'Active' AND docstatus = 1 """, - as_dict=True, - ) + as_dict=True, + ) - for tracker in pending_trackers: - try: - tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) - tracker_doc.calculate_totals() - tracker_doc.set_status() - tracker_doc.save() - except Exception as e: - frappe.log_error(f"Error updating tracker {tracker.name}: {str(e)}") + for tracker in pending_trackers: + try: + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) + tracker_doc.calculate_totals() + tracker_doc.set_status() + tracker_doc.save() + except Exception as e: + frappe.log_error(f"Error updating tracker {tracker.name}: {str(e)}") @frappe.whitelist() def create_manual_exchange_entry( - tracker_name, reference_type, reference_name, difference_type, amount, remarks + tracker_name, reference_type, reference_name, difference_type, amount, remarks ): - """Create manual exchange difference entry""" - tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) - - if tracker_doc.docstatus != 1: - frappe.throw("Transaction must be submitted to add manual entries") - - amount = flt(amount) - if amount <= 0: - frappe.throw("Amount must be greater than 0") - - settings = get_import_settings(tracker_doc.company) - - journal_entry = None - if settings.auto_create_journal_entries: - reference_doc = frappe.get_doc(reference_type, reference_name) - journal_entry = create_exchange_difference_je( - tracker_doc, - amount, - difference_type, - reference_doc, - f"Manual {difference_type} Entry", - ) - - tracker_doc.add_exchange_difference( - reference_type, - reference_name, - difference_type, - amount, - nowdate(), - remarks, - journal_entry.name if journal_entry else None, - ) - - return tracker_doc.name + """Create manual exchange difference entry""" + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) + + if tracker_doc.docstatus != 1: + frappe.throw("Transaction must be submitted to add manual entries") + + amount = flt(amount) + if amount <= 0: + frappe.throw("Amount must be greater than 0") + + settings = get_import_settings(tracker_doc.company) + + journal_entry = None + if settings.auto_create_journal_entries: + reference_doc = frappe.get_doc(reference_type, reference_name) + journal_entry = create_exchange_difference_je( + tracker_doc, + amount, + difference_type, + reference_doc, + f"Manual {difference_type} Entry", + ) + + tracker_doc.add_exchange_difference( + reference_type, + reference_name, + difference_type, + amount, + nowdate(), + remarks, + journal_entry.name if journal_entry else None, + ) + + return tracker_doc.name @frappe.whitelist() def debug_payment_linking_issue(payment_entry_name): - """Debug why a payment entry is not linking to import tracker""" - try: - payment_doc = frappe.get_doc("Payment Entry", payment_entry_name) - - debug_info = { - "payment_entry": payment_entry_name, - "payment_details": { - "payment_type": payment_doc.payment_type, - "party_type": payment_doc.party_type, - "party": payment_doc.party, - "paid_to_account_currency": payment_doc.paid_to_account_currency, - "source_exchange_rate": payment_doc.source_exchange_rate, - "paid_amount": payment_doc.paid_amount, - "docstatus": payment_doc.docstatus, - }, - "issues": [], - "potential_trackers": [], - } - - # Check basic conditions - if payment_doc.payment_type != "Pay": - debug_info["issues"].append( - f"Payment type is '{payment_doc.payment_type}', should be 'Pay'" - ) - - if payment_doc.party_type != "Supplier": - debug_info["issues"].append( - f"Party type is '{payment_doc.party_type}', should be 'Supplier'" - ) - - if payment_doc.docstatus != 1: - debug_info["issues"].append( - f"Payment Entry not submitted (docstatus = {payment_doc.docstatus})" - ) - - # Find potential trackers for this supplier - if payment_doc.party_type == "Supplier": - trackers = frappe.db.sql( - """ + """Debug why a payment entry is not linking to import tracker""" + try: + payment_doc = frappe.get_doc("Payment Entry", payment_entry_name) + + debug_info = { + "payment_entry": payment_entry_name, + "payment_details": { + "payment_type": payment_doc.payment_type, + "party_type": payment_doc.party_type, + "party": payment_doc.party, + "paid_to_account_currency": payment_doc.paid_to_account_currency, + "source_exchange_rate": payment_doc.source_exchange_rate, + "paid_amount": payment_doc.paid_amount, + "docstatus": payment_doc.docstatus, + }, + "issues": [], + "potential_trackers": [], + } + + # Check basic conditions + if payment_doc.payment_type != "Pay": + debug_info["issues"].append(f"Payment type is '{payment_doc.payment_type}', should be 'Pay'") + + if payment_doc.party_type != "Supplier": + debug_info["issues"].append(f"Party type is '{payment_doc.party_type}', should be 'Supplier'") + + if payment_doc.docstatus != 1: + debug_info["issues"].append(f"Payment Entry not submitted (docstatus = {payment_doc.docstatus})") + + # Find potential trackers for this supplier + if payment_doc.party_type == "Supplier": + trackers = frappe.db.sql( + """ SELECT name, purchase_invoice, currency, original_exchange_rate, invoice_amount_foreign, status, docstatus, supplier FROM `tabForeign Import Transaction` WHERE supplier = %s ORDER BY transaction_date DESC """, - payment_doc.party, - as_dict=True, - ) - - for tracker in trackers: - tracker_info = { - "name": tracker.name, - "purchase_invoice": tracker.purchase_invoice, - "currency": tracker.currency, - "status": tracker.status, - "docstatus": tracker.docstatus, - "currency_match": payment_doc.paid_to_account_currency - == tracker.currency, - "status_ok": tracker.status in ("Active", "Draft") - and tracker.docstatus == 1, - "issues": [], - } - - if not tracker_info["currency_match"]: - tracker_info["issues"].append( - f"Currency mismatch: Payment={payment_doc.paid_to_account_currency}, Tracker={tracker.currency}" - ) - - if not tracker_info["status_ok"]: - tracker_info["issues"].append( - f"Status issue: Status={tracker.status}, Docstatus={tracker.docstatus}" - ) - - # Check if already linked - tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) - already_linked = any( - row.payment_entry == payment_doc.name - for row in tracker_doc.payments - ) - if already_linked: - tracker_info["issues"].append( - "Payment already linked to this tracker" - ) - - debug_info["potential_trackers"].append(tracker_info) - - return debug_info - - except Exception as e: - return {"error": str(e)} + payment_doc.party, + as_dict=True, + ) + + for tracker in trackers: + tracker_info = { + "name": tracker.name, + "purchase_invoice": tracker.purchase_invoice, + "currency": tracker.currency, + "status": tracker.status, + "docstatus": tracker.docstatus, + "currency_match": payment_doc.paid_to_account_currency == tracker.currency, + "status_ok": tracker.status in ("Active", "Draft") and tracker.docstatus == 1, + "issues": [], + } + + if not tracker_info["currency_match"]: + tracker_info["issues"].append( + f"Currency mismatch: Payment={payment_doc.paid_to_account_currency}, Tracker={tracker.currency}" + ) + + if not tracker_info["status_ok"]: + tracker_info["issues"].append( + f"Status issue: Status={tracker.status}, Docstatus={tracker.docstatus}" + ) + + # Check if already linked + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker.name) + already_linked = any(row.payment_entry == payment_doc.name for row in tracker_doc.payments) + if already_linked: + tracker_info["issues"].append("Payment already linked to this tracker") + + debug_info["potential_trackers"].append(tracker_info) + + return debug_info + + except Exception as e: + return {"error": str(e)} @frappe.whitelist() def manually_link_payment_to_tracker(payment_entry_name, tracker_name=None): - """Manually link a payment entry to a foreign import tracker""" - try: - payment_doc = frappe.get_doc("Payment Entry", payment_entry_name) - - if not tracker_name: - # Find the best matching tracker - trackers = frappe.db.sql( - """ + """Manually link a payment entry to a foreign import tracker""" + try: + payment_doc = frappe.get_doc("Payment Entry", payment_entry_name) + + if not tracker_name: + # Find the best matching tracker + trackers = frappe.db.sql( + """ SELECT name, currency, status, docstatus FROM `tabForeign Import Transaction` WHERE supplier = %s AND status IN ('Active', 'Draft') AND docstatus = 1 ORDER BY transaction_date DESC LIMIT 1 """, - payment_doc.party, - as_dict=True, - ) - - if not trackers: - return {"error": "No active trackers found for this supplier"} - - tracker_name = trackers[0].name - - tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) - - # Validate - if payment_doc.party != tracker_doc.supplier: - return { - "error": f"Payment party ({payment_doc.party}) doesn't match tracker supplier ({tracker_doc.supplier})" - } - - # Check if already linked - existing_payment = any( - row.payment_entry == payment_doc.name for row in tracker_doc.payments - ) - if existing_payment: - return {"error": "Payment entry is already linked to this tracker"} - - # Add payment detail - payment_row = tracker_doc.add_payment_detail(payment_doc.name) - - # Calculate and create exchange difference entry - calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row) - - # Add custom field reference - frappe.db.set_value( - "Payment Entry", - payment_doc.name, - "foreign_import_tracker", - tracker_doc.name, - ) - - return { - "success": f"Payment {payment_doc.name} successfully linked to tracker {tracker_doc.name}" - } - - except Exception as e: - frappe.log_error( - f"Error manually linking payment {payment_entry_name} to tracker {tracker_name}: {str(e)}" - ) - return {"error": str(e)} + payment_doc.party, + as_dict=True, + ) + + if not trackers: + return {"error": "No active trackers found for this supplier"} + + tracker_name = trackers[0].name + + tracker_doc = frappe.get_doc("Foreign Import Transaction", tracker_name) + + # Validate + if payment_doc.party != tracker_doc.supplier: + return { + "error": f"Payment party ({payment_doc.party}) doesn't match tracker supplier ({tracker_doc.supplier})" + } + + # Check if already linked + existing_payment = any(row.payment_entry == payment_doc.name for row in tracker_doc.payments) + if existing_payment: + return {"error": "Payment entry is already linked to this tracker"} + + # Add payment detail + payment_row = tracker_doc.add_payment_detail(payment_doc.name) + + # Calculate and create exchange difference entry + calculate_payment_exchange_difference(tracker_doc, payment_doc, payment_row) + + # Add custom field reference + frappe.db.set_value( + "Payment Entry", + payment_doc.name, + "foreign_import_tracker", + tracker_doc.name, + ) + + return {"success": f"Payment {payment_doc.name} successfully linked to tracker {tracker_doc.name}"} + + except Exception as e: + frappe.log_error( + f"Error manually linking payment {payment_entry_name} to tracker {tracker_name}: {str(e)}" + ) + return {"error": str(e)} diff --git a/csf_tz/csftz_hooks/get_relation_json.py b/csf_tz/csftz_hooks/get_relation_json.py index 8647e2f0..d83f9252 100644 --- a/csf_tz/csftz_hooks/get_relation_json.py +++ b/csf_tz/csftz_hooks/get_relation_json.py @@ -1,10 +1,11 @@ -import frappe import json -def get_json(): +import frappe - doc_list = frappe.db.sql( - """SELECT CONCAT_WS('.', "erpnext", dt.module, dt.name) as name, dt.name as doctype_name + +def get_json(): + doc_list = frappe.db.sql( + """SELECT CONCAT_WS('.', "erpnext", dt.module, dt.name) as name, dt.name as doctype_name FROM `tabDocType` dt INNER JOIN `tabDocField` df ON dt.name = df.parent WHERE df.options IS NOT NULL @@ -16,17 +17,39 @@ def get_json(): INNER JOIN `tabCustom Field` df ON dt.name = df.parent WHERE df.options IS NOT NULL AND df.fieldtype = "Link" - GROUP BY dt.module, dt.name""", as_dict=1) - - for doc in doc_list: - docfield_list = frappe.get_all("DocField", filters={"parent": doc.doctype_name, "fieldtype": "Link"}, fields="options", group_by="options") - custom_field_list = frappe.get_all("Custom Field", filters={"parent": doc.doctype_name, "fieldtype": "Link"}, fields="options", group_by="options") - doc["imports"] = [] - for options in docfield_list: - options_string = "erpnext." + frappe.get_value("DocType", doc.doctype_name, "module") + "." + options.get("options") - doc["imports"].append(options_string) - for options in custom_field_list: - options_string = "erpnext." + frappe.get_value("DocType", doc.doctype_name, "module") + "." + options.get("options") - doc["imports"].append(options_string) + GROUP BY dt.module, dt.name""", + as_dict=1, + ) + + for doc in doc_list: + docfield_list = frappe.get_all( + "DocField", + filters={"parent": doc.doctype_name, "fieldtype": "Link"}, + fields="options", + group_by="options", + ) + custom_field_list = frappe.get_all( + "Custom Field", + filters={"parent": doc.doctype_name, "fieldtype": "Link"}, + fields="options", + group_by="options", + ) + doc["imports"] = [] + for options in docfield_list: + options_string = ( + "erpnext." + + frappe.get_value("DocType", doc.doctype_name, "module") + + "." + + options.get("options") + ) + doc["imports"].append(options_string) + for options in custom_field_list: + options_string = ( + "erpnext." + + frappe.get_value("DocType", doc.doctype_name, "module") + + "." + + options.get("options") + ) + doc["imports"].append(options_string) - return json.dumps(doc_list) + return json.dumps(doc_list) diff --git a/csf_tz/csftz_hooks/get_successor_json.py b/csf_tz/csftz_hooks/get_successor_json.py index 93aeee7f..f2ed6d05 100644 --- a/csf_tz/csftz_hooks/get_successor_json.py +++ b/csf_tz/csftz_hooks/get_successor_json.py @@ -1,9 +1,11 @@ -import frappe import json +import frappe + + def get_json(main_ancestor, ancestor_type="Accounts"): - doc_list = frappe.db.sql( - """SELECT CONCAT_WS('.', "erpnext", dt.module, dt.name) as name, dt.name as doctype_name + doc_list = frappe.db.sql( + """SELECT CONCAT_WS('.', "erpnext", dt.module, dt.name) as name, dt.name as doctype_name FROM `tabDocType` dt INNER JOIN `tabDocField` df ON dt.name = df.parent WHERE df.options IS NOT NULL @@ -17,17 +19,39 @@ def get_json(main_ancestor, ancestor_type="Accounts"): WHERE df.options IS NOT NULL AND df.fieldtype = "Link" AND dt.module = ancestor_type - GROUP BY dt.module, dt.name""", as_dict=1) - - for doc in doc_list: - docfield_list = frappe.get_all("DocField", filters={"parent": doc.doctype_name, "fieldtype": "Link"}, fields="options", group_by="options") - custom_field_list = frappe.get_all("Custom Field", filters={"parent": doc.doctype_name, "fieldtype": "Link"}, fields="options", group_by="options") - doc["imports"] = [] - for options in docfield_list: - options_string = "erpnext." + frappe.get_value("DocType", doc.doctype_name, "module") + "." + options.get("options") - doc["imports"].append(options_string) - for options in custom_field_list: - options_string = "erpnext." + frappe.get_value("DocType", doc.doctype_name, "module") + "." + options.get("options") - doc["imports"].append(options_string) + GROUP BY dt.module, dt.name""", + as_dict=1, + ) + + for doc in doc_list: + docfield_list = frappe.get_all( + "DocField", + filters={"parent": doc.doctype_name, "fieldtype": "Link"}, + fields="options", + group_by="options", + ) + custom_field_list = frappe.get_all( + "Custom Field", + filters={"parent": doc.doctype_name, "fieldtype": "Link"}, + fields="options", + group_by="options", + ) + doc["imports"] = [] + for options in docfield_list: + options_string = ( + "erpnext." + + frappe.get_value("DocType", doc.doctype_name, "module") + + "." + + options.get("options") + ) + doc["imports"].append(options_string) + for options in custom_field_list: + options_string = ( + "erpnext." + + frappe.get_value("DocType", doc.doctype_name, "module") + + "." + + options.get("options") + ) + doc["imports"].append(options_string) - return json.dumps(doc_list) + return json.dumps(doc_list) diff --git a/csf_tz/csftz_hooks/item_reposting.py b/csf_tz/csftz_hooks/item_reposting.py index 1ca799aa..439bf8b8 100644 --- a/csf_tz/csftz_hooks/item_reposting.py +++ b/csf_tz/csftz_hooks/item_reposting.py @@ -1,22 +1,36 @@ import frappe -from frappe import _ -from frappe.utils import getdate, get_time, today -from erpnext.stock.stock_ledger import update_entries_after from erpnext.accounts.utils import update_gl_entries_after +from erpnext.stock.stock_ledger import update_entries_after +from frappe import _ +from frappe.utils import today from frappe.utils.background_jobs import enqueue + def execute(): - for doctype in ('repost_item_valuation', 'stock_entry_detail', 'purchase_receipt_item', - 'purchase_invoice_item', 'delivery_note_item', 'sales_invoice_item', 'packed_item'): - frappe.reload_doc('stock', 'doctype', doctype) - frappe.reload_doc('buying', 'doctype', 'purchase_receipt_item_supplied') + for doctype in ( + "repost_item_valuation", + "stock_entry_detail", + "purchase_receipt_item", + "purchase_invoice_item", + "delivery_note_item", + "sales_invoice_item", + "packed_item", + ): + frappe.reload_doc("stock", "doctype", doctype) + frappe.reload_doc("buying", "doctype", "purchase_receipt_item_supplied") - sle_gle_reposting_start_date = frappe.get_value("CSF TZ Settings", "CSF TZ Settings", "sle_gle_reposting_start_date") + sle_gle_reposting_start_date = frappe.get_value( + "CSF TZ Settings", "CSF TZ Settings", "sle_gle_reposting_start_date" + ) if not sle_gle_reposting_start_date: - frappe.throw(_("SLE GLE Reposting Start Date not set in {0}").format(frappe.utils.get_url_to_form("CSF TZ Settings", "CSF TZ Settings"))) + frappe.throw( + _("SLE GLE Reposting Start Date not set in {0}").format( + frappe.utils.get_url_to_form("CSF TZ Settings", "CSF TZ Settings") + ) + ) reposting_project_deployed_on = sle_gle_reposting_start_date + " 00:00:00" posting_date = sle_gle_reposting_start_date - posting_time = '00:00:00' + posting_time = "00:00:00" if posting_date == today(): return @@ -26,7 +40,8 @@ def execute(): company_list = [] - data = frappe.db.sql(''' + data = frappe.db.sql( + """ SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company FROM @@ -35,7 +50,10 @@ def execute(): creation > %s and is_cancelled = 0 ORDER BY timestamp(posting_date, posting_time) asc, creation asc - ''', reposting_project_deployed_on, as_dict=1) + """, + reposting_project_deployed_on, + as_dict=1, + ) frappe.db.auto_commit_on_many_writes = 1 print("Reposting Stock Ledger Entries...") @@ -45,39 +63,51 @@ def execute(): if d.company not in company_list: company_list.append(d.company) - update_entries_after({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": d.posting_date, - "posting_time": d.posting_time, - "voucher_type": d.voucher_type, - "voucher_no": d.voucher_no, - "sle_id": d.name - }, allow_negative_stock=True) + update_entries_after( + { + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": d.posting_date, + "posting_time": d.posting_time, + "voucher_type": d.voucher_type, + "voucher_no": d.voucher_no, + "sle_id": d.name, + }, + allow_negative_stock=True, + ) i += 1 - if i%100 == 0: + if i % 100 == 0: print(i, "/", total_sle) - print("Reposting General Ledger Entries...") if data: - for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): + for row in frappe.get_all("Company", filters={"enable_perpetual_inventory": 1}): if row.name in company_list: update_gl_entries_after(posting_date, posting_time, company=row.name) frappe.db.auto_commit_on_many_writes = 0 + def get_creation_time(): - return frappe.db.sql(''' SELECT create_time FROM - INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0] + return frappe.db.sql( + """ SELECT create_time FROM + INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" """, + as_list=1, + )[0][0] + @frappe.whitelist() def enqueue_reposting_sle_gle(): - sle_gle_reposting_start_date = frappe.get_value("CSF TZ Settings", "CSF TZ Settings", "sle_gle_reposting_start_date") + sle_gle_reposting_start_date = frappe.get_value( + "CSF TZ Settings", "CSF TZ Settings", "sle_gle_reposting_start_date" + ) if not sle_gle_reposting_start_date: - frappe.throw(_("SLE GLE Reposting Start Date not set in {0}").format(frappe.utils.get_url_to_form("CSF TZ Settings", "CSF TZ Settings"))) + frappe.throw( + _("SLE GLE Reposting Start Date not set in {0}").format( + frappe.utils.get_url_to_form("CSF TZ Settings", "CSF TZ Settings") + ) + ) frappe.msgprint(_("Reposting of SLE and GLE started"), alert=True) - enqueue(method=execute, - queue='long', timeout=10000, is_async=True) + enqueue(method=execute, queue="long", timeout=10000, is_async=True) diff --git a/csf_tz/csftz_hooks/items_revaluation.py b/csf_tz/csftz_hooks/items_revaluation.py index d43b5da9..826e5966 100644 --- a/csf_tz/csftz_hooks/items_revaluation.py +++ b/csf_tz/csftz_hooks/items_revaluation.py @@ -1,88 +1,85 @@ import frappe -from frappe import _ from frappe.utils import flt @frappe.whitelist() def get_data(filters): - filters = frappe._dict(filters) - data = get_stock_ledger_entries(filters) - itewise_balance_qty = {} + filters = frappe._dict(filters) + data = get_stock_ledger_entries(filters) + itewise_balance_qty = {} - for row in data: - key = (row.item_code, row.warehouse) - itewise_balance_qty.setdefault(key, []).append(row) + for row in data: + key = (row.item_code, row.warehouse) + itewise_balance_qty.setdefault(key, []).append(row) - res = validate_data(itewise_balance_qty) - return res + res = validate_data(itewise_balance_qty) + return res def validate_data(itewise_balance_qty): - res = [] - for key, data in itewise_balance_qty.items(): - row = get_incorrect_data(data) - if row: - res.append(row) + res = [] + for _key, data in itewise_balance_qty.items(): + row = get_incorrect_data(data) + if row: + res.append(row) - return res + return res def get_incorrect_data(data): - balance_qty = 0.0 - for row in data: - balance_qty += row.actual_qty - if row.voucher_type == "Stock Reconciliation" and not row.batch_no: - balance_qty = flt(row.qty_after_transaction) + balance_qty = 0.0 + for row in data: + balance_qty += row.actual_qty + if row.voucher_type == "Stock Reconciliation" and not row.batch_no: + balance_qty = flt(row.qty_after_transaction) - row.expected_balance_qty = balance_qty - if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5: - row.differnce = abs( - flt(row.expected_balance_qty) - flt(row.qty_after_transaction) - ) - return row + row.expected_balance_qty = balance_qty + if abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) > 0.5: + row.differnce = abs(flt(row.expected_balance_qty) - flt(row.qty_after_transaction)) + return row def get_stock_ledger_entries(report_filters): - filters = {"is_cancelled": 0} - fields = [ - "name", - "voucher_type", - "voucher_no", - "item_code", - "actual_qty", - "posting_date", - "posting_time", - "company", - "warehouse", - "qty_after_transaction", - "batch_no", - ] - - for field in ["warehouse", "item_code", "company"]: - if report_filters.get(field): - filters[field] = report_filters.get(field) - - return frappe.get_all( - "Stock Ledger Entry", - fields=fields, - filters=filters, - order_by="timestamp(posting_date, posting_time) asc, creation asc", - ) + filters = {"is_cancelled": 0} + fields = [ + "name", + "voucher_type", + "voucher_no", + "item_code", + "actual_qty", + "posting_date", + "posting_time", + "company", + "warehouse", + "qty_after_transaction", + "batch_no", + ] + + for field in ["warehouse", "item_code", "company"]: + if report_filters.get(field): + filters[field] = report_filters.get(field) + + return frappe.get_all( + "Stock Ledger Entry", + fields=fields, + filters=filters, + order_by="timestamp(posting_date, posting_time) asc, creation asc", + ) def process_incorrect_balance_qty(): - data = get_data({}) - if len(data) > 0: - rec = frappe._dict(data[0]) - doc = frappe.new_doc("Repost Item Valuation") - doc.based_on = "Transaction" - doc.voucher_type = rec.voucher_type - doc.voucher_no = rec.voucher_no - doc.posting_date = rec.posting_date - doc.posting_time = rec.posting_time - doc.company = rec.company - doc.warehouse = rec.warehouse - doc.allow_negative_stock = 1 - doc.docstatus = 1 - doc.insert(ignore_permissions=True) - frappe.db.commit() + data = get_data({}) + if len(data) > 0: + rec = frappe._dict(data[0]) + doc = frappe.new_doc("Repost Item Valuation") + doc.based_on = "Transaction" + doc.voucher_type = rec.voucher_type + doc.voucher_no = rec.voucher_no + doc.posting_date = rec.posting_date + doc.posting_time = rec.posting_time + doc.company = rec.company + doc.warehouse = rec.warehouse + doc.allow_negative_stock = 1 + doc.docstatus = 1 + doc.insert(ignore_permissions=True) + frappe.db.commit() diff --git a/csf_tz/csftz_hooks/landed_cost_voucher.py b/csf_tz/csftz_hooks/landed_cost_voucher.py index 2b72628c..3f52543d 100644 --- a/csf_tz/csftz_hooks/landed_cost_voucher.py +++ b/csf_tz/csftz_hooks/landed_cost_voucher.py @@ -1,46 +1,48 @@ -from __future__ import unicode_literals import frappe -from frappe import _ -import frappe -import os -from frappe.utils.background_jobs import enqueue -from frappe.utils.pdf import get_pdf, cleanup -from csf_tz import console @frappe.whitelist() def get_landed_cost_expenses(import_file=None): - if not import_file: - return + if not import_file: + return - je_landed_cost = frappe.db.sql("""SELECT jea.account as 'expense_account', je.title as 'description', jea.debit as 'amount' + je_landed_cost = frappe.db.sql( + """SELECT jea.account as 'expense_account', je.title as 'description', jea.debit as 'amount' FROM `tabJournal Entry` je INNER JOIN `tabJournal Entry Account` jea ON jea.parent = je.name WHERE je.import_file = %s AND je.docstatus = 1 - AND jea.debit > 0""", import_file, as_dict=1) - # frappe.db.sql("""update `tabSales Order Item` set delivered_qty = 0 - # where parent = %s""", so.name) - pinv_landed_cost = frappe.db.sql("""SELECT pii.expense_account as 'expense_account', pi.title as 'description', pii.base_net_amount as 'amount' + AND jea.debit > 0""", + import_file, + as_dict=1, + ) + # frappe.db.sql("""update `tabSales Order Item` set delivered_qty = 0 + # where parent = %s""", so.name) + pinv_landed_cost = frappe.db.sql( + """SELECT pii.expense_account as 'expense_account', pi.title as 'description', pii.base_net_amount as 'amount' FROM `tabPurchase Invoice` pi INNER JOIN `tabPurchase Invoice Item` pii ON pii.parent = pi.name WHERE pi.import_file = %s - AND pi.docstatus = 1;""", import_file, as_dict=1) - return je_landed_cost + pinv_landed_cost + AND pi.docstatus = 1;""", + import_file, + as_dict=1, + ) + return je_landed_cost + pinv_landed_cost + def total_amount(doc, method): - for item in doc.items: - if item.amount and item.applicable_charges: - item.custom_total_amount = item.amount + item.applicable_charges - else: - item.custom_total_amount = 0 - - if doc.items: - grand_total = 0 - for item in doc.items: - grand_total += item.custom_total_amount or 0 - doc.custom_grand_total = grand_total - else: - doc.custom_grand_total = 0 \ No newline at end of file + for item in doc.items: + if item.amount and item.applicable_charges: + item.custom_total_amount = item.amount + item.applicable_charges + else: + item.custom_total_amount = 0 + + if doc.items: + grand_total = 0 + for item in doc.items: + grand_total += item.custom_total_amount or 0 + doc.custom_grand_total = grand_total + else: + doc.custom_grand_total = 0 diff --git a/csf_tz/csftz_hooks/leave_encashment.py b/csf_tz/csftz_hooks/leave_encashment.py index 49eb6347..27981e22 100644 --- a/csf_tz/csftz_hooks/leave_encashment.py +++ b/csf_tz/csftz_hooks/leave_encashment.py @@ -1,186 +1,177 @@ import frappe -from frappe import _, bold +from frappe import _ from frappe.utils import cint, flt from hrms.hr.doctype.leave_encashment.leave_encashment import ( - LeaveEncashment as HRMSLeaveEncashment, + LeaveEncashment as HRMSLeaveEncashment, ) def validate_flags(doc, method=None): - """Validate and auto-set is_deduction/is_earning flags.""" - if not _has_flag_fields(doc): - return + """Validate and auto-set is_deduction/is_earning flags.""" + if not _has_flag_fields(doc): + return - doc.is_deduction = cint(doc.is_deduction) - doc.is_earning = cint(doc.is_earning) + doc.is_deduction = cint(doc.is_deduction) + doc.is_earning = cint(doc.is_earning) - _auto_select_flags(doc) - _ensure_valid_selection(doc) + _auto_select_flags(doc) + _ensure_valid_selection(doc) def ensure_selection_before_submit(doc, method=None): - validate_flags(doc) + validate_flags(doc) - if not (getattr(doc, "is_deduction", 0) or getattr(doc, "is_earning", 0)): - frappe.throw( - _("Please select either Is Deduction or Is Earning before submitting.") - ) + if not (getattr(doc, "is_deduction", 0) or getattr(doc, "is_earning", 0)): + frappe.throw(_("Please select either Is Deduction or Is Earning before submitting.")) def _has_flag_fields(doc): - return hasattr(doc, "is_deduction") and hasattr(doc, "is_earning") + return hasattr(doc, "is_deduction") and hasattr(doc, "is_earning") def _auto_select_flags(doc): - days = flt(getattr(doc, "encashment_days", 0)) - amount = flt(getattr(doc, "encashment_amount", 0)) + days = flt(getattr(doc, "encashment_days", 0)) + amount = flt(getattr(doc, "encashment_amount", 0)) - if days < 0 or (not days and amount < 0): - doc.is_deduction = 1 - doc.is_earning = 0 - elif days > 0 or amount > 0: - doc.is_deduction = 0 - doc.is_earning = 1 + if days < 0 or (not days and amount < 0): + doc.is_deduction = 1 + doc.is_earning = 0 + elif days > 0 or amount > 0: + doc.is_deduction = 0 + doc.is_earning = 1 def _ensure_valid_selection(doc): - if getattr(doc, "is_deduction", 0) and getattr(doc, "is_earning", 0): - frappe.throw(_("Select either Is Deduction or Is Earning, not both.")) + if getattr(doc, "is_deduction", 0) and getattr(doc, "is_earning", 0): + frappe.throw(_("Select either Is Deduction or Is Earning, not both.")) def _get_salary_component(doc, purpose): - if purpose == "deduction": - doc_fields = ["deduction_salary_component", "salary_component_deduction"] - leave_type_fields = [ - "deduction_component", - "deduction_salary_component", - "leave_encashment_deduction_component", - ] - else: - doc_fields = ["earning_salary_component", "salary_component_earning"] - leave_type_fields = ["earning_salary_component", "earning_component"] - - component = _get_value_from_fields(doc, doc_fields) - if component: - return component, _("Leave Encashment") - - component = _get_leave_type_value(doc.leave_type, leave_type_fields) - if component: - return component, _("Leave Type {0}").format(doc.leave_type) - - source = ( - _("Leave Encashment") - if _has_any_field(doc, doc_fields) - else _("Leave Type {0}").format(doc.leave_type) - ) - return None, source + if purpose == "deduction": + doc_fields = ["deduction_salary_component", "salary_component_deduction"] + leave_type_fields = [ + "deduction_component", + "deduction_salary_component", + "leave_encashment_deduction_component", + ] + else: + doc_fields = ["earning_salary_component", "salary_component_earning"] + leave_type_fields = ["earning_salary_component", "earning_component"] + + component = _get_value_from_fields(doc, doc_fields) + if component: + return component, _("Leave Encashment") + + component = _get_leave_type_value(doc.leave_type, leave_type_fields) + if component: + return component, _("Leave Type {0}").format(doc.leave_type) + + source = ( + _("Leave Encashment") + if _has_any_field(doc, doc_fields) + else _("Leave Type {0}").format(doc.leave_type) + ) + return None, source def _get_value_from_fields(doc, fieldnames): - """Get first non-empty value from doc fields.""" - for field in fieldnames: - if hasattr(doc, field): - value = getattr(doc, field) - if value: - return value - return None + """Get first non-empty value from doc fields.""" + for field in fieldnames: + if hasattr(doc, field): + value = getattr(doc, field) + if value: + return value + return None def _get_leave_type_value(leave_type, fieldnames): - """Get first non-empty value from leave type fields.""" - for field in fieldnames: - if frappe.db.has_column("Leave Type", field): - value = frappe.db.get_value("Leave Type", leave_type, field) - if value: - return value - return None + """Get first non-empty value from leave type fields.""" + for field in fieldnames: + if frappe.db.has_column("Leave Type", field): + value = frappe.db.get_value("Leave Type", leave_type, field) + if value: + return value + return None def _has_any_field(doc, fieldnames): - """Check if doc has any of the specified fields.""" - return any(hasattr(doc, field) for field in fieldnames) + """Check if doc has any of the specified fields.""" + return any(hasattr(doc, field) for field in fieldnames) _original_before_submit = HRMSLeaveEncashment.before_submit def _custom_before_submit(self): - """Custom before_submit to allow negative amounts for deductions.""" - if self.encashment_amount is None: - frappe.throw(_("Encashment amount is required")) + """Custom before_submit to allow negative amounts for deductions.""" + if self.encashment_amount is None: + frappe.throw(_("Encashment amount is required")) - amount = flt(self.encashment_amount) + amount = flt(self.encashment_amount) - if _has_flag_fields(self): - ensure_selection_before_submit(self) + if _has_flag_fields(self): + ensure_selection_before_submit(self) - if self.is_deduction and amount < 0: - return + if self.is_deduction and amount < 0: + return - # Allow positive amounts for earnings - if self.is_earning and amount > 0: - # Call original validation for positive amounts - return _original_before_submit(self) + # Allow positive amounts for earnings + if self.is_earning and amount > 0: + # Call original validation for positive amounts + return _original_before_submit(self) - # Zero or mismatched amounts - if ( - amount == 0 - or (self.is_deduction and amount > 0) - or (self.is_earning and amount < 0) - ): - frappe.throw(_("Invalid amount for selected encashment type")) - else: - # Standard behavior - only positive amounts - return _original_before_submit(self) + # Zero or mismatched amounts + if amount == 0 or (self.is_deduction and amount > 0) or (self.is_earning and amount < 0): + frappe.throw(_("Invalid amount for selected encashment type")) + else: + # Standard behavior - only positive amounts + return _original_before_submit(self) def _custom_on_submit(self): - """Custom on_submit to handle deductions and earnings.""" - if not self.leave_allocation: - self.db_set("leave_allocation", self.get_leave_allocation().get("name")) + """Custom on_submit to handle deductions and earnings.""" + if not self.leave_allocation: + self.db_set("leave_allocation", self.get_leave_allocation().get("name")) - if self.pay_via_payment_entry: - self.create_gl_entries() - else: - if _has_flag_fields(self): - _create_custom_additional_salary(self) - else: - self.create_additional_salary() + if self.pay_via_payment_entry: + self.create_gl_entries() + else: + if _has_flag_fields(self): + _create_custom_additional_salary(self) + else: + self.create_additional_salary() - self.set_encashed_leaves_in_allocation() - self.create_leave_ledger_entry() + self.set_encashed_leaves_in_allocation() + self.create_leave_ledger_entry() def _create_custom_additional_salary(doc): - """Create additional salary for deduction or earning.""" - is_deduction = cint(getattr(doc, "is_deduction", 0)) - component_type = "deduction" if is_deduction else "earning" - - # Get salary component - component, source = _get_salary_component(doc, component_type) - if not component: - frappe.throw( - _( - "Please set a {0} Component on Leave Type {1} " - "or specify it on the Leave Encashment." - ).format(_("Deduction") if is_deduction else _("Earning"), doc.leave_type) - ) - - # Create additional salary - additional_salary = frappe.new_doc("Additional Salary") - additional_salary.company = doc.company or frappe.get_value( - "Employee", doc.employee, "company" - ) - additional_salary.employee = doc.employee - additional_salary.currency = doc.currency - additional_salary.salary_component = component - additional_salary.type = "Deduction" if is_deduction else "Earning" - additional_salary.payroll_date = doc.encashment_date - additional_salary.amount = abs(flt(doc.encashment_amount)) - additional_salary.overwrite_salary_structure_amount = 0 - additional_salary.ref_doctype = doc.doctype - additional_salary.ref_docname = doc.name - additional_salary.submit() - - doc.additional_salary = additional_salary.name - doc.db_set("additional_salary", additional_salary.name) + """Create additional salary for deduction or earning.""" + is_deduction = cint(getattr(doc, "is_deduction", 0)) + component_type = "deduction" if is_deduction else "earning" + + # Get salary component + component, source = _get_salary_component(doc, component_type) + if not component: + frappe.throw( + _("Please set a {0} Component on Leave Type {1} or specify it on the Leave Encashment.").format( + _("Deduction") if is_deduction else _("Earning"), doc.leave_type + ) + ) + + # Create additional salary + additional_salary = frappe.new_doc("Additional Salary") + additional_salary.company = doc.company or frappe.get_value("Employee", doc.employee, "company") + additional_salary.employee = doc.employee + additional_salary.currency = doc.currency + additional_salary.salary_component = component + additional_salary.type = "Deduction" if is_deduction else "Earning" + additional_salary.payroll_date = doc.encashment_date + additional_salary.amount = abs(flt(doc.encashment_amount)) + additional_salary.overwrite_salary_structure_amount = 0 + additional_salary.ref_doctype = doc.doctype + additional_salary.ref_docname = doc.name + additional_salary.submit() + + doc.additional_salary = additional_salary.name + doc.db_set("additional_salary", additional_salary.name) diff --git a/csf_tz/csftz_hooks/material_request.py b/csf_tz/csftz_hooks/material_request.py index 6d13e995..0ebfab49 100644 --- a/csf_tz/csftz_hooks/material_request.py +++ b/csf_tz/csftz_hooks/material_request.py @@ -1,67 +1,59 @@ import frappe from frappe.query_builder import DocType +from frappe.utils import add_days, create_batch, nowdate from frappe.utils.background_jobs import enqueue -from frappe.utils import add_days, nowdate, create_batch - cp = DocType("Company") mr = DocType("Material Request") -def _auto_close_material_request_batch(material_request_names): - for name in material_request_names: - try: - material_request_doc = frappe.get_doc("Material Request", name) - material_request_doc.update_status("Stopped") - except Exception: - frappe.log_error(frappe.get_traceback(), f"Auto Close Material Request Error: {name}") +def _auto_close_material_request_batch(material_request_names): + for name in material_request_names: + try: + material_request_doc = frappe.get_doc("Material Request", name) + material_request_doc.update_status("Stopped") + except Exception: + frappe.log_error(frappe.get_traceback(), f"Auto Close Material Request Error: {name}") def auto_close_material_request(): - """ - Auto close Material Request based on settings specified on Company under section of stock settings - """ - - def close_request_docs(date_before, row): - material_requests = ( - frappe.qb.from_(mr) - .select( - mr.name - ) - .where( - (mr.docstatus == 1) - & (mr.company == row.name) - & (mr.status != "Stopped") - & (mr.transaction_date <= date_before) - ) - ).run(as_dict=True) - - if len(material_requests) == 0: - return - - for records in create_batch(material_requests, 100): - enqueue( - _auto_close_material_request_batch, - queue="long", - timeout=1200, - job_name=f"auto_close_material_request_{row.name}_{records[0].name}", - kwargs={"material_request_names": [record.name for record in records]}, - ) - - company_details = ( - frappe.qb.from_(cp) - .select( - cp.name, - cp.close_material_request_after - ) - .where( - cp.enable_auto_close_material_request == 1 - ) - ).run(as_dict=True) - - if len(company_details) == 0: - return - - for row in company_details: - before_days = add_days(nowdate(), -row.close_material_request_after) - close_request_docs(before_days, row) + """ + Auto close Material Request based on settings specified on Company under section of stock settings + """ + + def close_request_docs(date_before, row): + material_requests = ( + frappe.qb.from_(mr) + .select(mr.name) + .where( + (mr.docstatus == 1) + & (mr.company == row.name) + & (mr.status != "Stopped") + & (mr.transaction_date <= date_before) + ) + ).run(as_dict=True) + + if len(material_requests) == 0: + return + + for records in create_batch(material_requests, 100): + enqueue( + _auto_close_material_request_batch, + queue="long", + timeout=1200, + job_name=f"auto_close_material_request_{row.name}_{records[0].name}", + kwargs={"material_request_names": [record.name for record in records]}, + ) + + company_details = ( + frappe.qb.from_(cp) + .select(cp.name, cp.close_material_request_after) + .where(cp.enable_auto_close_material_request == 1) + ).run(as_dict=True) + + if len(company_details) == 0: + return + + for row in company_details: + before_days = add_days(nowdate(), -row.close_material_request_after) + close_request_docs(before_days, row) diff --git a/csf_tz/csftz_hooks/payment_entry.py b/csf_tz/csftz_hooks/payment_entry.py index 152b4c45..77f78f6a 100644 --- a/csf_tz/csftz_hooks/payment_entry.py +++ b/csf_tz/csftz_hooks/payment_entry.py @@ -1,234 +1,224 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2021, Aakvatech Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe import json -from frappe import _ -from frappe.utils import nowdate, getdate -from erpnext.accounts.utils import get_outstanding_invoices, get_account_currency -from erpnext.setup.utils import get_exchange_rate -from erpnext.controllers.accounts_controller import get_supplier_block_status + +import frappe from erpnext.accounts.doctype.payment_entry.payment_entry import ( - get_orders_to_be_billed, - get_negative_outstanding_invoices, + get_negative_outstanding_invoices, + get_orders_to_be_billed, ) -from frappe import ValidationError, _, qb, scrub, throw +from erpnext.accounts.utils import get_account_currency, get_outstanding_invoices +from erpnext.controllers.accounts_controller import get_supplier_block_status +from erpnext.setup.utils import get_exchange_rate +from frappe import _, qb +from frappe.utils import getdate, nowdate @frappe.whitelist() def get_outstanding_reference_documents(args): - # Check if the feature is disabled in CSF TZ Settings - if frappe.db.get_single_value("CSF TZ Settings", "disable_get_outstanding_functionality"): - return [] - - if isinstance(args, str): - args = json.loads(args) - - if args.get("party_type") == "Member": - return - - ple = qb.DocType("Payment Ledger Entry") - common_filter = [] - accounting_dimensions_filter = [] - posting_and_due_date = [] - - # confirm that Supplier is not blocked - if args.get("party_type") == "Supplier": - supplier_status = get_supplier_block_status(args["party"]) - if supplier_status["on_hold"]: - if supplier_status["hold_type"] == "All": - return [] - elif supplier_status["hold_type"] == "Payments": - if ( - not supplier_status["release_date"] - or getdate(nowdate()) <= supplier_status["release_date"] - ): - return [] - - party_account_currency = get_account_currency(args.get("party_account")) - company_currency = frappe.get_cached_value( - "Company", args.get("company"), "default_currency" - ) - - # Get positive outstanding sales /purchase invoices - condition = "" - if args.get("voucher_type") and args.get("voucher_no"): - condition = " and voucher_type={0} and voucher_no={1}".format( - frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]) - ) - common_filter.append(ple.voucher_type == args["voucher_type"]) - common_filter.append(ple.voucher_no == args["voucher_no"]) - - # Add cost center condition - # if args.get("cost_center"): - # condition += " and cost_center='%s'" % args.get("cost_center") - # accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) - - date_fields_dict = { - "posting_date": ["from_posting_date", "to_posting_date"], - "due_date": ["from_due_date", "to_due_date"], - } - - for fieldname, date_fields in date_fields_dict.items(): - if args.get(date_fields[0]) and args.get(date_fields[1]): - condition += " and {0} between '{1}' and '{2}'".format( - fieldname, args.get(date_fields[0]), args.get(date_fields[1]) - ) - posting_and_due_date.append( - ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])] - ) - - if args.get("company"): - condition += " and company = {0}".format(frappe.db.escape(args.get("company"))) - common_filter.append(ple.company == args.get("company")) - - party_account = [args.get("party_account")] - outstanding_invoices = get_outstanding_invoices( - args.get("party_type"), - args.get("party"), - party_account, - common_filter=common_filter, - posting_date=posting_and_due_date, - min_outstanding=args.get("outstanding_amt_greater_than"), - max_outstanding=args.get("outstanding_amt_less_than"), - accounting_dimensions=accounting_dimensions_filter, - ) - from erpnext.accounts.doctype.payment_entry.payment_entry import ( - split_invoices_based_on_payment_terms, - ) - - outstanding_invoices = split_invoices_based_on_payment_terms( - outstanding_invoices, args.get("company") - ) - - for d in outstanding_invoices: - d["exchange_rate"] = 1 - d["posting_date"] = frappe.db.get_value( - d.voucher_type, d.voucher_no, "posting_date" - ) - if party_account_currency != company_currency: - if d.voucher_type in frappe.get_hooks("invoice_doctypes"): - d["exchange_rate"] = frappe.db.get_value( - d.voucher_type, d.voucher_no, "conversion_rate" - ) - elif d.voucher_type == "Journal Entry": - d["exchange_rate"] = get_exchange_rate( - party_account_currency, company_currency, d.posting_date - ) - if d.voucher_type in ("Purchase Invoice"): - d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") - - # Get all SO / PO which are not fully billed or against which full advance not paid - orders_to_be_billed = [] - # orders_to_be_billed = get_orders_to_be_billed( - # args.get("posting_date"), - # args.get("party_type"), - # args.get("party"), - # args.get("company"), - # party_account_currency, - # company_currency, - # filters=args, - # ) - - # Get negative outstanding sales /purchase invoices - negative_outstanding_invoices = [] - if args.get("party_type") != "Employee" and not args.get("voucher_no"): - negative_outstanding_invoices = get_negative_outstanding_invoices( - args.get("party_type"), - args.get("party"), - args.get("party_account"), - party_account_currency, - company_currency, - condition=condition, - ) - - data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed - - if not data: - frappe.msgprint( - _( - "No outstanding invoices found for the {0} {1} which qualify the filters you have specified." - ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))) - ) - - return data + # Check if the feature is disabled in CSF TZ Settings + if frappe.db.get_single_value("CSF TZ Settings", "disable_get_outstanding_functionality"): + return [] + + if isinstance(args, str): + args = json.loads(args) + + if args.get("party_type") == "Member": + return + + ple = qb.DocType("Payment Ledger Entry") + common_filter = [] + accounting_dimensions_filter = [] + posting_and_due_date = [] + + # confirm that Supplier is not blocked + if args.get("party_type") == "Supplier": + supplier_status = get_supplier_block_status(args["party"]) + if supplier_status["on_hold"]: + if supplier_status["hold_type"] == "All": + return [] + elif supplier_status["hold_type"] == "Payments": + if ( + not supplier_status["release_date"] + or getdate(nowdate()) <= supplier_status["release_date"] + ): + return [] + + party_account_currency = get_account_currency(args.get("party_account")) + company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency") + + # Get positive outstanding sales /purchase invoices + condition = "" + if args.get("voucher_type") and args.get("voucher_no"): + condition = " and voucher_type={} and voucher_no={}".format( + frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]) + ) + common_filter.append(ple.voucher_type == args["voucher_type"]) + common_filter.append(ple.voucher_no == args["voucher_no"]) + + # Add cost center condition + # if args.get("cost_center"): + # condition += " and cost_center='%s'" % args.get("cost_center") + # accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center")) + + date_fields_dict = { + "posting_date": ["from_posting_date", "to_posting_date"], + "due_date": ["from_due_date", "to_due_date"], + } + + for fieldname, date_fields in date_fields_dict.items(): + if args.get(date_fields[0]) and args.get(date_fields[1]): + condition += ( + f" and {fieldname} between '{args.get(date_fields[0])}' and '{args.get(date_fields[1])}'" + ) + posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])]) + + if args.get("company"): + condition += " and company = {}".format(frappe.db.escape(args.get("company"))) + common_filter.append(ple.company == args.get("company")) + + party_account = [args.get("party_account")] + outstanding_invoices = get_outstanding_invoices( + args.get("party_type"), + args.get("party"), + party_account, + common_filter=common_filter, + posting_date=posting_and_due_date, + min_outstanding=args.get("outstanding_amt_greater_than"), + max_outstanding=args.get("outstanding_amt_less_than"), + accounting_dimensions=accounting_dimensions_filter, + ) + from erpnext.accounts.doctype.payment_entry.payment_entry import ( + split_invoices_based_on_payment_terms, + ) + + outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices, args.get("company")) + + for d in outstanding_invoices: + d["exchange_rate"] = 1 + d["posting_date"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "posting_date") + if party_account_currency != company_currency: + if d.voucher_type in frappe.get_hooks("invoice_doctypes"): + d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate") + elif d.voucher_type == "Journal Entry": + d["exchange_rate"] = get_exchange_rate( + party_account_currency, company_currency, d.posting_date + ) + if d.voucher_type in ("Purchase Invoice"): + d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no") + + # Get all SO / PO which are not fully billed or against which full advance not paid + orders_to_be_billed = [] + # orders_to_be_billed = get_orders_to_be_billed( + # args.get("posting_date"), + # args.get("party_type"), + # args.get("party"), + # args.get("company"), + # party_account_currency, + # company_currency, + # filters=args, + # ) + + # Get negative outstanding sales /purchase invoices + negative_outstanding_invoices = [] + if args.get("party_type") != "Employee" and not args.get("voucher_no"): + negative_outstanding_invoices = get_negative_outstanding_invoices( + args.get("party_type"), + args.get("party"), + args.get("party_account"), + party_account_currency, + company_currency, + condition=condition, + ) + + data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed + + if not data: + frappe.msgprint( + _( + "No outstanding invoices found for the {0} {1} which qualify the filters you have specified." + ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))) + ) + + return data + @frappe.whitelist() def get_outstanding_sales_orders(args): - if isinstance(args, str): - args = json.loads(args) - - if args.get("party_type") == "Member": - return - - if args.get("party_type") != "Customer": - frappe.throw(_("Sales Orders can only be fetched for Customer")) - - # confirm that Supplier is not blocked (not needed for Customer, but keeping for consistency) - if args.get("party_type") == "Supplier": - supplier_status = get_supplier_block_status(args["party"]) - if supplier_status["on_hold"]: - if supplier_status["hold_type"] == "All": - return [] - elif supplier_status["hold_type"] == "Payments": - if ( - not supplier_status["release_date"] - or getdate(nowdate()) <= supplier_status["release_date"] - ): - return [] - - party_account_currency = get_account_currency(args.get("party_account")) - company_currency = frappe.get_cached_value( - "Company", args.get("company"), "default_currency" - ) - - # Get all SO which are not fully billed or against which full advance not paid - orders_to_be_billed = get_orders_to_be_billed( - args.get("posting_date"), - args.get("party_type"), - args.get("party"), - args.get("company"), - party_account_currency, - company_currency, - filters=args, - ) - - for d in orders_to_be_billed: - d["exchange_rate"] = 1 - if party_account_currency != company_currency: - if d.voucher_type in frappe.get_hooks("invoice_doctypes"): - d["exchange_rate"] = frappe.db.get_value( - d.voucher_type, d.voucher_no, "conversion_rate" - ) - else: - d["exchange_rate"] = get_exchange_rate( - party_account_currency, company_currency, d.posting_date - ) - - # Add posting date - for d in orders_to_be_billed: - d["posting_date"] = frappe.db.get_value( - d.voucher_type, d.voucher_no, "transaction_date" if d.voucher_type == "Sales Order" else "posting_date" - ) - - # Add due date (if available) - if d.voucher_type == "Sales Order": - d["due_date"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "delivery_date") or "" - - if not orders_to_be_billed: - frappe.msgprint( - _( - "No outstanding Sales Orders found for the {0} {1} which qualify the filters you have specified." - ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))) - ) - - return orders_to_be_billed + if isinstance(args, str): + args = json.loads(args) + + if args.get("party_type") == "Member": + return + + if args.get("party_type") != "Customer": + frappe.throw(_("Sales Orders can only be fetched for Customer")) + + # confirm that Supplier is not blocked (not needed for Customer, but keeping for consistency) + if args.get("party_type") == "Supplier": + supplier_status = get_supplier_block_status(args["party"]) + if supplier_status["on_hold"]: + if supplier_status["hold_type"] == "All": + return [] + elif supplier_status["hold_type"] == "Payments": + if ( + not supplier_status["release_date"] + or getdate(nowdate()) <= supplier_status["release_date"] + ): + return [] + + party_account_currency = get_account_currency(args.get("party_account")) + company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency") + + # Get all SO which are not fully billed or against which full advance not paid + orders_to_be_billed = get_orders_to_be_billed( + args.get("posting_date"), + args.get("party_type"), + args.get("party"), + args.get("company"), + party_account_currency, + company_currency, + filters=args, + ) + + for d in orders_to_be_billed: + d["exchange_rate"] = 1 + if party_account_currency != company_currency: + if d.voucher_type in frappe.get_hooks("invoice_doctypes"): + d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate") + else: + d["exchange_rate"] = get_exchange_rate( + party_account_currency, company_currency, d.posting_date + ) + + # Add posting date + for d in orders_to_be_billed: + d["posting_date"] = frappe.db.get_value( + d.voucher_type, + d.voucher_no, + "transaction_date" if d.voucher_type == "Sales Order" else "posting_date", + ) + + # Add due date (if available) + if d.voucher_type == "Sales Order": + d["due_date"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "delivery_date") or "" + + if not orders_to_be_billed: + frappe.msgprint( + _( + "No outstanding Sales Orders found for the {0} {1} which qualify the filters you have specified." + ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))) + ) + + return orders_to_be_billed + def validate(self, method): - company = frappe.get_cached_doc("Company", self.company) + company = frappe.get_cached_doc("Company", self.company) - if company.restrict_unallocated_amount_for_supplier and self.restrict_unallocated_amount_for_supplier: - if self.unallocated_amount > 0: - frappe.throw(_("Cannot submit Payment Entry {0} with unallocated amount.").format(self.name)) + if company.restrict_unallocated_amount_for_supplier and self.restrict_unallocated_amount_for_supplier: + if self.unallocated_amount > 0: + frappe.throw( + _("Cannot submit Payment Entry {0} with unallocated amount.").format(self.name) + ) diff --git a/csf_tz/csftz_hooks/payroll.py b/csf_tz/csftz_hooks/payroll.py index fb836969..b21b3cf2 100644 --- a/csf_tz/csftz_hooks/payroll.py +++ b/csf_tz/csftz_hooks/payroll.py @@ -1,367 +1,345 @@ +import os +from io import BytesIO + import frappe from frappe import _ -import os +from frappe.model.workflow import apply_workflow +from frappe.utils import flt from frappe.utils.background_jobs import enqueue -from io import BytesIO from PyPDF3 import PdfFileReader, PdfFileWriter + from csf_tz import console -from frappe.model.workflow import apply_workflow -from frappe.utils import cint, flt def before_insert_payroll_entry(doc, method): - enable_payroll_approval = frappe.db.get_single_value( - "CSF TZ Settings", "enable_payroll_approval" - ) - if enable_payroll_approval: - doc.has_payroll_approval = 1 + enable_payroll_approval = frappe.db.get_single_value("CSF TZ Settings", "enable_payroll_approval") + if enable_payroll_approval: + doc.has_payroll_approval = 1 def before_insert_salary_slip(doc, method): - enable_payroll_approval = frappe.db.get_single_value( - "CSF TZ Settings", "enable_payroll_approval" - ) - if enable_payroll_approval: - doc.has_payroll_approval = 1 + enable_payroll_approval = frappe.db.get_single_value("CSF TZ Settings", "enable_payroll_approval") + if enable_payroll_approval: + doc.has_payroll_approval = 1 def before_cancel_payroll_entry(doc, method): - if not doc.has_payroll_approval: - return - - doc.ignore_linked_doctypes = "GL Entry" - salary_slips = frappe.get_all( - "Salary Slip", filters={"payroll_entry": doc.name}, pluck="name" - ) - - journal_entry = None - if len(salary_slips) > 0: - for slip in salary_slips: - try: - slip_doc = frappe.get_doc("Salary Slip", slip) - if not journal_entry: - journal_entry = slip_doc.journal_entry - - if slip_doc.docstatus == 1: - slip_doc.cancel() - slip_doc.delete() - except: - traceback = frappe.get_traceback() - title = _(f"Error for Salary Slip: {slip_doc.name}") - frappe.log_error(traceback, title) - continue - - if journal_entry: - try: - jv_doc = frappe.get_doc("Journal Entry", journal_entry) - if jv_doc.docstatus == 1: - jv_doc.cancel() - jv_doc.delete() - except: - traceback = frappe.get_traceback() - title = _(f"Error for Journal Entry: {jv_doc.name}") - frappe.log_error(traceback, title) - return + if not doc.has_payroll_approval: + return + + doc.ignore_linked_doctypes = "GL Entry" + salary_slips = frappe.get_all("Salary Slip", filters={"payroll_entry": doc.name}, pluck="name") + + journal_entry = None + if len(salary_slips) > 0: + for slip in salary_slips: + try: + slip_doc = frappe.get_doc("Salary Slip", slip) + if not journal_entry: + journal_entry = slip_doc.journal_entry + + if slip_doc.docstatus == 1: + slip_doc.cancel() + slip_doc.delete() + except Exception: + traceback = frappe.get_traceback() + title = _(f"Error for Salary Slip: {slip_doc.name}") + frappe.log_error(traceback, title) + continue + + if journal_entry: + try: + jv_doc = frappe.get_doc("Journal Entry", journal_entry) + if jv_doc.docstatus == 1: + jv_doc.cancel() + jv_doc.delete() + except Exception: + traceback = frappe.get_traceback() + title = _(f"Error for Journal Entry: {jv_doc.name}") + frappe.log_error(traceback, title) + return @frappe.whitelist() def update_slips(payroll_entry): - salary_slips = frappe.get_all( - "Salary Slip", - filters={"payroll_entry": payroll_entry, "docstatus": 0}, - pluck="name", - ) - count = len(salary_slips) - - job = enqueue( - method=enqueue_update_slips, - queue="short", - timeout=4600, - is_async=True, - enqueue_after_commit=False, - job_id=f"update-salary-slips::{payroll_entry}", - deduplicate=True, - payroll_entry=payroll_entry, - ) - - frappe.msgprint(_("{0} Salary Slips is updated".format(count))) - return count + salary_slips = frappe.get_all( + "Salary Slip", + filters={"payroll_entry": payroll_entry, "docstatus": 0}, + pluck="name", + ) + count = len(salary_slips) + + enqueue( + method=enqueue_update_slips, + queue="short", + timeout=4600, + is_async=True, + enqueue_after_commit=False, + job_id=f"update-salary-slips::{payroll_entry}", + deduplicate=True, + payroll_entry=payroll_entry, + ) + + frappe.msgprint(_(f"{count} Salary Slips is updated")) + return count def enqueue_update_slips(payroll_entry): - salary_slips = frappe.get_all( - "Salary Slip", filters={"payroll_entry": payroll_entry}, pluck="name" - ) - - for salary_slip in salary_slips: - try: - _update_salary_slip(salary_slip) - except frappe.DocumentLockedError: - continue - except Exception: - frappe.log_error( - frappe.get_traceback(), - _("Failed to update Salary Slip {0}").format(salary_slip), - ) + salary_slips = frappe.get_all("Salary Slip", filters={"payroll_entry": payroll_entry}, pluck="name") + + for salary_slip in salary_slips: + try: + _update_salary_slip(salary_slip) + except frappe.DocumentLockedError: + continue + except Exception: + frappe.log_error( + frappe.get_traceback(), + _("Failed to update Salary Slip {0}").format(salary_slip), + ) @frappe.whitelist() def update_slip(salary_slip, show_message=True): - result = _update_salary_slip(salary_slip) - if show_message and result == "updated": - frappe.msgprint(_("Salary Slips is updated")) - return result + result = _update_salary_slip(salary_slip) + if show_message and result == "updated": + frappe.msgprint(_("Salary Slips is updated")) + return result def _update_salary_slip(salary_slip): - ss_doc = frappe.get_doc("Salary Slip", salary_slip) - if ss_doc.docstatus != 0: - return "skipped" - ss_doc.earnings = [] - ss_doc.deductions = [] - ss_doc.save() - return "updated" + ss_doc = frappe.get_doc("Salary Slip", salary_slip) + if ss_doc.docstatus != 0: + return "skipped" + ss_doc.earnings = [] + ss_doc.deductions = [] + ss_doc.save() + return "updated" @frappe.whitelist() def print_slips(payroll_entry): - enqueue( - method=enqueue_print_slips, - queue="short", - timeout=100000, - is_async=True, - job_name="print_salary_slips", - kwargs=payroll_entry, - ) + enqueue( + method=enqueue_print_slips, + queue="short", + timeout=100000, + is_async=True, + job_name="print_salary_slips", + kwargs=payroll_entry, + ) def enqueue_print_slips(kwargs): - console("Start Printing") - payroll_entry = kwargs - ss_data = frappe.get_all("Salary Slip", filters={"payroll_entry": payroll_entry}) - ss_list = [] - for i in ss_data: - ss_list.append(i.name) - doctype = dict({"Salary Slip": ss_list}) - print_format = "" - default_print_format = frappe.db.get_value( - "Property Setter", - dict(property="default_print_format", doc_type="Salary Slip"), - "value", - ) - if default_print_format: - print_format = default_print_format - else: - print_format = "Standard" - - pdf = download_multi_pdf( - doctype, payroll_entry, format=print_format, no_letterhead=0 - ) - if pdf: - ret = frappe.get_doc( - { - "doctype": "File", - "attached_to_doctype": "Payroll Entry", - "attached_to_name": payroll_entry, - "folder": "Home/Attachments", - "file_name": payroll_entry + ".pdf", - "file_url": "/files/" + payroll_entry + ".pdf", - "content": pdf, - } - ) - ret.save(ignore_permissions=1) - console("Printing Finished", "The PDF file is ready in attachments") - return ret + console("Start Printing") + payroll_entry = kwargs + ss_data = frappe.get_all("Salary Slip", filters={"payroll_entry": payroll_entry}) + ss_list = [] + for i in ss_data: + ss_list.append(i.name) + doctype = dict({"Salary Slip": ss_list}) + print_format = "" + default_print_format = frappe.db.get_value( + "Property Setter", + dict(property="default_print_format", doc_type="Salary Slip"), + "value", + ) + if default_print_format: + print_format = default_print_format + else: + print_format = "Standard" + + pdf = download_multi_pdf(doctype, payroll_entry, format=print_format, no_letterhead=0) + if pdf: + ret = frappe.get_doc( + { + "doctype": "File", + "attached_to_doctype": "Payroll Entry", + "attached_to_name": payroll_entry, + "folder": "Home/Attachments", + "file_name": payroll_entry + ".pdf", + "file_url": "/files/" + payroll_entry + ".pdf", + "content": pdf, + } + ) + ret.save(ignore_permissions=1) + console("Printing Finished", "The PDF file is ready in attachments") + return ret def download_multi_pdf(doctype, name, format=None, no_letterhead=0): - output = PdfFileWriter() - if isinstance(doctype, dict): - for doctype_name in doctype: - for doc_name in doctype[doctype_name]: - try: - console(doc_name) - pdf_data = frappe.get_print( - doctype_name, - doc_name, - format, - as_pdf=True, - output=None, - no_letterhead=no_letterhead, - ) - - # Convert the PDF bytes into a file-like object - pdf_file = BytesIO(pdf_data) - - # Create a PdfFileReader from the byte stream (file-like object) - reader = PdfFileReader(pdf_file) - - # Add each page from the reader to the writer - for page_num in range(reader.getNumPages()): - output.addPage(reader.getPage(page_num)) - - except Exception: - frappe.log_error( - f"Permission Error on doc {doc_name} of doctype {doctype_name}" - ) - frappe.local.response.filename = f"{name}.pdf" - return read_multi_pdf(output) + output = PdfFileWriter() + if isinstance(doctype, dict): + for doctype_name in doctype: + for doc_name in doctype[doctype_name]: + try: + console(doc_name) + pdf_data = frappe.get_print( + doctype_name, + doc_name, + format, + as_pdf=True, + output=None, + no_letterhead=no_letterhead, + ) + + # Convert the PDF bytes into a file-like object + pdf_file = BytesIO(pdf_data) + + # Create a PdfFileReader from the byte stream (file-like object) + reader = PdfFileReader(pdf_file) + + # Add each page from the reader to the writer + for page_num in range(reader.getNumPages()): + output.addPage(reader.getPage(page_num)) + + except Exception: + frappe.log_error(f"Permission Error on doc {doc_name} of doctype {doctype_name}") + frappe.local.response.filename = f"{name}.pdf" + return read_multi_pdf(output) def read_multi_pdf(output): - fname = os.path.join("/tmp", "frappe-pdf-{0}.pdf".format(frappe.generate_hash())) - with open(fname, "wb") as f: - output.write(f) + fname = os.path.join("/tmp", f"frappe-pdf-{frappe.generate_hash()}.pdf") + with open(fname, "wb") as f: + output.write(f) - with open(fname, "rb") as fileobj: - filedata = fileobj.read() + with open(fname, "rb") as fileobj: + filedata = fileobj.read() - return filedata + return filedata @frappe.whitelist() def create_journal_entry(payroll_entry): - payroll_entry_doc = frappe.get_doc("Payroll Entry", payroll_entry) - if ( - payroll_entry_doc.docstatus != 1 - or payroll_entry_doc.salary_slips_submitted == 1 - ): - return - draft_slips_count = frappe.db.count( - "Salary Slip", filters={"payroll_entry": payroll_entry, "docstatus": 0} - ) - - if draft_slips_count > 0: - frappe.throw(_("Salary Slips are not submitted")) - else: - submitted_ss = payroll_entry_doc.get_sal_slip_list(ss_status=1, as_dict=True) - jv_name = payroll_entry_doc.make_accrual_jv_entry(submitted_ss) - jv_url = frappe.utils.get_url_to_form("Journal Entry", jv_name) - si_msgprint = _("Journal Entry Created {1}").format( - jv_url, jv_name - ) - frappe.msgprint(si_msgprint) - return "True" + payroll_entry_doc = frappe.get_doc("Payroll Entry", payroll_entry) + if payroll_entry_doc.docstatus != 1 or payroll_entry_doc.salary_slips_submitted == 1: + return + draft_slips_count = frappe.db.count( + "Salary Slip", filters={"payroll_entry": payroll_entry, "docstatus": 0} + ) + + if draft_slips_count > 0: + frappe.throw(_("Salary Slips are not submitted")) + else: + submitted_ss = payroll_entry_doc.get_sal_slip_list(ss_status=1, as_dict=True) + jv_name = payroll_entry_doc.make_accrual_jv_entry(submitted_ss) + jv_url = frappe.utils.get_url_to_form("Journal Entry", jv_name) + si_msgprint = _("Journal Entry Created {1}").format(jv_url, jv_name) + frappe.msgprint(si_msgprint) + return "True" def before_update_after_submit(doc, method): - if not doc.has_payroll_approval: - return + if not doc.has_payroll_approval: + return - # submit salary slips directly if payroll entry is approved - if "Approved" in doc.workflow_state: - doc.submit_salary_slips() - return + # submit salary slips directly if payroll entry is approved + if "Approved" in doc.workflow_state: + doc.submit_salary_slips() + return - salary_slips = frappe.get_all( - "Salary Slip", filters={"payroll_entry": doc.name}, pluck="name" - ) - if len(salary_slips) == 0: - return + salary_slips = frappe.get_all("Salary Slip", filters={"payroll_entry": doc.name}, pluck="name") + if len(salary_slips) == 0: + return - params = {"salary_slips": salary_slips, "action": get_workflow_action(doc)} + params = {"salary_slips": salary_slips, "action": get_workflow_action(doc)} - enqueue( - method=enqueue_apply_workflow_for_salary_slips, - queue="short", - timeout=100000, - is_async=True, - job_name="apply_workflow_for_salary_slips", - kwargs=params, - ) + enqueue( + method=enqueue_apply_workflow_for_salary_slips, + queue="short", + timeout=100000, + is_async=True, + job_name="apply_workflow_for_salary_slips", + kwargs=params, + ) def get_workflow_action(doc): - if doc.workflow_state == "Approval Requested": - return "Submit" - elif doc.workflow_state == "Change Requested": - return "Reject" - elif "Reviewed" in doc.workflow_state: - return "Submit" + if doc.workflow_state == "Approval Requested": + return "Submit" + elif doc.workflow_state == "Change Requested": + return "Reject" + elif "Reviewed" in doc.workflow_state: + return "Submit" def enqueue_apply_workflow_for_salary_slips(kwargs): - for slip in kwargs.get("salary_slips"): - slip_doc = frappe.get_doc("Salary Slip", slip) - - if kwargs.get("action") == "Reject" and slip_doc.workflow_state == "Open": - continue - elif ( - kwargs.get("action") == "Submit" - and slip_doc.workflow_state == "Ongoing Approval" - ): - continue - elif kwargs.get("action") == "Submit" and slip_doc.workflow_state == "Approved": - continue - elif ( - kwargs.get("action") == "Cancel" and slip_doc.workflow_state == "Cancelled" - ): - continue - elif not kwargs.get("action"): - continue - - try: - apply_workflow(slip_doc, kwargs.get("action")) - - except: - traceback = frappe.get_traceback() - title = _(f"Error for Salary Slip: {slip_doc.name}") - frappe.log_error(traceback, title) - continue + for slip in kwargs.get("salary_slips"): + slip_doc = frappe.get_doc("Salary Slip", slip) + + if kwargs.get("action") == "Reject" and slip_doc.workflow_state == "Open": + continue + elif kwargs.get("action") == "Submit" and slip_doc.workflow_state == "Ongoing Approval": + continue + elif kwargs.get("action") == "Submit" and slip_doc.workflow_state == "Approved": + continue + elif kwargs.get("action") == "Cancel" and slip_doc.workflow_state == "Cancelled": + continue + elif not kwargs.get("action"): + continue + + try: + apply_workflow(slip_doc, kwargs.get("action")) + + except Exception: + traceback = frappe.get_traceback() + title = _(f"Error for Salary Slip: {slip_doc.name}") + frappe.log_error(traceback, title) + continue @frappe.whitelist() def get_amounts_summary(payroll_entry): - summary = { - "gross_pay": 0.0, - "net_pay": 0.0, - "components": [], - } - - salary_slips = frappe.get_all( - "Salary Slip", - filters={ - "payroll_entry": payroll_entry, - "docstatus": ["!=", 2], - }, - fields=["name", "gross_pay", "net_pay"], - ) - - slip_names = [] - for slip in salary_slips: - summary["gross_pay"] += flt(slip.gross_pay) - summary["net_pay"] += flt(slip.net_pay) - slip_names.append(slip.name) - - tracked_components = frappe.get_all( - "Salary Component", - filters={"include_in_payroll_summary": 1}, - fields=["name"], - order_by="name asc", - ) - - component_names = [component.name for component in tracked_components] - totals_map = {} - - if slip_names and component_names: - component_totals = frappe.get_all( - "Salary Detail", - filters={ - "parent": ("in", slip_names), - "salary_component": ("in", component_names), - }, - fields=["salary_component", "sum(amount) as total"], - group_by="salary_component", - ) - totals_map = {row.salary_component: flt(row.total) for row in component_totals} - - for component in tracked_components: - summary["components"].append( - { - "component": component.name, - "label": component.name, - "amount": totals_map.get(component.name, 0.0), - } - ) - - return summary + summary = { + "gross_pay": 0.0, + "net_pay": 0.0, + "components": [], + } + + salary_slips = frappe.get_all( + "Salary Slip", + filters={ + "payroll_entry": payroll_entry, + "docstatus": ["!=", 2], + }, + fields=["name", "gross_pay", "net_pay"], + ) + + slip_names = [] + for slip in salary_slips: + summary["gross_pay"] += flt(slip.gross_pay) + summary["net_pay"] += flt(slip.net_pay) + slip_names.append(slip.name) + + tracked_components = frappe.get_all( + "Salary Component", + filters={"include_in_payroll_summary": 1}, + fields=["name"], + order_by="name asc", + ) + + component_names = [component.name for component in tracked_components] + totals_map = {} + + if slip_names and component_names: + component_totals = frappe.get_all( + "Salary Detail", + filters={ + "parent": ("in", slip_names), + "salary_component": ("in", component_names), + }, + fields=["salary_component", "sum(amount) as total"], + group_by="salary_component", + ) + totals_map = {row.salary_component: flt(row.total) for row in component_totals} + + for component in tracked_components: + summary["components"].append( + { + "component": component.name, + "label": component.name, + "amount": totals_map.get(component.name, 0.0), + } + ) + + return summary diff --git a/csf_tz/csftz_hooks/program_enrollment.py b/csf_tz/csftz_hooks/program_enrollment.py index 75744520..fad98b18 100644 --- a/csf_tz/csftz_hooks/program_enrollment.py +++ b/csf_tz/csftz_hooks/program_enrollment.py @@ -1,53 +1,57 @@ -from __future__ import unicode_literals import frappe -from frappe import _ + +# nosemgrep: frappe-semgrep-rules.rules.frappe-monkey-patching-not-allowed from education.education.doctype.program_enrollment.program_enrollment import ProgramEnrollment +from frappe import _ + # from csf_tz import console def create_course_enrollments(self): - student = frappe.get_doc("Student", self.student) - program = frappe.get_doc("Program", self.program) - course_list = [course.course for course in program.courses] - for course_name in course_list: - student.enroll_in_course( - course_name=course_name, program_enrollment=self.name) + student = frappe.get_doc("Student", self.student) + program = frappe.get_doc("Program", self.program) + course_list = [course.course for course in program.courses] + for course_name in course_list: + student.enroll_in_course(course_name=course_name, program_enrollment=self.name) def create_course_enrollments_override(doc, method): - ProgramEnrollment.create_course_enrollments = create_course_enrollments + ProgramEnrollment.create_course_enrollments = create_course_enrollments @frappe.whitelist() def get_fee_schedule(program, academic_year, academic_term=None, student_category=None): - """Returns Fee Schedule. - - :param program: Program. - :param student_category: Student Category - :param academic_year - :param academic_term - """ - fs = frappe.get_list("Program Fee", fields=["academic_term", "fee_structure", "due_date", "amount"], - filters={"parent": program, "student_category": student_category}, parent_doctype="Program Enrollment", order_by="idx") - - fees_list = [] - for i in fs: - fs_academic_year = frappe.get_value( - "Fee Structure", i["fee_structure"], "academic_year") or "" - fs_academic_term = "False" - if academic_term: - fs_academic_term = frappe.get_value( - "Fee Structure", i["fee_structure"], "academic_term") or "" - if fs_academic_term != "False": - if fs_academic_term == academic_term and fs_academic_year == academic_year: - fees_list.append(i) - else: - if fs_academic_year == academic_year: - fees_list.append(i) - - return fees_list + """Returns Fee Schedule. + + :param program: Program. + :param student_category: Student Category + :param academic_year + :param academic_term + """ + fs = frappe.get_list( + "Program Fee", + fields=["academic_term", "fee_structure", "due_date", "amount"], + filters={"parent": program, "student_category": student_category}, + parent_doctype="Program Enrollment", + order_by="idx", + ) + + fees_list = [] + for i in fs: + fs_academic_year = frappe.get_value("Fee Structure", i["fee_structure"], "academic_year") or "" + fs_academic_term = "False" + if academic_term: + fs_academic_term = frappe.get_value("Fee Structure", i["fee_structure"], "academic_term") or "" + if fs_academic_term != "False": + if fs_academic_term == academic_term and fs_academic_year == academic_year: + fees_list.append(i) + else: + if fs_academic_year == academic_year: + fees_list.append(i) + + return fees_list def validate_submit_program_enrollment(doc, method): - if not doc.student_category: - frappe.throw(_("Please set Student Category")) + if not doc.student_category: + frappe.throw(_("Please set Student Category")) diff --git a/csf_tz/csftz_hooks/stock.py b/csf_tz/csftz_hooks/stock.py index 80faa89a..91c1c7ec 100644 --- a/csf_tz/csftz_hooks/stock.py +++ b/csf_tz/csftz_hooks/stock.py @@ -1,33 +1,47 @@ -from __future__ import unicode_literals import frappe + +# nosemgrep: frappe-semgrep-rules.rules.frappe-monkey-patching-not-allowed from erpnext.stock.doctype.stock_entry.stock_entry import StockEntry +from frappe import _ def validate_with_material_request(self): - bypass_material_request_validation = frappe.get_value("Company", self.company,"bypass_material_request_validation") or 0 - if bypass_material_request_validation: - return - for item in self.get("items"): - if item.material_request: - mreq_item = frappe.db.get_value("Material Request Item", - {"name": item.material_request_item, "parent": item.material_request}, - ["item_code", "warehouse", "idx"], as_dict=True) - if mreq_item.item_code != item.item_code or \ - mreq_item.warehouse != (item.s_warehouse if self.purpose== "Material Issue" else item.t_warehouse): - frappe.throw(_("Item or Warehouse for row {0} does not match Material Request").format(item.idx), - frappe.MappingMismatchError) + bypass_material_request_validation = ( + frappe.get_value("Company", self.company, "bypass_material_request_validation") or 0 + ) + if bypass_material_request_validation: + return + for item in self.get("items"): + if item.material_request: + mreq_item = frappe.db.get_value( + "Material Request Item", + {"name": item.material_request_item, "parent": item.material_request}, + ["item_code", "warehouse", "idx"], + as_dict=True, + ) + if mreq_item.item_code != item.item_code or mreq_item.warehouse != ( + item.s_warehouse if self.purpose == "Material Issue" else item.t_warehouse + ): + frappe.throw( + _("Item or Warehouse for row {0} does not match Material Request").format(item.idx), + frappe.MappingMismatchError, + ) def validate_with_material_request_override(doc, method): - StockEntry.validate_with_material_request = validate_with_material_request + StockEntry.validate_with_material_request = validate_with_material_request + def import_from_bom(self, method): - if self.stock_entry_type == "Manufacture" and self.bom_no: - bom = frappe.get_doc("BOM", self.bom_no) - for d in bom.additional_costs: - self.append("additional_costs", { - "expense_account": d.expense_account, - "amount": d.cost_per_unit, - "base_amount": d.cost_per_unit, - "description": d.cost_type - }) + if self.stock_entry_type == "Manufacture" and self.bom_no: + bom = frappe.get_doc("BOM", self.bom_no) + for d in bom.additional_costs: + self.append( + "additional_costs", + { + "expense_account": d.expense_account, + "amount": d.cost_per_unit, + "base_amount": d.cost_per_unit, + "description": d.cost_type, + }, + ) diff --git a/csf_tz/csftz_hooks/student_applicant.py b/csf_tz/csftz_hooks/student_applicant.py index 7ad9abad..ee6e645e 100644 --- a/csf_tz/csftz_hooks/student_applicant.py +++ b/csf_tz/csftz_hooks/student_applicant.py @@ -1,56 +1,55 @@ -from __future__ import unicode_literals import frappe -from frappe.model.document import Document from frappe.utils import today -from frappe import _ -from csf_tz.custom_api import print_out + # from frappe.utils import from frappe.utils import today, format_datetime, now, nowdate, getdate, get_url, get_host_name, format_datetime, now, nowdate, getdate, get_url, get_host_name def make_student_applicant_fees(doc, method): - if doc.docstatus != 1: - return - if doc.application_status != "Awaiting Registration Fees" or doc.student_applicant_fee: - return - fee_structure = frappe.get_doc("Fee Structure", doc.fee_structure) - student_name = doc.first_name - if doc.middle_name: - student_name += " " + doc.middle_name - if doc.last_name: - student_name += " " + doc.last_name - - fee_doc =frappe.get_doc ({ - 'doctype': 'Student Applicant Fees', - 'student': doc.name, - 'student_name': student_name, - 'fee_schedule': None, - 'company': fee_structure.company, - 'posting_date': today(), - 'due_date': today(), - 'program_enrollment': doc.program_enrollment, - 'program': fee_structure.program, - 'student_batch': None, - 'student_email': doc.student_email_id, - 'student_category': fee_structure.student_category, - 'academic_term': fee_structure.academic_term, - 'academic_year': fee_structure.academic_year, - 'currency': frappe.get_value("Company", fee_structure.company, "default_currency"), - 'fee_structure': doc.fee_structure, - 'grand_total': fee_structure.total_amount, - 'receivable_account': fee_structure.receivable_account, - 'income_account': fee_structure.income_account, - 'cost_center': fee_structure.cost_center, - }) + if doc.docstatus != 1: + return + if doc.application_status != "Awaiting Registration Fees" or doc.student_applicant_fee: + return + fee_structure = frappe.get_doc("Fee Structure", doc.fee_structure) + student_name = doc.first_name + if doc.middle_name: + student_name += " " + doc.middle_name + if doc.last_name: + student_name += " " + doc.last_name + + fee_doc = frappe.get_doc( + { + "doctype": "Student Applicant Fees", + "student": doc.name, + "student_name": student_name, + "fee_schedule": None, + "company": fee_structure.company, + "posting_date": today(), + "due_date": today(), + "program_enrollment": doc.program_enrollment, + "program": fee_structure.program, + "student_batch": None, + "student_email": doc.student_email_id, + "student_category": fee_structure.student_category, + "academic_term": fee_structure.academic_term, + "academic_year": fee_structure.academic_year, + "currency": frappe.get_value("Company", fee_structure.company, "default_currency"), + "fee_structure": doc.fee_structure, + "grand_total": fee_structure.total_amount, + "receivable_account": fee_structure.receivable_account, + "income_account": fee_structure.income_account, + "cost_center": fee_structure.cost_center, + } + ) - fee_doc.flags.ignore_permissions = True - frappe.flags.ignore_account_permission = True - fee_doc.save() - callback_token = fee_doc.callback_token - doc.bank_reference = fee_doc.bank_reference or "None" - doc.student_applicant_fee = fee_doc.name or "None" - doc.db_update() - fee_doc.reload() - fee_doc.callback_token = callback_token - fee_doc.bank_reference = doc.bank_reference - fee_doc.db_update() - fee_doc.submit() \ No newline at end of file + fee_doc.flags.ignore_permissions = True + frappe.flags.ignore_account_permission = True + fee_doc.save() + callback_token = fee_doc.callback_token + doc.bank_reference = fee_doc.bank_reference or "None" + doc.student_applicant_fee = fee_doc.name or "None" + doc.db_update() + fee_doc.reload() + fee_doc.callback_token = callback_token + fee_doc.bank_reference = doc.bank_reference + fee_doc.db_update() + fee_doc.submit() diff --git a/csf_tz/custom_api.py b/csf_tz/custom_api.py index 50e097d8..762542c3 100644 --- a/csf_tz/custom_api.py +++ b/csf_tz/custom_api.py @@ -1,5 +1,3 @@ -from __future__ import unicode_literals - import base64 import io import json @@ -82,11 +80,11 @@ def check_msg(msg): def get_stock_ledger_entries(item_code): if get_version() == 12: - conditions = " and sle.item_code = '%s'" % item_code + conditions = f" and sle.item_code = '{item_code}'" else: - conditions = " and sle.is_cancelled = 0 and sle.item_code = '%s'" % item_code + conditions = f" and sle.is_cancelled = 0 and sle.item_code = '{item_code}'" return frappe.db.sql( - """ + f""" select sle.batch_no, sle.item_code, sle.warehouse, sle.qty_after_transaction as actual_qty from `tabStock Ledger Entry` sle inner join ( @@ -97,9 +95,8 @@ def get_stock_ledger_entries(item_code): and sle.item_code = sle_max.item_code and sle.warehouse = sle_max.warehouse and sle.posting_datetime = sle_max.posting_datetime - where sle.docstatus = 1 %s - order by sle.warehouse, sle.item_code, sle.batch_no""" - % conditions, + where sle.docstatus = 1 {conditions} + order by sle.warehouse, sle.item_code, sle.batch_no""", as_dict=1, ) @@ -119,9 +116,7 @@ def get_app_branch(app): import subprocess try: - branch = subprocess.check_output( - "cd ../apps/{0} && git rev-parse --abbrev-ref HEAD".format(app), shell=True - ) + branch = subprocess.check_output(["git", "rev-parse", "--abbrev-ref", "HEAD"], cwd=f"../apps/{app}") branch = branch.decode("utf-8") branch = branch.strip() return branch @@ -167,32 +162,29 @@ def get_item_info(item_code: Any): @frappe.whitelist() def get_item_prices(item_code: Any, currency: Any, customer: Any = None, company: Any = None): - item_code = "'{0}'".format(item_code) - currency = "'{0}'".format(currency) + item_code = f"'{item_code}'" + currency = f"'{currency}'" unique_records = int(frappe.db.get_single_value("CSF TZ Settings", "unique_records")) prices_list = [] unique_price_list = [] max_records = frappe.db.get_value("Company", company, "max_records_in_dialog") or 20 if customer: - conditions = " and SI.customer = '%s'" % customer + conditions = f" and SI.customer = '{customer}'" else: conditions = "" - query = ( - """ SELECT SI.name, SI.posting_date, SI.customer, SIT.item_code, SIT.qty, SIT.rate + query = f""" SELECT SI.name, SI.posting_date, SI.customer, SIT.item_code, SIT.qty, SIT.rate FROM `tabSales Invoice` AS SI INNER JOIN `tabSales Invoice Item` AS SIT ON SIT.parent = SI.name WHERE - SIT.item_code = {0} + SIT.item_code = {item_code} AND SIT.parent = SI.name AND SI.docstatus=%s - AND SI.currency = {2} + AND SI.currency = {currency} AND SI.is_return != 1 - AND SI.company = '{3}' - {1} - ORDER by SI.posting_date DESC""".format(item_code, conditions, currency, company) - % (1) - ) + AND SI.company = '{company}' + {conditions} + ORDER by SI.posting_date DESC""" % (1) items = frappe.db.sql(query, as_dict=True) for item in items: @@ -229,8 +221,8 @@ def get_item_prices_custom(filters: Any = None, start: Any = 0, limit: Any = 20) unique_records = int(frappe.db.get_single_value("CSF TZ Settings", "unique_records")) customer = filters.get("customer", "") company = filters.get("company", "") - item_code = "'{0}'".format(filters.get("item_code", "")) - currency = "'{0}'".format(filters.get("currency", "")) + item_code = "'{}'".format(filters.get("item_code", "")) + currency = "'{}'".format(filters.get("currency", "")) prices_list = [] unique_price_list = [] max_records = int(start) + int(limit) @@ -238,28 +230,26 @@ def get_item_prices_custom(filters: Any = None, start: Any = 0, limit: Any = 20) if "posting_date" in filters: posting_date = filters["posting_date"] - from_date = "'{from_date}'".format(from_date=posting_date[1][0]) - to_date = "'{to_date}'".format(to_date=posting_date[1][1]) - conditions += "AND DATE(SI.posting_date) BETWEEN {start} AND {end}".format( - start=from_date, end=to_date - ) + from_date = f"'{posting_date[1][0]}'" + to_date = f"'{posting_date[1][1]}'" + conditions += f"AND DATE(SI.posting_date) BETWEEN {from_date} AND {to_date}" if customer: - conditions += " AND SI.customer = '%s'" % customer + conditions += f" AND SI.customer = '{customer}'" # TODO: refactor to parameterized query; inputs are pre-quoted upstream # nosemgrep: frappe-sql-format-injection - query = """ SELECT SI.name, SI.posting_date, SI.customer, SIT.item_code, SIT.qty, SIT.rate + query = f""" SELECT SI.name, SI.posting_date, SI.customer, SIT.item_code, SIT.qty, SIT.rate FROM `tabSales Invoice` AS SI INNER JOIN `tabSales Invoice Item` AS SIT ON SIT.parent = SI.name WHERE - SIT.item_code = {0} + SIT.item_code = {item_code} AND SIT.parent = SI.name AND SI.docstatus= 1 - AND SI.currency = {2} + AND SI.currency = {currency} AND SI.is_return != 1 - AND SI.company = '{3}' - {1} - ORDER by SI.posting_date DESC""".format(item_code, conditions, currency, company) + AND SI.company = '{company}' + {conditions} + ORDER by SI.posting_date DESC""" items = frappe.db.sql(query, as_dict=True) for item in items: @@ -565,16 +555,13 @@ def update_delivery_on_sales_invoice(doc, method): def get_delivery_note_item_count(item_row_name, sales_invoice): - query = """ SELECT SUM(stock_qty) as cont + query = f""" SELECT SUM(stock_qty) as cont FROM `tabDelivery Note Item` WHERE - si_detail = '%s' + si_detail = '{item_row_name}' AND docstatus != 2 - AND against_sales_invoice = '%s' - """ % ( - item_row_name, - sales_invoice, - ) + AND against_sales_invoice = '{sales_invoice}' + """ counts = frappe.db.sql(query, as_dict=True) if len(counts) > 0 and counts[0]["cont"]: @@ -590,21 +577,19 @@ def get_pending_sales_invoice(*args): page_length = cint(args[4]) conditions = "" if args[1] != "": - conditions += " AND SI.name = '%s'" % args[1] + conditions += f" AND SI.name = '{args[1]}'" if "posting_date" in filters: posting_date = filters["posting_date"] - from_date = "'{from_date}'".format(from_date=posting_date[1][0]) - to_date = "'{to_date}'".format(to_date=posting_date[1][1]) - conditions += "AND DATE(SI.posting_date) BETWEEN {start} AND {end}".format( - start=from_date, end=to_date - ) + from_date = f"'{posting_date[1][0]}'" + to_date = f"'{posting_date[1][1]}'" + conditions += f"AND DATE(SI.posting_date) BETWEEN {from_date} AND {to_date}" if "customer" in filters: - conditions += " AND SI.customer = '%s'" % filters["customer"] + conditions += " AND SI.customer = '{}'".format(filters["customer"]) if "company" in filters: - conditions += " AND SI.company = '%s'" % filters["company"] + conditions += " AND SI.company = '{}'".format(filters["company"]) if "set_warehouse" in filters: - conditions += " AND SIT.warehouse = '%s'" % filters["set_warehouse"] - query = """ + conditions += " AND SIT.warehouse = '{}'".format(filters["set_warehouse"]) + query = f""" WITH CTE AS( SELECT SIT.stock_qty, @@ -626,18 +611,14 @@ def get_pending_sales_invoice(*args): AND SI.is_return = 0 AND SI.status NOT IN ("Credit Note Issued", "Internal Transfer") AND SIT.stock_qty != SIT.delivered_qty - %s + {conditions} GROUP BY SI.name, SIT.name HAVING SIT.stock_qty > DNI_sum_stock_qty ) SELECT * FROM `CTE` WHERE RN = 1 - LIMIT %s - OFFSET %s - """ % ( - conditions, - page_length, - start, - ) + LIMIT {page_length} + OFFSET {start} + """ data = frappe.db.sql(query, as_dict=True) return data @@ -645,10 +626,10 @@ def get_pending_sales_invoice(*args): def get_list_pending_sales_invoice(invoice_name=None, warehouse=None): conditions = "" if invoice_name: - conditions += " AND SI.name = '%s'" % invoice_name + conditions += f" AND SI.name = '{invoice_name}'" if warehouse: - conditions += " AND SIT.warehouse = '%s'" % warehouse - query = """ + conditions += f" AND SIT.warehouse = '{warehouse}'" + query = f""" WITH CTE AS( SELECT SIT.stock_qty, @@ -670,12 +651,12 @@ def get_list_pending_sales_invoice(invoice_name=None, warehouse=None): AND SI.update_stock != 1 AND SIT.stock_qty != SIT.delivered_qty AND SI.enabled_auto_create_delivery_notes = 1 - %s + {conditions} GROUP BY SI.name, SIT.name HAVING SIT.stock_qty > DNI_sum_stock_qty ) SELECT * FROM `CTE` WHERE RN = 1 - """ % (conditions) + """ data = frappe.db.sql(query, as_dict=True) return data @@ -795,7 +776,7 @@ def make_stock_reconciliation_for_all_pending_material_request(*args): data[mat_req_doc.company][item.warehouse].append(item_dict) for key, value in data.items(): - for key1, value1 in value.items(): + for _key1, value1 in value.items(): if len(value1) > 0: items_list = [] items = [] @@ -1390,14 +1371,8 @@ def allocate_batches_for_single_items(doc, items, warehouse, fields_to_clear): if b_qty < row.qty: frappe.throw( - "Qty: {0} available for item: {1} on warehouse: {2} is not enough to complete requested Qty: {3}
\ - Please update sales order: {4} to match the Qty available on stock".format( - frappe.bold(b_qty), - frappe.bold(row.item_code), - frappe.bold(warehouse), - frappe.bold(row.qty), - frappe.bold(row.parent), - ) + f"Qty: {frappe.bold(b_qty)} available for item: {frappe.bold(row.item_code)} on warehouse: {frappe.bold(warehouse)} is not enough to complete requested Qty: {frappe.bold(row.qty)}
\ + Please update sales order: {frappe.bold(row.parent)} to match the Qty available on stock" ) else: @@ -2149,8 +2124,8 @@ def get_item_prices_custom_po(filters: Any = None, start: Any = 0, limit: Any = unique_records = int(frappe.db.get_single_value("CSF TZ Settings", "unique_records")) customer = filters.get("customer", "") company = filters.get("company", "") - item_code = "'{0}'".format(filters.get("item_code", "")) - currency = "'{0}'".format(filters.get("currency", "")) + item_code = "'{}'".format(filters.get("item_code", "")) + currency = "'{}'".format(filters.get("currency", "")) prices_list = [] unique_price_list = [] max_records = int(start) + int(limit) @@ -2158,28 +2133,26 @@ def get_item_prices_custom_po(filters: Any = None, start: Any = 0, limit: Any = if "posting_date" in filters: posting_date = filters["posting_date"] - from_date = "'{from_date}'".format(from_date=posting_date[1][0]) - to_date = "'{to_date}'".format(to_date=posting_date[1][1]) - conditions += "AND DATE(PI.posting_date) BETWEEN {start} AND {end}".format( - start=from_date, end=to_date - ) + from_date = f"'{posting_date[1][0]}'" + to_date = f"'{posting_date[1][1]}'" + conditions += f"AND DATE(PI.posting_date) BETWEEN {from_date} AND {to_date}" if customer: - conditions += " AND PI.supplier = '%s'" % customer + conditions += f" AND PI.supplier = '{customer}'" # TODO: refactor to parameterized query; inputs are pre-quoted upstream # nosemgrep: frappe-sql-format-injection - query = """ SELECT PI.name, PI.posting_date, PI.supplier, PIT.item_code, PIT.qty, PIT.rate + query = f""" SELECT PI.name, PI.posting_date, PI.supplier, PIT.item_code, PIT.qty, PIT.rate FROM `tabPurchase Invoice` AS PI INNER JOIN `tabPurchase Invoice Item` AS PIT ON PIT.parent = PI.name WHERE - PIT.item_code = {0} + PIT.item_code = {item_code} AND PIT.parent = PI.name AND PI.docstatus= 1 - AND PI.currency = {2} + AND PI.currency = {currency} AND PI.is_return != 1 - AND PI.company = '{3}' - {1} - ORDER by PI.posting_date DESC""".format(item_code, conditions, currency, company) + AND PI.company = '{company}' + {conditions} + ORDER by PI.posting_date DESC""" items = frappe.db.sql(query, as_dict=True) for item in items: @@ -2202,32 +2175,29 @@ def get_item_prices_custom_po(filters: Any = None, start: Any = 0, limit: Any = @frappe.whitelist() def get_item_prices_po(item_code: Any, currency: Any, customer: Any = None, company: Any = None): - item_code = "'{0}'".format(item_code) - currency = "'{0}'".format(currency) + item_code = f"'{item_code}'" + currency = f"'{currency}'" unique_records = int(frappe.db.get_single_value("CSF TZ Settings", "unique_records")) prices_list = [] unique_price_list = [] max_records = frappe.db.get_value("Company", company, "max_records_in_dialog") or 20 if customer: - conditions = " and PI.supplier = '%s'" % customer + conditions = f" and PI.supplier = '{customer}'" else: conditions = "" - query = ( - """ SELECT PI.name, PI.posting_date, PI.supplier, PIT.item_code, PIT.qty, PIT.rate + query = f""" SELECT PI.name, PI.posting_date, PI.supplier, PIT.item_code, PIT.qty, PIT.rate FROM `tabPurchase Invoice` AS PI INNER JOIN `tabPurchase Invoice Item` AS PIT ON PIT.parent = PI.name WHERE - PIT.item_code = {0} + PIT.item_code = {item_code} AND PIT.parent = PI.name AND PI.docstatus=%s - AND PI.currency = {2} + AND PI.currency = {currency} AND PI.is_return != 1 - AND PI.company = '{3}' - {1} - ORDER by PI.posting_date DESC""".format(item_code, conditions, currency, company) - % (1) - ) + AND PI.company = '{company}' + {conditions} + ORDER by PI.posting_date DESC""" % (1) items = frappe.db.sql(query, as_dict=True) for item in items: diff --git a/csf_tz/hooks.py b/csf_tz/hooks.py index c0fdec93..b91bc488 100755 --- a/csf_tz/hooks.py +++ b/csf_tz/hooks.py @@ -1,6 +1,3 @@ -# -*- coding: utf-8 -*- -from __future__ import unicode_literals - app_name = "csf_tz" app_title = "CSF TZ" app_publisher = "Aakvatech" @@ -13,7 +10,7 @@ # Override Document Class -override_doctype_class = { +override_doctype_class = { # nosemgrep: frappe-semgrep-rules.rules.override-doctype-class "Salary Slip": "csf_tz.overrides.salary_slip.SalarySlip", "Additional Salary": "csf_tz.overrides.additional_salary.AdditionalSalary", "Leave Encashment": "csf_tz.overrides.leave_encashment.LeaveEncashment", @@ -125,12 +122,12 @@ after_migrate = [ "csf_tz.utils.create_custom_fields.execute", - "csf_tz.utils.authority_notification_settings_fields.execute", + "csf_tz.utils.authority_notification_settings_fields.execute", "csf_tz.utils.create_property_setter.execute", "csf_tz.patches.custom_fields.vfd_providers_updated_custom_fields.execute", "csf_tz.patches.migrate_vfd_providers_to_csf_tz.execute", "csf_tz.patches.remove_ot_component_custom_fields.execute", - "csf_tz.patches.custom_fields.attendance_overtime_calculation_custom_fields.execute", + "csf_tz.patches.custom_fields.attendance_overtime_calculation_custom_fields.execute", ] # Desk Notifications diff --git a/csf_tz/kcb/api/kcb_api.py b/csf_tz/kcb/api/kcb_api.py index 5dbfa8fa..deac358f 100644 --- a/csf_tz/kcb/api/kcb_api.py +++ b/csf_tz/kcb/api/kcb_api.py @@ -1,228 +1,205 @@ # kcb_api.py # This file handles all REST API endpoints for KCB — token generation, file upload, and file status +import os + import frappe import requests -import os from frappe.utils.file_manager import get_file @frappe.whitelist() def is_kcb_enabled(): - settings = frappe.get_single("KCB Settings") - return settings.enabled + settings = frappe.get_single("KCB Settings") + return settings.enabled def _get_supporting_file_docs(doc): - attachments = frappe.get_all( - "File", - filters={ - "attached_to_doctype": "KCB Payments Initiation", - "attached_to_name": doc.name, - "is_folder": 0, - }, - fields=["name", "file_name", "file_url", "creation"], - order_by="creation asc", - ) + attachments = frappe.get_all( + "File", + filters={ + "attached_to_doctype": "KCB Payments Initiation", + "attached_to_name": doc.name, + "is_folder": 0, + }, + fields=["name", "file_name", "file_url", "creation"], + order_by="creation asc", + ) - excluded_urls = {doc.payment_file, doc.encrypted_file} - supporting_docs = [f for f in attachments if f.get("file_url") not in excluded_urls] + excluded_urls = {doc.payment_file, doc.encrypted_file} + supporting_docs = [f for f in attachments if f.get("file_url") not in excluded_urls] - if not supporting_docs: - frappe.throw( - "Attach at least one supporting document before submitting to KCB." - ) + if not supporting_docs: + frappe.throw("Attach at least one supporting document before submitting to KCB.") - return supporting_docs + return supporting_docs def get_kcb_token(): - cache_key = "kcb_token" # Cache key for the token - expiry_key = "kcb_token_expiry" # Cache key for the token expiry time - token = frappe.cache().get_value(cache_key) # Retrieve token from cache - expiry = frappe.cache().get_value(expiry_key) # Retrieve expiry time from cache - - if token and expiry: - from datetime import datetime - - # Check if the token is still valid - if datetime.strptime(expiry, "%Y-%m-%d %H:%M:%S") > datetime.now(): - return token # Return the valid token - - # Generate a new token if not cached or expired - config = frappe.get_single("KCB Settings") # Fetch KCB settings - password = config.get_password("password") - if not config.username or not password: - frappe.throw("KCB Settings username/password is missing.") - auth = (config.username, password) # Authentication credentials - headers = { - "Content-Type": "application/json", - "Accept": "application/json", - } - - response = requests.post( - config.token_url, headers=headers, auth=auth, timeout=30 - ) # Request a new token - - if response.status_code == 200: - token_data = response.json() # Parse the response - # Support both KCB formats: access_token/expires_in or bearer_token/expires_in_seconds - token = token_data.get("access_token") or token_data.get("bearer_token") - expires_in = int( - token_data.get("expires_in") - or token_data.get("expires_in_seconds") - or 3600 - ) # Extract expiry time (default 1 hour) - - if not token: - frappe.throw(f"Token generation failed: {token_data}") - - from datetime import datetime, timedelta - - expiry_time = datetime.now() + timedelta( - seconds=expires_in - 60 - ) # Set expiry 1 minute earlier - frappe.cache().set_value(cache_key, token) # Cache the token - frappe.cache().set_value( - expiry_key, expiry_time.strftime("%Y-%m-%d %H:%M:%S") - ) # Cache expiry time - - return token - else: - frappe.throw( - f"Token generation failed ({response.status_code}) from {config.token_url}: {response.text}" - ) + cache_key = "kcb_token" # Cache key for the token + expiry_key = "kcb_token_expiry" # Cache key for the token expiry time + token = frappe.cache().get_value(cache_key) # Retrieve token from cache + expiry = frappe.cache().get_value(expiry_key) # Retrieve expiry time from cache + + if token and expiry: + from datetime import datetime + + # Check if the token is still valid + if datetime.strptime(expiry, "%Y-%m-%d %H:%M:%S") > datetime.now(): + return token # Return the valid token + + # Generate a new token if not cached or expired + config = frappe.get_single("KCB Settings") # Fetch KCB settings + password = config.get_password("password") + if not config.username or not password: + frappe.throw("KCB Settings username/password is missing.") + auth = (config.username, password) # Authentication credentials + headers = { + "Content-Type": "application/json", + "Accept": "application/json", + } + + response = requests.post(config.token_url, headers=headers, auth=auth, timeout=30) # Request a new token + + if response.status_code == 200: + token_data = response.json() # Parse the response + # Support both KCB formats: access_token/expires_in or bearer_token/expires_in_seconds + token = token_data.get("access_token") or token_data.get("bearer_token") + expires_in = int( + token_data.get("expires_in") or token_data.get("expires_in_seconds") or 3600 + ) # Extract expiry time (default 1 hour) + + if not token: + frappe.throw(f"Token generation failed: {token_data}") + + from datetime import datetime, timedelta + + expiry_time = datetime.now() + timedelta(seconds=expires_in - 60) # Set expiry 1 minute earlier + frappe.cache().set_value(cache_key, token) # Cache the token + frappe.cache().set_value(expiry_key, expiry_time.strftime("%Y-%m-%d %H:%M:%S")) # Cache expiry time + + return token + else: + frappe.throw( + f"Token generation failed ({response.status_code}) from {config.token_url}: {response.text}" + ) def submit_file_details(doc): - config = frappe.get_single("KCB Settings") # Fetch KCB settings - token = get_kcb_token() # Get the token - - headers = { - "Authorization": f"Bearer {token}", # Bearer token for authorization - "Content-Type": "application/json", - } - - originator_id = getattr(doc, "originator_conversation_id", None) - if not originator_id: - originator_id = frappe.generate_hash(length=20) - doc.db_set("originator_conversation_id", originator_id, update_modified=False) - - supporting_docs = _get_supporting_file_docs(doc) - supporting_names = ", ".join( - [f.get("file_name", "") for f in supporting_docs if f.get("file_name")] - ) - - payload = { - "originatorConversationID": originator_id, # Unique ID for the conversation - "fileName": doc.encrypted_file.split("/")[ - -1 - ], # Extract file name from the file path - "supportingFilesNames": supporting_names, - "partnerCode": config.partner_code, # Partner code from settings - "processorCode": config.processor_code, # Processor code from settings - "subsidiaryCode": config.subsidiary_code, # Subsidiary code from settings - "templateName": config.template_name, # Template name from settings - "checkSum": doc.file_checksum, # File checksum - "checkSumSignature": doc.checksum_signature, # Checksum signature - } - - response = requests.post( - config.file_details_submission_url, json=payload, headers=headers - ) # Submit file details - - if response.status_code != 200: - frappe.throw( - f"File details submission failed: {response.text}" - ) # Raise error if submission fails - - return response.json() + config = frappe.get_single("KCB Settings") # Fetch KCB settings + token = get_kcb_token() # Get the token + + headers = { + "Authorization": f"Bearer {token}", # Bearer token for authorization + "Content-Type": "application/json", + } + + originator_id = getattr(doc, "originator_conversation_id", None) + if not originator_id: + originator_id = frappe.generate_hash(length=20) + doc.db_set("originator_conversation_id", originator_id, update_modified=False) + + supporting_docs = _get_supporting_file_docs(doc) + supporting_names = ", ".join([f.get("file_name", "") for f in supporting_docs if f.get("file_name")]) + + payload = { + "originatorConversationID": originator_id, # Unique ID for the conversation + "fileName": doc.encrypted_file.split("/")[-1], # Extract file name from the file path + "supportingFilesNames": supporting_names, + "partnerCode": config.partner_code, # Partner code from settings + "processorCode": config.processor_code, # Processor code from settings + "subsidiaryCode": config.subsidiary_code, # Subsidiary code from settings + "templateName": config.template_name, # Template name from settings + "checkSum": doc.file_checksum, # File checksum + "checkSumSignature": doc.checksum_signature, # Checksum signature + } + + response = requests.post( + config.file_details_submission_url, json=payload, headers=headers + ) # Submit file details + + if response.status_code != 200: + frappe.throw(f"File details submission failed: {response.text}") # Raise error if submission fails + + return response.json() def upload_encrypted_file(doc): - config = frappe.get_single("KCB Settings") # Fetch KCB settings - token = get_kcb_token() # Get the token - - file_doc = frappe.get_doc( - "File", {"file_url": doc.encrypted_file} - ) # Get the file document - file_content = get_file(file_doc.file_url)[1] # Retrieve the file content - - originator_id = getattr(doc, "originator_conversation_id", None) or doc.name - - supporting_docs = _get_supporting_file_docs(doc) - - # Bulk Receiver expects repeated `files` entries. - files = [ - ( - "files", - ( - file_doc.file_name, - file_content, - "application/octet-stream", - ), - ) - ] - for support_doc in supporting_docs: - support_content = get_file(support_doc.get("file_url"))[1] - files.append( - ( - "files", - ( - support_doc.get("file_name"), - support_content, - "application/octet-stream", - ), - ) - ) - - # Include originatorConversationID as form-data - files.append(("originatorConversationID", (None, originator_id))) - - headers = {"Authorization": f"Bearer {token}"} # Bearer token for authorization - - response = requests.post( - config.file_upload_url, headers=headers, files=files - ) # Upload the file - - if response.status_code != 200: - frappe.throw( - f"File upload failed: {response.text}" - ) # Raise error if upload fails - - return response.json() + config = frappe.get_single("KCB Settings") # Fetch KCB settings + token = get_kcb_token() # Get the token + + file_doc = frappe.get_doc("File", {"file_url": doc.encrypted_file}) # Get the file document + file_content = get_file(file_doc.file_url)[1] # Retrieve the file content + + originator_id = getattr(doc, "originator_conversation_id", None) or doc.name + + supporting_docs = _get_supporting_file_docs(doc) + + # Bulk Receiver expects repeated `files` entries. + files = [ + ( + "files", + ( + file_doc.file_name, + file_content, + "application/octet-stream", + ), + ) + ] + for support_doc in supporting_docs: + support_content = get_file(support_doc.get("file_url"))[1] + files.append( + ( + "files", + ( + support_doc.get("file_name"), + support_content, + "application/octet-stream", + ), + ) + ) + + # Include originatorConversationID as form-data + files.append(("originatorConversationID", (None, originator_id))) + + headers = {"Authorization": f"Bearer {token}"} # Bearer token for authorization + + response = requests.post(config.file_upload_url, headers=headers, files=files) # Upload the file + + if response.status_code != 200: + frappe.throw(f"File upload failed: {response.text}") # Raise error if upload fails + + return response.json() @frappe.whitelist() def check_file_status(docname: str): - doc = frappe.get_doc("KCB Payments Initiation", docname) - config = frappe.get_single("KCB Settings") - token = get_kcb_token() + doc = frappe.get_doc("KCB Payments Initiation", docname) + config = frappe.get_single("KCB Settings") + token = get_kcb_token() - originator_id = getattr(doc, "originator_conversation_id", None) - if not originator_id: - frappe.throw("Originator Conversation ID is missing on this document.") + originator_id = getattr(doc, "originator_conversation_id", None) + if not originator_id: + frappe.throw("Originator Conversation ID is missing on this document.") - if not doc.encrypted_file: - frappe.throw("Encrypted file is missing on this document.") + if not doc.encrypted_file: + frappe.throw("Encrypted file is missing on this document.") - file_name = os.path.basename(doc.encrypted_file) + file_name = os.path.basename(doc.encrypted_file) - payload = { - "fileName": file_name, - "partnerCode": config.partner_code, - "originatorConversationID": originator_id, - } - headers = { - "Authorization": f"Bearer {token}", - "Content-Type": "application/json", - } + payload = { + "fileName": file_name, + "partnerCode": config.partner_code, + "originatorConversationID": originator_id, + } + headers = { + "Authorization": f"Bearer {token}", + "Content-Type": "application/json", + } - response = requests.post( - config.file_status_check_url, json=payload, headers=headers, timeout=30 - ) + response = requests.post(config.file_status_check_url, json=payload, headers=headers, timeout=30) - if response.status_code != 200: - frappe.throw(f"File status check failed: {response.text}") + if response.status_code != 200: + frappe.throw(f"File status check failed: {response.text}") - return response.json() + return response.json() diff --git a/csf_tz/kcb/doctype/kcb_payments_initiation/kcb_payments_initiation.py b/csf_tz/kcb/doctype/kcb_payments_initiation/kcb_payments_initiation.py index 99d62806..ece645b6 100644 --- a/csf_tz/kcb/doctype/kcb_payments_initiation/kcb_payments_initiation.py +++ b/csf_tz/kcb/doctype/kcb_payments_initiation/kcb_payments_initiation.py @@ -3,85 +3,87 @@ import frappe from frappe.model.document import Document -from csf_tz.kcb.utils.crypto_utils import generate_checksum, sign_checksum_with_p12 + from csf_tz.kcb.api.kcb_api import submit_file_details, upload_encrypted_file from csf_tz.kcb.pgp import encrypt_pgp +from csf_tz.kcb.utils.crypto_utils import generate_checksum, sign_checksum_with_p12 def _clean(value) -> str: - if value is None: - return "" - text = str(value) - text = text.replace("|", " ").replace("\r", " ").replace("\n", " ") - return " ".join(text.split()) + if value is None: + return "" + text = str(value) + text = text.replace("|", " ").replace("\r", " ").replace("\n", " ") + return " ".join(text.split()) def _purpose(value) -> str: - return _clean(value)[:25] + return _clean(value)[:25] class KCBPaymentsInitiation(Document): - - def before_save(self): - header = "Debit Account|Beneficiary Name|Transaction Code|Amount|Currency|Beneficiary Account|Beneficiary Clearing Code|My Ref|Beneficiary Ref|CBK Code|Ordering Customer Physical Address|Payment Purpose" - - body_lines = [] - for item in self.kcb_payments_initiation_info: - line = ( - f"{_clean(self.debit_account)}|{_clean(item.beneficiary_name)}|{_clean(item.transaction_code)}|{_clean(item.amount)}|" - f"{_clean(item.currency)}|{_clean(item.beneficiary_account)}|{_clean(item.beneficiary_clearing_code)}|" - f"{_clean(item.my_ref)}|{_clean(item.beneficiary_ref)}|{_clean(item.cbk_code)}|" - f"{_clean(item.ordering_customer_physical_address)}|{_purpose(item.payment_purpose)}" - ) - body_lines.append(line) - - body = "\n".join(body_lines) - - total_amount = sum( - [item.amount for item in self.kcb_payments_initiation_info if item.amount] - ) - # Total is a trailer line (not a field per record) - file_content = f"{header}\n{body}\n{total_amount}" - file_bytes = file_content.encode("utf-8") - - self.file_checksum = generate_checksum(file_bytes) - - self.checksum_signature = sign_checksum_with_p12(self.file_checksum) - - settings = frappe.get_single("KCB Settings") - public_key = getattr(settings, "pgp_public_key", None) - if not public_key: - frappe.throw("KCB Settings PGP public key is missing.") - - encrypted_data = encrypt_pgp(file_bytes, public_key) - if not encrypted_data: - frappe.throw("Encryption failed: empty result") - - file_base_name = self.name - - txt_file = frappe.get_doc({ - "doctype": "File", - "file_name": f"{file_base_name}.txt", - "attached_to_doctype": "KCB Payments Initiation", - "attached_to_name": self.name, - "content": file_content, - "folder": "Home" - }) - txt_file.save() - - gpg_file = frappe.get_doc({ - "doctype": "File", - "file_name": f"{file_base_name}.txt.gpg", - "attached_to_doctype": "KCB Payments Initiation", - "attached_to_name": self.name, - "content": encrypted_data, - "folder": "Home" - }) - gpg_file.save() - - self.payment_file = txt_file.file_url - self.encrypted_file = gpg_file.file_url - - def on_submit(self): - submit_file_details(self) - upload_encrypted_file(self) + def before_save(self): + header = "Debit Account|Beneficiary Name|Transaction Code|Amount|Currency|Beneficiary Account|Beneficiary Clearing Code|My Ref|Beneficiary Ref|CBK Code|Ordering Customer Physical Address|Payment Purpose" + + body_lines = [] + for item in self.kcb_payments_initiation_info: + line = ( + f"{_clean(self.debit_account)}|{_clean(item.beneficiary_name)}|{_clean(item.transaction_code)}|{_clean(item.amount)}|" + f"{_clean(item.currency)}|{_clean(item.beneficiary_account)}|{_clean(item.beneficiary_clearing_code)}|" + f"{_clean(item.my_ref)}|{_clean(item.beneficiary_ref)}|{_clean(item.cbk_code)}|" + f"{_clean(item.ordering_customer_physical_address)}|{_purpose(item.payment_purpose)}" + ) + body_lines.append(line) + + body = "\n".join(body_lines) + + total_amount = sum([item.amount for item in self.kcb_payments_initiation_info if item.amount]) + # Total is a trailer line (not a field per record) + file_content = f"{header}\n{body}\n{total_amount}" + file_bytes = file_content.encode("utf-8") + + self.file_checksum = generate_checksum(file_bytes) + + self.checksum_signature = sign_checksum_with_p12(self.file_checksum) + + settings = frappe.get_single("KCB Settings") + public_key = getattr(settings, "pgp_public_key", None) + if not public_key: + frappe.throw("KCB Settings PGP public key is missing.") + + encrypted_data = encrypt_pgp(file_bytes, public_key) + if not encrypted_data: + frappe.throw("Encryption failed: empty result") + + file_base_name = self.name + + txt_file = frappe.get_doc( + { + "doctype": "File", + "file_name": f"{file_base_name}.txt", + "attached_to_doctype": "KCB Payments Initiation", + "attached_to_name": self.name, + "content": file_content, + "folder": "Home", + } + ) + txt_file.save() + + gpg_file = frappe.get_doc( + { + "doctype": "File", + "file_name": f"{file_base_name}.txt.gpg", + "attached_to_doctype": "KCB Payments Initiation", + "attached_to_name": self.name, + "content": encrypted_data, + "folder": "Home", + } + ) + gpg_file.save() + + self.payment_file = txt_file.file_url + self.encrypted_file = gpg_file.file_url + + def on_submit(self): + submit_file_details(self) + upload_encrypted_file(self) diff --git a/csf_tz/kcb/payments.py b/csf_tz/kcb/payments.py index 8ddcf530..74a4d15f 100644 --- a/csf_tz/kcb/payments.py +++ b/csf_tz/kcb/payments.py @@ -11,42 +11,46 @@ def _get_bank_account_details(bank_account_name: str | None) -> dict: - if not bank_account_name: - return {} - return frappe.get_value( - "Bank Account", - bank_account_name, - ["bank_account_no", "kcb_beneficiary_clearing_code", "bank"], - as_dict=True, - ) or {} + if not bank_account_name: + return {} + return ( + frappe.get_value( + "Bank Account", + bank_account_name, + ["bank_account_no", "kcb_beneficiary_clearing_code", "bank"], + as_dict=True, + ) + or {} + ) def _require_kcb_enabled(): - settings = frappe.get_single("KCB Settings") - if not settings.enabled: - frappe.throw(_("KCB Settings is disabled. Enable it to generate payments.")) + settings = frappe.get_single("KCB Settings") + if not settings.enabled: + frappe.throw(_("KCB Settings is disabled. Enable it to generate payments.")) def _validate_single_value(values, label): - unique_values = {v for v in values if v} - if len(unique_values) > 1: - frappe.throw(_("{0} must be the same for all entries.").format(label)) - return unique_values.pop() if unique_values else "" + unique_values = {v for v in values if v} + if len(unique_values) > 1: + frappe.throw(_("{0} must be the same for all entries.").format(label)) + return unique_values.pop() if unique_values else "" + def _attach_supplier_batch_summary_pdf(target_name: str, doc, pe_docs): - rows_html = "" - for pe in pe_docs: - rows_html += ( - "" - f"{escape_html(pe.name)}" - f"{escape_html(pe.party_name or pe.party or '')}" - f"{escape_html(str(pe.paid_amount or 0))}" - f"{escape_html(pe.paid_from_account_currency or '')}" - f"{escape_html(pe.party_bank_account or '')}" - "" - ) - - html = f""" + rows_html = "" + for pe in pe_docs: + rows_html += ( + "" + f"{escape_html(pe.name)}" + f"{escape_html(pe.party_name or pe.party or '')}" + f"{escape_html(str(pe.paid_amount or 0))}" + f"{escape_html(pe.paid_from_account_currency or '')}" + f"{escape_html(pe.party_bank_account or '')}" + "" + ) + + html = f""" \ No newline at end of file + diff --git a/csf_tz/public/js/jobcards/JobCards.vue b/csf_tz/public/js/jobcards/JobCards.vue index 1baa5805..cbcf1431 100644 --- a/csf_tz/public/js/jobcards/JobCards.vue +++ b/csf_tz/public/js/jobcards/JobCards.vue @@ -27,7 +27,7 @@ Satus: {{ item.status }} - Current Time: + Current Time: {{ get_current(item.current_time).hours }} : {{ get_current(item.current_time).minutes }} @@ -157,4 +157,4 @@ div.navbar .container { .img-border { border: 1px solid #BDBDBD; } - \ No newline at end of file + diff --git a/csf_tz/public/js/jobcards/bus.js b/csf_tz/public/js/jobcards/bus.js index c6ae75aa..28cb7a7b 100644 --- a/csf_tz/public/js/jobcards/bus.js +++ b/csf_tz/public/js/jobcards/bus.js @@ -1 +1 @@ -export const evntBus = new Vue(); \ No newline at end of file +export const evntBus = new Vue(); diff --git a/csf_tz/public/js/jobcards/jobcards.bundle.js b/csf_tz/public/js/jobcards/jobcards.bundle.js index eb78df29..6d0eaa38 100644 --- a/csf_tz/public/js/jobcards/jobcards.bundle.js +++ b/csf_tz/public/js/jobcards/jobcards.bundle.js @@ -8,17 +8,17 @@ class JobCardsBuilder { this.page = page; this.init(); } - + init() { // Create Vuetify instance const vuetify = createVuetify(); - + // Create Vue app const app = createApp(JobCardsComponent); - + // Use Vuetify app.use(vuetify); - + // Mount the app this.vue = app.mount(this.$wrapper[0]); } diff --git a/csf_tz/public/js/jobcards/jobcards.js b/csf_tz/public/js/jobcards/jobcards.js index e30d5047..bd9e8e9a 100644 --- a/csf_tz/public/js/jobcards/jobcards.js +++ b/csf_tz/public/js/jobcards/jobcards.js @@ -9,10 +9,10 @@ frappe.JobCards.job_cards = class { this.page = parent.page; this.make_body(); } - + make_body() { this.$EL = this.$parent.find('.layout-main'); - + // Check if Vue bundle is available and load Vue component if (frappe.JobCards.JobCardsBuilder) { this.load_vue_component(); @@ -22,7 +22,7 @@ frappe.JobCards.job_cards = class { this.load_fallback(); } } - + load_vue_component() { // Use the Vue-based JobCards builder this.vue_builder = new frappe.JobCards.JobCardsBuilder({ @@ -30,11 +30,11 @@ frappe.JobCards.job_cards = class { page: this.page }); } - + load_fallback() { // Fallback: Simple placeholder this.$EL.html('

Job Cards functionality loading...

'); - + // Try again after a short delay in case the bundle is still loading setTimeout(() => { if (frappe.JobCards.JobCardsBuilder) { @@ -42,8 +42,8 @@ frappe.JobCards.job_cards = class { } }, 1000); } - + setup_header() { // Header setup functionality } -}; \ No newline at end of file +}; diff --git a/csf_tz/public/js/po_shortcuts.js b/csf_tz/public/js/po_shortcuts.js index 8bd29ca3..4772f68f 100644 --- a/csf_tz/public/js/po_shortcuts.js +++ b/csf_tz/public/js/po_shortcuts.js @@ -158,4 +158,4 @@ function ctrlU (TableName) { } } }); -} \ No newline at end of file +} diff --git a/csf_tz/public/js/select_dialog.js b/csf_tz/public/js/select_dialog.js index 84dbd9f7..97695f1d 100644 --- a/csf_tz/public/js/select_dialog.js +++ b/csf_tz/public/js/select_dialog.js @@ -247,4 +247,3 @@ frappe.ui.form.SelectDialog = Class.extend({ }); }, }); - diff --git a/csf_tz/public/js/shortcuts.js b/csf_tz/public/js/shortcuts.js index 2f440c22..d321fa6b 100644 --- a/csf_tz/public/js/shortcuts.js +++ b/csf_tz/public/js/shortcuts.js @@ -231,4 +231,4 @@ function ctrlU (TableName) { } } }); -} \ No newline at end of file +} diff --git a/csf_tz/public/js/to_console.js b/csf_tz/public/js/to_console.js index 0e91cc0f..4866e4fb 100644 --- a/csf_tz/public/js/to_console.js +++ b/csf_tz/public/js/to_console.js @@ -5,4 +5,4 @@ $(function() { console.log(element); }); }); -}); \ No newline at end of file +}); diff --git a/csf_tz/purchase_and_stock_management/doctype/bin_list/bin_list.py b/csf_tz/purchase_and_stock_management/doctype/bin_list/bin_list.py index 19e83a2a..07c69ab7 100644 --- a/csf_tz/purchase_and_stock_management/doctype/bin_list/bin_list.py +++ b/csf_tz/purchase_and_stock_management/doctype/bin_list/bin_list.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class BinList(Document): pass diff --git a/csf_tz/purchase_and_stock_management/doctype/bin_setup/bin_setup.py b/csf_tz/purchase_and_stock_management/doctype/bin_setup/bin_setup.py index b955db48..c6c46159 100644 --- a/csf_tz/purchase_and_stock_management/doctype/bin_setup/bin_setup.py +++ b/csf_tz/purchase_and_stock_management/doctype/bin_setup/bin_setup.py @@ -1,20 +1,25 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe.model.document import Document + class BinSetup(Document): def validate(self): self.before_save() def before_save(self): for d in self.bin_table: - if d.new_label: - bin_no = frappe.db.get_value('Bin', {"item_code": d.item_code, "warehouse": d.warehouse, }, "name") + bin_no = frappe.db.get_value( + "Bin", + { + "item_code": d.item_code, + "warehouse": d.warehouse, + }, + "name", + ) if bin_no: doc = frappe.get_doc("Bin", bin_no) doc.bin_label = d.new_label diff --git a/csf_tz/purchase_and_stock_management/doctype/bin_setup/test_bin_setup.py b/csf_tz/purchase_and_stock_management/doctype/bin_setup/test_bin_setup.py index c881f1b1..e193e4ce 100644 --- a/csf_tz/purchase_and_stock_management/doctype/bin_setup/test_bin_setup.py +++ b/csf_tz/purchase_and_stock_management/doctype/bin_setup/test_bin_setup.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestBinSetup(unittest.TestCase): pass diff --git a/csf_tz/purchase_and_stock_management/doctype/item_number/item_number.py b/csf_tz/purchase_and_stock_management/doctype/item_number/item_number.py index 2b7209bb..84a4d5b4 100644 --- a/csf_tz/purchase_and_stock_management/doctype/item_number/item_number.py +++ b/csf_tz/purchase_and_stock_management/doctype/item_number/item_number.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class ItemNumber(Document): pass diff --git a/csf_tz/purchase_and_stock_management/doctype/item_number/test_item_number.py b/csf_tz/purchase_and_stock_management/doctype/item_number/test_item_number.py index 1a73e7da..98d09b19 100644 --- a/csf_tz/purchase_and_stock_management/doctype/item_number/test_item_number.py +++ b/csf_tz/purchase_and_stock_management/doctype/item_number/test_item_number.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestItemNumber(unittest.TestCase): pass diff --git a/csf_tz/purchase_and_stock_management/doctype/order_track/order_track.js b/csf_tz/purchase_and_stock_management/doctype/order_track/order_track.js index 6c4acf3b..fd9181bb 100644 --- a/csf_tz/purchase_and_stock_management/doctype/order_track/order_track.js +++ b/csf_tz/purchase_and_stock_management/doctype/order_track/order_track.js @@ -2,61 +2,61 @@ // For license information, please see license.txt frappe.ui.form.on('Order Track', { - + refresh: function(frm) { frm.events.show_hide_fields(frm); console.log(frm); //console.log(hide_show_sections.name); //alert(cur_frm.doc.docstatus) - + //make product inspection ie. submitted if(cur_frm.doc.docstatus === 1 ) { cur_frm.add_custom_button(__('Product Inspection'), function(){frm.events.make_product_inspection(frm)}, __("Make")); - - + + } - - - + + + //Arrival date entered,clearing company and completion date ! blank if (frm.doc.arrival_date && frm.doc.arrival_date != null ){ if (frm.doc.clearing_company == '' || (frm.doc.expected_clearing_completion_date ==null)){ var msg = "Either Clearing Company or Clearing Completion Date is unfilled,Please fill the fields"; frappe.msgprint(msg); throw msg; - + } } }, - - + + show_hide_fields:function(frm){ frm.toggle_display('section_international_supplier',(frm.doc.supplier && frm.doc.supplier_type && frm.doc.supplier_type=='International Supplier' )); frm.toggle_display('section_containers',(frm.doc.supplier && frm.doc.supplier_type && frm.doc.supplier_type=='International Supplier')); frm.toggle_display('section_local_supplier', (frm.doc.supplier && frm.doc.supplier_type && frm.doc.supplier_type=='Local Supplier')); frm.toggle_display('section_order_progress',(frm.doc.supplier && frm.doc.supplier_type)); frm.toggle_display('section_items_ordered', (frm.doc.supplier && frm.doc.supplier_type)); - frm.toggle_display('section_status', (frm.doc.supplier && frm.doc.supplier_type)); + frm.toggle_display('section_status', (frm.doc.supplier && frm.doc.supplier_type)); }, - - + + supplier:function(frm){ frm.events.show_hide_fields(frm); - + }, - + supplier_type:function(frm){ frm.events.show_hide_fields(frm); }, - + //Product Inspection function make_product_inspection:function(){ frappe.model.open_mapped_doc({ method: "erpnext.purchase_and_stock_management.doctype.order_track.order_track.make_product_inspection", frm: cur_frm }) - - }, + + }, }); diff --git a/csf_tz/purchase_and_stock_management/doctype/order_track/order_track.py b/csf_tz/purchase_and_stock_management/doctype/order_track/order_track.py index 9ee66a38..e500c05e 100644 --- a/csf_tz/purchase_and_stock_management/doctype/order_track/order_track.py +++ b/csf_tz/purchase_and_stock_management/doctype/order_track/order_track.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class OrderTrack(Document): pass diff --git a/csf_tz/purchase_and_stock_management/doctype/order_track/test_order_track.py b/csf_tz/purchase_and_stock_management/doctype/order_track/test_order_track.py index 7675ce16..85f65872 100644 --- a/csf_tz/purchase_and_stock_management/doctype/order_track/test_order_track.py +++ b/csf_tz/purchase_and_stock_management/doctype/order_track/test_order_track.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestOrderTrack(unittest.TestCase): pass diff --git a/csf_tz/purchase_and_stock_management/doctype/order_tracking_container/order_tracking_container.py b/csf_tz/purchase_and_stock_management/doctype/order_tracking_container/order_tracking_container.py index 491d7a51..1d207dc2 100644 --- a/csf_tz/purchase_and_stock_management/doctype/order_tracking_container/order_tracking_container.py +++ b/csf_tz/purchase_and_stock_management/doctype/order_tracking_container/order_tracking_container.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class OrderTrackingContainer(Document): pass diff --git a/csf_tz/purchase_and_stock_management/doctype/purchase_and_stock_management_test/purchase_and_stock_management_test.py b/csf_tz/purchase_and_stock_management/doctype/purchase_and_stock_management_test/purchase_and_stock_management_test.py index 80d616c1..33dda281 100644 --- a/csf_tz/purchase_and_stock_management/doctype/purchase_and_stock_management_test/purchase_and_stock_management_test.py +++ b/csf_tz/purchase_and_stock_management/doctype/purchase_and_stock_management_test/purchase_and_stock_management_test.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class PurchaseAndStockManagementTest(Document): pass diff --git a/csf_tz/purchase_and_stock_management/doctype/purchase_and_stock_management_test/test_purchase_and_stock_management_test.py b/csf_tz/purchase_and_stock_management/doctype/purchase_and_stock_management_test/test_purchase_and_stock_management_test.py index 1d9b1f30..2c58fd9e 100644 --- a/csf_tz/purchase_and_stock_management/doctype/purchase_and_stock_management_test/test_purchase_and_stock_management_test.py +++ b/csf_tz/purchase_and_stock_management/doctype/purchase_and_stock_management_test/test_purchase_and_stock_management_test.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestPurchaseAndStockManagementTest(unittest.TestCase): pass diff --git a/csf_tz/purchase_and_stock_management/report/ordered_items_to_be_delivered/ordered_items_to_be_delivered.py b/csf_tz/purchase_and_stock_management/report/ordered_items_to_be_delivered/ordered_items_to_be_delivered.py index 70a80ae4..d1f15482 100644 --- a/csf_tz/purchase_and_stock_management/report/ordered_items_to_be_delivered/ordered_items_to_be_delivered.py +++ b/csf_tz/purchase_and_stock_management/report/ordered_items_to_be_delivered/ordered_items_to_be_delivered.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -9,35 +8,20 @@ def execute(filters=None): columns, data = [], [] columns = [ - { - "fieldname": "name", - "label" : _("Sales Order"), - "fieldtype": "Link", - "options": "Sales Order" - }, + {"fieldname": "name", "label": _("Sales Order"), "fieldtype": "Link", "options": "Sales Order"}, { "fieldname": "customer", "label": _("Customer"), "fieldtype": "Link", "options": "Customer", }, - { - "fieldname": "customer_name", - "label": _("Customer Name"), - "fieldtype": "Data", - "width": 150 - }, + {"fieldname": "customer_name", "label": _("Customer Name"), "fieldtype": "Data", "width": 150}, { "fieldname": "transaction_date", "label": _("Date "), "fieldtype": "Date", }, - { - "fieldname": "project", - "label": _("Project"), - "fieldtype": "Data", - "width": 150 - }, + {"fieldname": "project", "label": _("Project"), "fieldtype": "Data", "width": 150}, { "fieldname": "item_code", "label": _("Item Code"), @@ -49,113 +33,55 @@ def execute(filters=None): "label": _("Req By Date "), "fieldtype": "Date", }, - - { - "fieldname": "qty", - "label": _("Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "delivered_qty", - "label": _("Delivered Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "qty_to_deliver", - "label": _("Qty to Deliver"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "base_rate", - "label": _("Rate"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "base_amount", - "label": _("Amount"), - "fieldtype": "Float", - "width": 150 - }, + {"fieldname": "qty", "label": _("Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "delivered_qty", "label": _("Delivered Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "qty_to_deliver", "label": _("Qty to Deliver"), "fieldtype": "Float", "width": 150}, + {"fieldname": "base_rate", "label": _("Rate"), "fieldtype": "Float", "width": 150}, + {"fieldname": "base_amount", "label": _("Amount"), "fieldtype": "Float", "width": 150}, { "fieldname": "amount_to_deliver", "label": _("Amount To Deliver"), "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "actual_qty", - "label": _("Available Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "projected_qty", - "label": _("Projected Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "delivery_date", - "label": _("Item Delivery Date"), - "fieldtype": "Date", - "width": 150 - }, - { - "fieldname": "delay_days", - "label": _("Delay Days"), - "fieldtype": "Int", - "width": 150 - }, - { - "fieldname": "item_name", - "label": _("Item Name"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 200 - }, - { - "fieldname": "item_group", - "label": _("Item Group"), - "fieldtype": "Data", - "width": 200 + "width": 150, }, + {"fieldname": "actual_qty", "label": _("Available Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "projected_qty", "label": _("Projected Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "delivery_date", "label": _("Item Delivery Date"), "fieldtype": "Date", "width": 150}, + {"fieldname": "delay_days", "label": _("Delay Days"), "fieldtype": "Int", "width": 150}, + {"fieldname": "item_name", "label": _("Item Name"), "fieldtype": "Data", "width": 150}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 200}, + {"fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Data", "width": 200}, { "fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Link", "options": "Warehouse", - "width": 150 + "width": 150, }, ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } where = "" if filters.sales_order: - where += ' AND tso.name = %(sales_order)s ' + where += " AND tso.name = %(sales_order)s " where_filter.update({"sales_order": filters.sales_order}) if filters.item_code: - where += ' AND so_itm.item_code = %(item_code)s ' + where += " AND so_itm.item_code = %(item_code)s " where_filter.update({"item_code": filters.item_code}) if filters.customer: - where += ' AND tso.customer = %(customer)s ' + where += " AND tso.customer = %(customer)s " where_filter.update({"customer": filters.customer}) if filters.warehouse: - where += ' AND so_itm.warehouse = %(warehouse)s ' + where += " AND so_itm.warehouse = %(warehouse)s " where_filter.update({"warehouse": filters.warehouse}) - - data = frappe.db.sql('''SELECT + data = frappe.db.sql( + """SELECT tso.name, tso.customer, tso.customer_name, @@ -167,7 +93,7 @@ def execute(filters=None): (so_itm.qty - ifnull(so_itm.delivered_qty, 0)) AS qty_to_deliver, so_itm.base_rate, so_itm.base_amount, - ((so_itm.qty - ifnull(so_itm.delivered_qty, 0))* so_itm.base_rate) + ((so_itm.qty - ifnull(so_itm.delivered_qty, 0))* so_itm.base_rate) AS amount_to_deliver, bin.actual_qty, bin.projected_qty, @@ -177,19 +103,22 @@ def execute(filters=None): so_itm.description, so_itm.item_group, so_itm.warehouse - FROM + FROM (`tabSales Order` tso) JOIN (`tabSales Order Item` so_itm) - LEFT JOIN + LEFT JOIN (tabBin AS bin ) ON (bin.item_code = so_itm.item_code and bin.warehouse = so_itm.warehouse) WHERE so_itm.parent = tso.name AND tso.docstatus = 1 AND tso.status not in ("Stopped", "Closed") - AND ifnull(so_itm.delivered_qty, 0) < ifnull(so_itm.qty, 0) + AND ifnull(so_itm.delivered_qty, 0) < ifnull(so_itm.qty, 0) /* ORDER BY tso.transaction_date -*/ '''+ where, - where_filter, as_dict=1,as_list=1 - ); +*/ """ + + where, + where_filter, + as_dict=1, + as_list=1, + ) return columns, data diff --git a/csf_tz/purchase_and_stock_management/report/pending_ordered_items/pending_ordered_items.py b/csf_tz/purchase_and_stock_management/report/pending_ordered_items/pending_ordered_items.py index d9b03561..8e417646 100644 --- a/csf_tz/purchase_and_stock_management/report/pending_ordered_items/pending_ordered_items.py +++ b/csf_tz/purchase_and_stock_management/report/pending_ordered_items/pending_ordered_items.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -9,12 +8,7 @@ def execute(filters=None): columns, data = [], [] columns = [ - { - "fieldname": "name", - "label" : _("Purchase Order"), - "fieldtype": "Link", - "options": "Purchase Order" - }, + {"fieldname": "name", "label": _("Purchase Order"), "fieldtype": "Link", "options": "Purchase Order"}, { "fieldname": "transaction_date", "label": _("Date "), @@ -31,95 +25,58 @@ def execute(filters=None): "fieldtype": "Link", "options": "Supplier", }, - { - "fieldname": "supplier_name", - "label": _("Supplier Name"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "project", - "label": _("Project"), - "fieldtype": "Data", - "width": 150 - }, + {"fieldname": "supplier_name", "label": _("Supplier Name"), "fieldtype": "Data", "width": 150}, + {"fieldname": "project", "label": _("Project"), "fieldtype": "Data", "width": 150}, { "fieldname": "item_code", "label": _("Item Code"), "fieldtype": "Link", "options": "Item", }, - { - "fieldname": "qty", - "label": _("Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "received_qty", - "label": _("Received Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "qty_to_receive", - "label": _("Qty To Receive"), - "fieldtype": "Float", - "width": 150 - }, + {"fieldname": "qty", "label": _("Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "received_qty", "label": _("Received Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "qty_to_receive", "label": _("Qty To Receive"), "fieldtype": "Float", "width": 150}, { "fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Link", "options": "Warehouse", - "width": 150 - }, - { - "fieldname": "item_name", - "label": _("Item Name"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 200 - }, - { - "fieldname": "brand", - "label": _("Brand"), - "fieldtype": "Data", - "width": 100 + "width": 150, }, + {"fieldname": "item_name", "label": _("Item Name"), "fieldtype": "Data", "width": 150}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 200}, + {"fieldname": "brand", "label": _("Brand"), "fieldtype": "Data", "width": 100}, { "fieldname": "company", "label": _("Company"), "fieldtype": "Link", "options": "Company", - "width": 150 + "width": 150, }, - ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } where = "" if filters.purchase_order: - where += ' AND tpo.name = %(purchase_order)s ' + where += " AND tpo.name = %(purchase_order)s " where_filter.update({"purchase_order": filters.purchase_order}) if filters.item_code: - where += ' AND tpoi.item_code = %(item_code)s ' + where += " AND tpoi.item_code = %(item_code)s " where_filter.update({"item_code": filters.item_code}) if filters.warehouse: - where += ' AND tpoi.warehouse = %(warehouse)s ' + where += " AND tpoi.warehouse = %(warehouse)s " where_filter.update({"warehouse": filters.warehouse}) if filters.supplier: - where += ' AND tpo.supplier = %(supplier)s ' + where += " AND tpo.supplier = %(supplier)s " where_filter.update({"supplier": filters.supplier}) - data = frappe.db.sql('''SELECT + data = frappe.db.sql( + """SELECT tpo.name , tpo.transaction_date , tpoi.schedule_date AS req_by, @@ -128,14 +85,14 @@ def execute(filters=None): tpoi.project, tpoi.item_code, tpoi.qty , - tpoi.received_qty, + tpoi.received_qty, (tpoi.qty - ifnull(tpoi.received_qty, 0)) AS qty_to_receive, tpoi.warehouse, tpoi.item_name, tpoi.description, tpoi.brand, tpo.company - FROM + FROM (`tabPurchase Order` tpo), (`tabPurchase Order Item` tpoi) /*LEFT JOIN (`tabPurchase Order Item` tpoi) ON (tpoi.parent = tpo.name)*/ @@ -144,10 +101,13 @@ def execute(filters=None): and tpo.docstatus = 1 and tpo.status not in ("Stopped", "Closed") and ifnull(tpoi.received_qty, 0) < ifnull(tpoi.qty, 0) - AND tpo.transaction_date BETWEEN %(from_date)s AND %(to_date)s -/* ORDER BY tpo.transaction_date -*/ '''+ where, - where_filter, as_dict=1,as_list=1 - ); + AND tpo.transaction_date BETWEEN %(from_date)s AND %(to_date)s +/* ORDER BY tpo.transaction_date +*/ """ + + where, + where_filter, + as_dict=1, + as_list=1, + ) return columns, data diff --git a/csf_tz/purchase_and_stock_management/report/purchase_history/purchase_history.py b/csf_tz/purchase_and_stock_management/report/purchase_history/purchase_history.py index 55c48d3b..ce70679b 100644 --- a/csf_tz/purchase_and_stock_management/report/purchase_history/purchase_history.py +++ b/csf_tz/purchase_and_stock_management/report/purchase_history/purchase_history.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -15,56 +14,19 @@ def execute(filters=None): "fieldtype": "Link", "options": "Item", }, - { - "fieldname": "item_name", - "label": _("Item Name"), - "fieldtype": "Data", - "width": 150 - }, + {"fieldname": "item_name", "label": _("Item Name"), "fieldtype": "Data", "width": 150}, { "fieldname": "item_group", "label": _("Item Group"), "fieldtype": "Link", "options": "Item Group", }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 200 - }, - { - "fieldname": "qty", - "label": _("Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "uom", - "label": _("UOM"), - "fieldtype": "Link", - "options": "UOM", - "width": 80 - - }, - { - "fieldname": "base_rate", - "label": _("Rate"), - "fieldtype": "Currency", - "width": 80 - }, - { - "fieldname": "base_amount", - "label": _("Amount"), - "fieldtype": "Currency", - "width": 80 - }, - { - "fieldname": "name", - "label" : _("Purchase Order"), - "fieldtype": "Link", - "options": "Purchase Order" - }, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 200}, + {"fieldname": "qty", "label": _("Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Link", "options": "UOM", "width": 80}, + {"fieldname": "base_rate", "label": _("Rate"), "fieldtype": "Currency", "width": 80}, + {"fieldname": "base_amount", "label": _("Amount"), "fieldtype": "Currency", "width": 80}, + {"fieldname": "name", "label": _("Purchase Order"), "fieldtype": "Link", "options": "Purchase Order"}, { "fieldname": "transaction_date", "label": _("Date "), @@ -76,49 +38,37 @@ def execute(filters=None): "fieldtype": "Link", "options": "Supplier", }, - { - "fieldname": "supplier_name", - "label": _("Supplier Name"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "project", - "label": _("Project"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "received_qty", - "label": _("Received Qty"), - "fieldtype": "Float", - "width": 150 - }, + {"fieldname": "supplier_name", "label": _("Supplier Name"), "fieldtype": "Data", "width": 150}, + {"fieldname": "project", "label": _("Project"), "fieldtype": "Data", "width": 150}, + {"fieldname": "received_qty", "label": _("Received Qty"), "fieldtype": "Float", "width": 150}, { "fieldname": "company", "label": _("Company"), "fieldtype": "Link", "options": "Company", - "width": 150 + "width": 150, }, - ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } where = "" if filters.purchase_order: - where += ' AND tpo.name = %(purchase_order)s ' + where += " AND tpo.name = %(purchase_order)s " where_filter.update({"purchase_order": filters.purchase_order}) if filters.item_code: - where += ' AND po_item.item_code = %(item_code)s ' + where += " AND po_item.item_code = %(item_code)s " where_filter.update({"item_code": filters.item_code}) if filters.supplier: - where += ' AND tpo.supplier = %(supplier)s ' + where += " AND tpo.supplier = %(supplier)s " where_filter.update({"supplier": filters.supplier}) - data = frappe.db.sql('''SELECT + data = frappe.db.sql( + """SELECT po_item.item_code, po_item.item_name, po_item.item_group, @@ -126,7 +76,7 @@ def execute(filters=None): po_item.qty , po_item.uom, po_item.base_rate, - po_item.base_amount, + po_item.base_amount, tpo.name , tpo.transaction_date , @@ -134,18 +84,21 @@ def execute(filters=None): sup.supplier_name, po_item.project, ifnull(po_item.received_qty, 0) AS received_qty, - tpo.company - - FROM + tpo.company + + FROM (`tabPurchase Order` tpo), (`tabPurchase Order Item` po_item), (`tabSupplier` sup) /*LEFT JOIN (`tabPurchase Order Item` po_item) ON (po_item.parent = tpo.name)*/ WHERE po_item.parent = tpo.name - AND tpo.supplier = sup.name + AND tpo.supplier = sup.name AND tpo.transaction_date BETWEEN %(from_date)s AND %(to_date)s - AND tpo.docstatus = 1 '''+ where, - where_filter, as_dict=1,as_list=1 - ); + AND tpo.docstatus = 1 """ + + where, + where_filter, + as_dict=1, + as_list=1, + ) return columns, data diff --git a/csf_tz/purchase_and_stock_management/report/reordering_items/reordering_items.py b/csf_tz/purchase_and_stock_management/report/reordering_items/reordering_items.py index 1bcb7799..0e55fc4d 100644 --- a/csf_tz/purchase_and_stock_management/report/reordering_items/reordering_items.py +++ b/csf_tz/purchase_and_stock_management/report/reordering_items/reordering_items.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -11,9 +10,9 @@ def execute(filters=None): columns = [ { "fieldname": "name", - "label" : _("Material Request"), + "label": _("Material Request"), "fieldtype": "Link", - "options": "Material Request" + "options": "Material Request", }, { "fieldname": "transaction_date", @@ -26,69 +25,47 @@ def execute(filters=None): "fieldtype": "Link", "options": "Item", }, - { - "fieldname": "qty", - "label": _("Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "ordered_qty", - "label": _("Ordered Qty"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "qty_to_order", - "label": _("Qty To Order"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "item_name", - "label": _("Item Name"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "description", - "label": _("Description"), - "fieldtype": "Data", - "width": 150 - }, + {"fieldname": "qty", "label": _("Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "ordered_qty", "label": _("Ordered Qty"), "fieldtype": "Float", "width": 150}, + {"fieldname": "qty_to_order", "label": _("Qty To Order"), "fieldtype": "Float", "width": 150}, + {"fieldname": "item_name", "label": _("Item Name"), "fieldtype": "Data", "width": 150}, + {"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 150}, { "fieldname": "company", "label": _("Company"), "fieldtype": "Link", "options": "Company", - "width": 150 + "width": 150, }, - ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } where = "" if filters.material_request: - where += ' AND tmr.name = %(material_request)s ' + where += " AND tmr.name = %(material_request)s " where_filter.update({"material_request": filters.material_request}) if filters.item_code: - where += ' AND tmri.item_code = %(item_code)s ' + where += " AND tmri.item_code = %(item_code)s " where_filter.update({"item_code": filters.item_code}) - data = frappe.db.sql('''SELECT + data = frappe.db.sql( + """SELECT tmr.name , tmr.transaction_date , tmr.company, tmri.item_code, sum(ifnull(tmri.qty, 0)) AS qty, - sum(ifnull(tmri.ordered_qty, 0)) AS ordered_qty, + sum(ifnull(tmri.ordered_qty, 0)) AS ordered_qty, (sum(tmri.qty) - sum(ifnull(tmri.ordered_qty, 0))) AS qty_to_order, tmri.item_name, tmri.description, tmr.company - FROM + FROM (`tabMaterial Request` tmr), (`tabMaterial Request Item` tmri) WHERE tmri.parent = tmr.name @@ -99,9 +76,12 @@ def execute(filters=None): GROUP BY tmr.name, tmri.item_code HAVING sum(ifnull(tmri.ordered_qty, 0)) < sum(ifnull(tmri.qty, 0)) -/* ORDER BY tmr.transaction_date -*/ '''+ where, - where_filter, as_dict=1,as_list=1 - ); +/* ORDER BY tmr.transaction_date +*/ """ + + where, + where_filter, + as_dict=1, + as_list=1, + ) return columns, data diff --git a/csf_tz/purchase_and_stock_management/report/shipment_tracking/shipment_tracking.py b/csf_tz/purchase_and_stock_management/report/shipment_tracking/shipment_tracking.py index b4012c68..5c962254 100644 --- a/csf_tz/purchase_and_stock_management/report/shipment_tracking/shipment_tracking.py +++ b/csf_tz/purchase_and_stock_management/report/shipment_tracking/shipment_tracking.py @@ -1,27 +1,22 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ + def execute(filters=None): columns, data = [], [] - + columns = [ { - "fieldname": "order_no", - "label": _("Order No"), - "fieldtype": "Link", - "options": "Order Tracking", - "width": 150 - }, - { - "fieldname": "project", - "label" : _("Project"), + "fieldname": "order_no", + "label": _("Order No"), "fieldtype": "Link", - "options": "Project" + "options": "Order Tracking", + "width": 150, }, + {"fieldname": "project", "label": _("Project"), "fieldtype": "Link", "options": "Project"}, { "fieldname": "supplier", "label": _("Supplier"), @@ -29,46 +24,26 @@ def execute(filters=None): "options": "Supplier", }, { - "fieldname": "mode_of_transport", - "label": _("Mode of Transport"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "shipped_date", - "label": _("Shipping Date"), - "fieldtype": "Date", - "width": 150 + "fieldname": "mode_of_transport", + "label": _("Mode of Transport"), + "fieldtype": "Data", + "width": 150, }, + {"fieldname": "shipped_date", "label": _("Shipping Date"), "fieldtype": "Date", "width": 150}, { "fieldname": "expected_arrival_date", "label": _("Expected Arrival Date"), "fieldtype": "Date", - "width": 150 + "width": 150, }, { "fieldname": "arrival_date", "label": _("Arrival Date"), "fieldtype": "Date", }, - { - "fieldname": "order_status", - "label": _("Status"), - "fieldtype": "Data", - "width": 200 - }, - { - "fieldname": "bl_number", - "label": _("Bl No"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "container_no", - "label": _("Container"), - "fieldtype": "Data", - "width": 150 - }, + {"fieldname": "order_status", "label": _("Status"), "fieldtype": "Data", "width": 200}, + {"fieldname": "bl_number", "label": _("Bl No"), "fieldtype": "Data", "width": 150}, + {"fieldname": "container_no", "label": _("Container"), "fieldtype": "Data", "width": 150}, { "fieldname": "container_size", "label": _("Container Size"), @@ -80,37 +55,40 @@ def execute(filters=None): "fieldtype": "Data", }, { - "fieldname": "clearing_completion_date", - "label": _("Clearing Completion Date"), - "fieldtype": "Date", + "fieldname": "clearing_completion_date", + "label": _("Clearing Completion Date"), + "fieldtype": "Date", }, { - "fieldname": "delivered_date", - "label": _("Delivered Date"), - "fieldtype": "Date", + "fieldname": "delivered_date", + "label": _("Delivered Date"), + "fieldtype": "Date", }, { - "fieldname": "offloading_date", - "label": _("Off-Loading Date"), - "fieldtype": "Date", + "fieldname": "offloading_date", + "label": _("Off-Loading Date"), + "fieldtype": "Date", }, - ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } where = "" if filters.order: - where += ' AND tot.name = %(order)s ' + where += " AND tot.name = %(order)s " where_filter.update({"order": filters.order}) - + if filters.supplier: - where += ' AND tot.supplier = %(supplier)s ' + where += " AND tot.supplier = %(supplier)s " where_filter.update({"supplier": filters.supplier}) - - data = frappe.db.sql('''SELECT + + data = frappe.db.sql( + """SELECT tot.name AS order_no, tot.supplier, tot.project, @@ -126,13 +104,13 @@ def execute(filters=None): tc.size, tc.no_of_packages,*/ - (SELECT - CONCAT(op.date, " : ", op.current_location, ":", op.status) - FROM + (SELECT + CONCAT(op.date, " : ", op.current_location, ":", op.status) + FROM `tabOrder Progress` AS op WHERE tot.name = op.parent - ORDER BY + ORDER BY op.date DESC LIMIT 0,1 ) AS order_status @@ -143,30 +121,38 @@ def execute(filters=None): ON (tot.name = tc.parent)*/ Where tot.expected_arrival_date BETWEEN %(from_date)s AND %(to_date)s - '''+ where, - where_filter, as_dict=1) + """ + + where, + where_filter, + as_dict=1, + ) for order in data: # # For container info # - container_info = frappe.db.sql('''SELECT + container_info = frappe.db.sql( + """SELECT container_no, size,no_of_packages - FROM - (`tabContainer` tc) + FROM + (`tabContainer` tc) LEFT JOIN (`tabOrder Tracking` tot) ON (tot.name = tc.parent) - WHERE - tot.name = %(parent)s ''', - {"parent": order.order_no,}, as_dict=1) - order.container_no = '' - order.container_size='' - order.no_of_packages='' + WHERE + tot.name = %(parent)s """, + { + "parent": order.order_no, + }, + as_dict=1, + ) + order.container_no = "" + order.container_size = "" + order.no_of_packages = "" for co in container_info: - order.container_no += str(co.container_no) + ',' - order.container_size += str(co.size) + ',' - order.no_of_packages += str(co.no_of_packages) + ',' + order.container_no += str(co.container_no) + "," + order.container_size += str(co.size) + "," + order.no_of_packages += str(co.no_of_packages) + "," return columns, data diff --git a/csf_tz/purchase_and_stock_management/report/supplier_contacts/supplier_contacts.py b/csf_tz/purchase_and_stock_management/report/supplier_contacts/supplier_contacts.py index 583c41d8..d0f8e289 100644 --- a/csf_tz/purchase_and_stock_management/report/supplier_contacts/supplier_contacts.py +++ b/csf_tz/purchase_and_stock_management/report/supplier_contacts/supplier_contacts.py @@ -1,20 +1,27 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -from six.moves import range import frappe - field_map = { - "Contact": [ "first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact" ], - "Address": [ "address_line1", "address_line2", "city", "state", "pincode", "country", "is_primary_address" ] + "Contact": ["first_name", "last_name", "phone", "mobile_no", "email_id", "is_primary_contact"], + "Address": [ + "address_line1", + "address_line2", + "city", + "state", + "pincode", + "country", + "is_primary_address", + ], } + def execute(filters=None): columns, data = get_columns(filters), get_data(filters) return columns, data + def get_columns(filters): return [ "{party_type}:Link/{party_type}".format(party_type=filters.get("party_type")), @@ -30,16 +37,17 @@ def get_columns(filters): "Phone", "Mobile No", "Email Id", - "Is Primary Contact:Check" + "Is Primary Contact:Check", ] + def get_data(filters): - data = [] party_type = filters.get("party_type") party = filters.get("party_name") return get_party_addresses_and_contact(party_type, party) + def get_party_addresses_and_contact(party_type, party): data = [] filters = None @@ -49,8 +57,8 @@ def get_party_addresses_and_contact(party_type, party): return [] if party: - filters = { "name": party } - + filters = {"name": party} + party_details = frappe.get_list(party_type, filters=filters, fields=["name"], as_list=True) for party_detail in map(list, party_details): docname = party_detail[0] @@ -59,8 +67,8 @@ def get_party_addresses_and_contact(party_type, party): contacts = get_party_details(party_type, docname, doctype="Contact") if not any([addresses, contacts]): - party_detail.extend([ "" for field in field_map.get("Address", []) ]) - party_detail.extend([ "" for field in field_map.get("Contact", []) ]) + party_detail.extend(["" for field in field_map.get("Address", [])]) + party_detail.extend(["" for field in field_map.get("Contact", [])]) data.append(party_detail) else: addresses = map(list, addresses) @@ -70,22 +78,25 @@ def get_party_addresses_and_contact(party_type, party): for idx in range(0, max_length): result = list(party_detail) - address = addresses[idx] if idx < len(addresses) else [ "" for field in field_map.get("Address", []) ] - contact = contacts[idx] if idx < len(contacts) else [ "" for field in field_map.get("Contact", []) ] + address = ( + addresses[idx] if idx < len(addresses) else ["" for field in field_map.get("Address", [])] + ) + contact = ( + contacts[idx] if idx < len(contacts) else ["" for field in field_map.get("Contact", [])] + ) result.extend(address) result.extend(contact) data.append(result) return data + def get_party_details(party_type, docname, doctype="Address", fields=None): default_filters = get_default_address_contact_filters(party_type, docname) if not fields: fields = field_map.get(doctype, ["name"]) return frappe.get_list(doctype, filters=default_filters, fields=fields, as_list=True) + def get_default_address_contact_filters(party_type, docname): - return [ - ["Dynamic Link", "link_doctype", "=", party_type], - ["Dynamic Link", "link_name", "=", docname] - ] \ No newline at end of file + return [["Dynamic Link", "link_doctype", "=", party_type], ["Dynamic Link", "link_name", "=", docname]] diff --git a/csf_tz/sales_and_marketing/doctype/allert_custom/allert_custom.py b/csf_tz/sales_and_marketing/doctype/allert_custom/allert_custom.py index fd9aa7a9..e38358ad 100644 --- a/csf_tz/sales_and_marketing/doctype/allert_custom/allert_custom.py +++ b/csf_tz/sales_and_marketing/doctype/allert_custom/allert_custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class AllertCustom(Document): pass diff --git a/csf_tz/sales_and_marketing/doctype/allert_custom/test_allert_custom.py b/csf_tz/sales_and_marketing/doctype/allert_custom/test_allert_custom.py index 1eb0b0a0..3f5de245 100644 --- a/csf_tz/sales_and_marketing/doctype/allert_custom/test_allert_custom.py +++ b/csf_tz/sales_and_marketing/doctype/allert_custom/test_allert_custom.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestAllertCustom(unittest.TestCase): pass diff --git a/csf_tz/sales_and_marketing/doctype/communications/communications.py b/csf_tz/sales_and_marketing/doctype/communications/communications.py index 606cdbfb..ada4eb36 100644 --- a/csf_tz/sales_and_marketing/doctype/communications/communications.py +++ b/csf_tz/sales_and_marketing/doctype/communications/communications.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class Communications(Document): pass diff --git a/csf_tz/sales_and_marketing/doctype/communications/test_communications.py b/csf_tz/sales_and_marketing/doctype/communications/test_communications.py index 365e3a41..e7e88871 100644 --- a/csf_tz/sales_and_marketing/doctype/communications/test_communications.py +++ b/csf_tz/sales_and_marketing/doctype/communications/test_communications.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestCommunications(unittest.TestCase): pass diff --git a/csf_tz/sales_and_marketing/doctype/customer_item/customer_item.py b/csf_tz/sales_and_marketing/doctype/customer_item/customer_item.py index 4d3aba28..9581343b 100644 --- a/csf_tz/sales_and_marketing/doctype/customer_item/customer_item.py +++ b/csf_tz/sales_and_marketing/doctype/customer_item/customer_item.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class CustomerItem(Document): pass diff --git a/csf_tz/sales_and_marketing/doctype/marketing_dept/marketing_dept.py b/csf_tz/sales_and_marketing/doctype/marketing_dept/marketing_dept.py index cdb1acb9..945f713a 100644 --- a/csf_tz/sales_and_marketing/doctype/marketing_dept/marketing_dept.py +++ b/csf_tz/sales_and_marketing/doctype/marketing_dept/marketing_dept.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class MarketingDept(Document): pass diff --git a/csf_tz/sales_and_marketing/doctype/marketing_dept/test_marketing_dept.py b/csf_tz/sales_and_marketing/doctype/marketing_dept/test_marketing_dept.py index 4058f620..1ff18488 100644 --- a/csf_tz/sales_and_marketing/doctype/marketing_dept/test_marketing_dept.py +++ b/csf_tz/sales_and_marketing/doctype/marketing_dept/test_marketing_dept.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestMarketingDept(unittest.TestCase): pass diff --git a/csf_tz/sales_and_marketing/doctype/past_sales/past_sales.py b/csf_tz/sales_and_marketing/doctype/past_sales/past_sales.py index 514fe7ca..0f99feae 100644 --- a/csf_tz/sales_and_marketing/doctype/past_sales/past_sales.py +++ b/csf_tz/sales_and_marketing/doctype/past_sales/past_sales.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class PastSales(Document): pass diff --git a/csf_tz/sales_and_marketing/doctype/past_sales/test_past_sales.py b/csf_tz/sales_and_marketing/doctype/past_sales/test_past_sales.py index 91b9a786..1683951c 100644 --- a/csf_tz/sales_and_marketing/doctype/past_sales/test_past_sales.py +++ b/csf_tz/sales_and_marketing/doctype/past_sales/test_past_sales.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestPastSales(unittest.TestCase): pass diff --git a/csf_tz/sales_and_marketing/doctype/past_serial_no/past_serial_no.py b/csf_tz/sales_and_marketing/doctype/past_serial_no/past_serial_no.py index 49b2600e..c01ddb29 100644 --- a/csf_tz/sales_and_marketing/doctype/past_serial_no/past_serial_no.py +++ b/csf_tz/sales_and_marketing/doctype/past_serial_no/past_serial_no.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class PastSerialNo(Document): pass diff --git a/csf_tz/sales_and_marketing/doctype/past_serial_no/test_past_serial_no.py b/csf_tz/sales_and_marketing/doctype/past_serial_no/test_past_serial_no.py index cbb2b9bf..5b15bb52 100644 --- a/csf_tz/sales_and_marketing/doctype/past_serial_no/test_past_serial_no.py +++ b/csf_tz/sales_and_marketing/doctype/past_serial_no/test_past_serial_no.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors # See license.txt -from __future__ import unicode_literals -import frappe import unittest + class TestPastSerialNo(unittest.TestCase): pass diff --git a/csf_tz/sales_and_marketing/doctype/payment_plan/payment_plan.py b/csf_tz/sales_and_marketing/doctype/payment_plan/payment_plan.py index 4031d41a..ce8654b5 100644 --- a/csf_tz/sales_and_marketing/doctype/payment_plan/payment_plan.py +++ b/csf_tz/sales_and_marketing/doctype/payment_plan/payment_plan.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class PaymentPlan(Document): pass diff --git a/csf_tz/sales_and_marketing/doctype/products_of_interest/products_of_interest.py b/csf_tz/sales_and_marketing/doctype/products_of_interest/products_of_interest.py index e84b5d91..5106255a 100644 --- a/csf_tz/sales_and_marketing/doctype/products_of_interest/products_of_interest.py +++ b/csf_tz/sales_and_marketing/doctype/products_of_interest/products_of_interest.py @@ -1,10 +1,8 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe from frappe.model.document import Document + class ProductsofInterest(Document): pass diff --git a/csf_tz/sales_and_marketing/report/brand_sales_report/brand_sales_report.py b/csf_tz/sales_and_marketing/report/brand_sales_report/brand_sales_report.py index 260253d3..d8676a62 100644 --- a/csf_tz/sales_and_marketing/report/brand_sales_report/brand_sales_report.py +++ b/csf_tz/sales_and_marketing/report/brand_sales_report/brand_sales_report.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -9,12 +8,7 @@ def execute(filters=None): columns, data = [], [] columns = [ - { - "fieldname": "posting_date", - "label": _("Sale Date"), - "fieldtype": "Date", - "width": 150 - }, + {"fieldname": "posting_date", "label": _("Sale Date"), "fieldtype": "Date", "width": 150}, { "fieldname": "customer", "label": _("Customer"), @@ -26,60 +20,48 @@ def execute(filters=None): "fieldname": "customer_address", "label": _("Customer Address"), "fieldtype": "Small Text", - "width": 200 - }, - { - "fieldname": "contact", - "label": _("Customer Contacts"), - "fieldtype": "Small Text", - "width": 150 - }, - { - "fieldname": "itm", - "label": _("Item Sold"), - "fieldtype": "Link", - "options": "Item", - "width": 200 + "width": 200, }, + {"fieldname": "contact", "label": _("Customer Contacts"), "fieldtype": "Small Text", "width": 150}, + {"fieldname": "itm", "label": _("Item Sold"), "fieldtype": "Link", "options": "Item", "width": 200}, { "fieldname": "brand", "label": _("Brand"), "fieldtype": "Link", "options": "Sales Invoice", - "width": 150 - }, - { - "fieldname": "quantity", - "label": _("Quantity"), - "fieldtype": "data", - "width": 150 + "width": 150, }, + {"fieldname": "quantity", "label": _("Quantity"), "fieldtype": "data", "width": 150}, ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} - where = '' + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } + where = "" if filters.customer: - where += ' AND si.customer = %(customer)s' + where += " AND si.customer = %(customer)s" where_filter.update({"customer": filters.customer}) if filters.brand: - where += ' AND ti.brand = %(brand)s' + where += " AND ti.brand = %(brand)s" where_filter.update({"brand": filters.brand}) - - data = frappe.db.sql('''SELECT + + data = frappe.db.sql( + """SELECT si.customer, si.name, si.posting_date, si.grand_total, sit.warehouse, - sit.item_code as itm, - sit.qty as quantity, + sit.item_code as itm, + sit.qty as quantity, ti.brand as brand, - sit.parent as si_name, + sit.parent as si_name, si.customer as cust FROM (`tabSales Invoice Item` sit) @@ -92,33 +74,40 @@ def execute(filters=None): WHERE si.docstatus = 1 AND ti.item_group <> 'Spare Parts' AND si.customer <> 'Guest' AND si.posting_date BETWEEN %(from_date)s AND %(to_date)s - ''' + where, - where_filter, as_dict=1 - ); - + """ + + where, + where_filter, + as_dict=1, + ) + for customer in data: address = [] - address_name = frappe.db.sql("""SELECT - dl.parent - FROM - (`tabDynamic Link` dl) - WHERE - dl.parenttype='Address' AND dl.link_doctype='Customer' AND dl.link_name= %(customer)s - ORDER BY - dl.creation - LIMIT 1""", {"customer": customer.customer}, as_dict=1) + address_name = frappe.db.sql( + """SELECT + dl.parent + FROM + (`tabDynamic Link` dl) + WHERE + dl.parenttype='Address' AND dl.link_doctype='Customer' AND dl.link_name= %(customer)s + ORDER BY + dl.creation + LIMIT 1""", + {"customer": customer.customer}, + as_dict=1, + ) if address_name: - address = frappe.db.sql("""SELECT + address = frappe.db.sql( + """SELECT CONCAT(ta.address_line1,",", ta.city,',', ta.country) AS customer_address, ta.phone AS contact FROM - (`tabAddress` ta) - WHERE - ta.name = %(address_name)s""", {"address_name": address_name[0].parent}, as_dict=1) + (`tabAddress` ta) + WHERE + ta.name = %(address_name)s""", + {"address_name": address_name[0].parent}, + as_dict=1, + ) if address: - customer.update({ - "customer_address": address[0].customer_address, - "contact": address[0].contact - }) + customer.update({"customer_address": address[0].customer_address, "contact": address[0].contact}) return columns, data diff --git a/csf_tz/sales_and_marketing/report/customer_loan_assistance_report/customer_loan_assistance_report.py b/csf_tz/sales_and_marketing/report/customer_loan_assistance_report/customer_loan_assistance_report.py index b85aaa6a..9875d7f0 100644 --- a/csf_tz/sales_and_marketing/report/customer_loan_assistance_report/customer_loan_assistance_report.py +++ b/csf_tz/sales_and_marketing/report/customer_loan_assistance_report/customer_loan_assistance_report.py @@ -1,80 +1,69 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ + def execute(filters=None): columns, data = [], [] - + columns = [ { "fieldname": "reference", - "label" : _("Reference"), + "label": _("Reference"), "fieldtype": "Link", "options": "Customer Loan Assistance", - "width": 150 + "width": 150, }, { "fieldname": "customer_type", - "label" : _("Lead / Customer"), + "label": _("Lead / Customer"), "fieldtype": "Link", "options": "Doctype", "width": 150, - "hidden": 1 + "hidden": 1, }, { "fieldname": "customer_reference", - "label" : _("Customer Reference"), + "label": _("Customer Reference"), "fieldtype": "Dynamic Link", "options": "customer_type", - "width": 150 - }, - { - "fieldname": "customer_name", - "label" : _("Customer Name"), - "fieldtype": "Data", - "width": 150 + "width": 150, }, + {"fieldname": "customer_name", "label": _("Customer Name"), "fieldtype": "Data", "width": 150}, { "fieldname": "loan_supplier", - "label" : _("Loan Supplier"), + "label": _("Loan Supplier"), "fieldtype": "Link", "options": "Supplier", - "width": 150 - }, - { - "fieldname": "start_date", - "label" : _("Start Date"), - "fieldtype": "Date", - "width": 150 - }, - { - "fieldname": "end_date", - "label" : _("End Date"), - "fieldtype": "Date", - "width": 150 + "width": 150, }, + {"fieldname": "start_date", "label": _("Start Date"), "fieldtype": "Date", "width": 150}, + {"fieldname": "end_date", "label": _("End Date"), "fieldtype": "Date", "width": 150}, { "fieldname": "loan_status", - "label" : _("Loan Status"), + "label": _("Loan Status"), "fieldtype": "Dynamic Link", "options": "customer_type", - "width": 150 - } + "width": 150, + }, ] - + if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"start_date": filters.from_date,"end_date": filters.to_date,} + where_filter = { + "start_date": filters.from_date, + "end_date": filters.to_date, + } where = "" if filters.loan_supplier: - where += ' AND loan_supplier = %(loan_supplier)s ' + where += " AND loan_supplier = %(loan_supplier)s " where_filter.update({"loan_supplier": filters.loan_supplier}) - - data = frappe.db.sql('''SELECT + + data = frappe.db.sql( + """SELECT name AS reference, CASE WHEN customer IS NOT NULL THEN 'Customer' @@ -93,5 +82,9 @@ def execute(filters=None): `tabCustomer Loan Assistance` WHERE creation BETWEEN %(start_date)s AND %(end_date)s - ''' + where, where_filter, as_dict=1) + """ + + where, + where_filter, + as_dict=1, + ) return columns, data diff --git a/csf_tz/sales_and_marketing/report/item_wise_leads_report/item_wise_leads_report.py b/csf_tz/sales_and_marketing/report/item_wise_leads_report/item_wise_leads_report.py index a5e489ca..adc2a32d 100644 --- a/csf_tz/sales_and_marketing/report/item_wise_leads_report/item_wise_leads_report.py +++ b/csf_tz/sales_and_marketing/report/item_wise_leads_report/item_wise_leads_report.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -9,51 +8,34 @@ def execute(filters=None): columns, data = [], [] columns = [ - { - "fieldname": "item_code", - "label": _("Item"), - "fieldtype": "Link", - "options": "Item", - "width": 200 - }, + {"fieldname": "item_code", "label": _("Item"), "fieldtype": "Link", "options": "Item", "width": 200}, { "fieldname": "total_qty", "label": _("Quantity"), "fieldtype": "Float", }, - { - "fieldname": "quotations", - "label" : _("Quotations"), - "fieldtype": "int", - "width": 150 - }, - { - "fieldname": "customers", - "label": _("Customers"), - "fieldtype": "Int", - "width": 150 - }, - { - "fieldname": "leads", - "label": _("Leads"), - "fieldtype": "Int", - "width": 150 - }, + {"fieldname": "quotations", "label": _("Quotations"), "fieldtype": "int", "width": 150}, + {"fieldname": "customers", "label": _("Customers"), "fieldtype": "Int", "width": 150}, + {"fieldname": "leads", "label": _("Leads"), "fieldtype": "Int", "width": 150}, { "fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Link", "options": "Warehouse", - "width": 200 + "width": 200, }, ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } where = "" - data = frappe.db.sql('''SELECT + data = frappe.db.sql( + """SELECT tqi.item_code, tqi.item_name, SUM(tqi.qty) AS total_qty, @@ -69,8 +51,11 @@ def execute(filters=None): (`tabQuotation` customer_quot) ON tqi.parent = customer_quot.name AND customer_quot.quotation_to = 'Customer' WHERE tq.docstatus = 1 AND tq.transaction_date BETWEEN %(from_date)s AND %(to_date)s - GROUP BY tqi.item_code, tqi.warehouse - '''+ where, - where_filter, as_dict=1,as_list=1 - ); + GROUP BY tqi.item_code, tqi.warehouse + """ + + where, + where_filter, + as_dict=1, + as_list=1, + ) return columns, data diff --git a/csf_tz/sales_and_marketing/report/items_marked_for_delivery/items_marked_for_delivery.py b/csf_tz/sales_and_marketing/report/items_marked_for_delivery/items_marked_for_delivery.py index a816888c..56a977e9 100644 --- a/csf_tz/sales_and_marketing/report/items_marked_for_delivery/items_marked_for_delivery.py +++ b/csf_tz/sales_and_marketing/report/items_marked_for_delivery/items_marked_for_delivery.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -9,100 +8,56 @@ def execute(filters=None): columns, data = [], [] columns = [ - { - "fieldname": "customer_name", - "label": _("Customer"), - "fieldtype": "Data", - "width": 150 - }, + {"fieldname": "customer_name", "label": _("Customer"), "fieldtype": "Data", "width": 150}, { "fieldname": "customer_address", "label": _("Customer Address"), "fieldtype": "Small Text", - "width": 200 - }, - { - "fieldname": "phone_number", - "label": _("Phone Number"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "item_name", - "label": _("Item"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "item_code", - "label": _("Item Code"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "quantity", - "label": _("Quantity"), - "fieldtype": "Float", - "width": 150 - }, - { - "fieldname": "uom", - "label": _("UOM"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "serial_no", - "label": _("Serial Numbers"), - "fieldtype": "Data", - "width": 150 - }, - { - "fieldname": "actual_quantity", - "label": _("Actual Quantity"), - "fieldtype": "Float", - "width": 100 + "width": 200, }, + {"fieldname": "phone_number", "label": _("Phone Number"), "fieldtype": "Data", "width": 150}, + {"fieldname": "item_name", "label": _("Item"), "fieldtype": "Data", "width": 150}, + {"fieldname": "item_code", "label": _("Item Code"), "fieldtype": "Data", "width": 150}, + {"fieldname": "quantity", "label": _("Quantity"), "fieldtype": "Float", "width": 150}, + {"fieldname": "uom", "label": _("UOM"), "fieldtype": "Data", "width": 150}, + {"fieldname": "serial_no", "label": _("Serial Numbers"), "fieldtype": "Data", "width": 150}, + {"fieldname": "actual_quantity", "label": _("Actual Quantity"), "fieldtype": "Float", "width": 100}, { "fieldname": "projected_quantity", "label": _("Projected Quantity"), "fieldtype": "Float", - "width": 100 - }, - { - "fieldname": "warehouse", - "label": _("Warehouse"), - "fieldtype": "Data", - "width": 150 + "width": 100, }, + {"fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Data", "width": 150}, { "fieldname": "sales_invoice", "label": _("Sales Invoice"), "fieldtype": "Link", "options": "Sales Invoice", - "width": 150 + "width": 150, }, { "fieldname": "sales_order", "label": _("Sales Order"), "fieldtype": "Link", "options": "Sales Order", - "width": 150 - } + "width": 150, + }, ] - - where = '' + + where = "" where_filter = {} if filters.warehouse: - where += ' AND sit.warehouse = %(warehouse)s' + where += " AND sit.warehouse = %(warehouse)s" where_filter.update({"warehouse": filters.warehouse}) - + if filters.item_group: - where += ' AND tabItem.item_group = %(item_group)s' + where += " AND tabItem.item_group = %(item_group)s" where_filter.update({"item_group": filters.item_group}) - - data = frappe.db.sql('''SELECT + + data = frappe.db.sql( + """SELECT si.customer_name, CONCAT(ta.address_line1,",", ta.city,',', ta.country) AS customer_address, ta.phone AS phone_number, @@ -120,11 +75,11 @@ def execute(filters=None): (`tabSales Invoice Item`sit) LEFT JOIN (`tabSales Order` so) - ON (sit.sales_order = so.name) + ON (sit.sales_order = so.name) LEFT JOIN (`tabSales Invoice` si) ON (sit.parent=si.name) - LEFT JOIN + LEFT JOIN `tabBin` ON (`tabBin`.item_code = sit.item_code and `tabBin`.warehouse = sit.warehouse) LEFT JOIN @@ -132,13 +87,15 @@ def execute(filters=None): LEFT JOIN (`tabAddress` ta) ON ta.name=(SELECT dl.parent FROM (`tabDynamic Link` dl) - WHERE - dl.parenttype='Address' AND dl.link_doctype='Customer' AND + WHERE + dl.parenttype='Address' AND dl.link_doctype='Customer' AND dl.link_name=si.customer ORDER BY ta.creation LIMIT 1 ) WHERE is_marked = 1 AND sit.delivered_qty < sit.qty AND si.docstatus = 1 AND si.status IN ('Submitted', 'Paid', 'Overdue', 'Unpaid') - AND (SELECT name FROM `tabSales Invoice` WHERE return_against = si.name LIMIT 1) IS NULL''' + where, - where_filter, as_dict=1 - ); + AND (SELECT name FROM `tabSales Invoice` WHERE return_against = si.name LIMIT 1) IS NULL""" + + where, + where_filter, + as_dict=1, + ) return columns, data diff --git a/csf_tz/sales_and_marketing/report/previous_ams_customer_report/previous_ams_customer_report.py b/csf_tz/sales_and_marketing/report/previous_ams_customer_report/previous_ams_customer_report.py index 271a5bae..733cf00c 100644 --- a/csf_tz/sales_and_marketing/report/previous_ams_customer_report/previous_ams_customer_report.py +++ b/csf_tz/sales_and_marketing/report/previous_ams_customer_report/previous_ams_customer_report.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -9,12 +8,7 @@ def execute(filters=None): columns, data = [], [] columns = [ - { - "fieldname": "date_of_sale", - "label": _("Date of Sale"), - "fieldtype": "Date", - "width": 150 - }, + {"fieldname": "date_of_sale", "label": _("Date of Sale"), "fieldtype": "Date", "width": 150}, { "fieldname": "customer", "label": _("Customer"), @@ -26,65 +20,41 @@ def execute(filters=None): "fieldname": "customer_address", "label": _("Customer Address"), "fieldtype": "Small Text", - "width": 200 - }, - { - "fieldname": "contact", - "label": _("Customer Contacts"), - "fieldtype": "Small Text", - "width": 150 - }, - { - "fieldname": "itm", - "label": _("Item Sold"), - "fieldtype": "Link", - "options": "Item", - "width": 200 - }, - { - "fieldname": "brand", - "label": _("Brand"), - "fieldtype": "Link", - "options": "Brand", - "width": 150 + "width": 200, }, + {"fieldname": "contact", "label": _("Customer Contacts"), "fieldtype": "Small Text", "width": 150}, + {"fieldname": "itm", "label": _("Item Sold"), "fieldtype": "Link", "options": "Item", "width": 200}, + {"fieldname": "brand", "label": _("Brand"), "fieldtype": "Link", "options": "Brand", "width": 150}, { "fieldname": "name", "label": _("Serial No"), "fieldtype": "Link", "options": "Past Serial No", - "width": 200 - }, - { - "fieldname": "amount", - "label": _("Price"), - "fieldtype": "Currency", - "width": 150 + "width": 200, }, - { - "fieldname": "past_item_group", - "label": _("Item Group"), - "fieldtype": "Data", - "width": 150 - }, - + {"fieldname": "amount", "label": _("Price"), "fieldtype": "Currency", "width": 150}, + {"fieldname": "past_item_group", "label": _("Item Group"), "fieldtype": "Data", "width": 150}, ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} - where = '' + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } + where = "" if filters.customer: - where += ' AND psn.customer = %(customer)s' + where += " AND psn.customer = %(customer)s" where_filter.update({"customer": filters.customer}) if filters.brand: - where += ' AND `tabItem`.brand = %(brand)s' + where += " AND `tabItem`.brand = %(brand)s" where_filter.update({"brand": filters.brand}) - - data = frappe.db.sql('''SELECT + + data = frappe.db.sql( + """SELECT psn.customer, psn.name, psn.date_of_sale, @@ -92,40 +62,48 @@ def execute(filters=None): psn.past_item_group, psn.item_code as itm, psn.past_item_group, - `tabItem`.brand + `tabItem`.brand FROM (`tabPast Serial No` psn) LEFT JOIN `tabItem` ON `tabItem`.name = psn.item_code WHERE psn.docstatus = 1 AND psn.date_of_sale BETWEEN %(from_date)s AND %(to_date)s - ''' + where + ''' GROUP BY psn.name - ORDER BY psn.date_of_sale''', - where_filter, as_dict=1 - ); + """ + + where + + """ GROUP BY psn.name + ORDER BY psn.date_of_sale""", + where_filter, + as_dict=1, + ) for customer in data: address = [] - address_name = frappe.db.sql("""SELECT - dl.parent - FROM - (`tabDynamic Link` dl) - WHERE - dl.parenttype='Address' AND dl.link_doctype='Customer' AND dl.link_name= %(customer)s - ORDER BY - dl.creation - LIMIT 1""", {"customer": customer.customer}, as_dict=1) + address_name = frappe.db.sql( + """SELECT + dl.parent + FROM + (`tabDynamic Link` dl) + WHERE + dl.parenttype='Address' AND dl.link_doctype='Customer' AND dl.link_name= %(customer)s + ORDER BY + dl.creation + LIMIT 1""", + {"customer": customer.customer}, + as_dict=1, + ) if address_name: - address = frappe.db.sql("""SELECT + address = frappe.db.sql( + """SELECT CONCAT(ta.address_line1,",", ta.city,',', ta.country) AS customer_address, ta.phone AS contact FROM - (`tabAddress` ta) - WHERE - ta.name = %(address_name)s""", {"address_name": address_name[0].parent}, as_dict=1) + (`tabAddress` ta) + WHERE + ta.name = %(address_name)s""", + {"address_name": address_name[0].parent}, + as_dict=1, + ) if address: - customer.update({ - "customer_address": address[0].customer_address, - "contact": address[0].contact - }) + customer.update({"customer_address": address[0].customer_address, "contact": address[0].contact}) return columns, data diff --git a/csf_tz/sales_and_marketing/report/sales_details_report/sales_details_report.py b/csf_tz/sales_and_marketing/report/sales_details_report/sales_details_report.py index 1ceea19f..7c26f23f 100644 --- a/csf_tz/sales_and_marketing/report/sales_details_report/sales_details_report.py +++ b/csf_tz/sales_and_marketing/report/sales_details_report/sales_details_report.py @@ -1,7 +1,6 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ @@ -9,12 +8,7 @@ def execute(filters=None): columns, data = [], [] columns = [ - { - "fieldname": "posting_date", - "label": _("Date"), - "fieldtype": "Date", - "width": 150 - }, + {"fieldname": "posting_date", "label": _("Date"), "fieldtype": "Date", "width": 150}, { "fieldname": "customer", "label": _("Customer"), @@ -27,61 +21,50 @@ def execute(filters=None): "label": _("Customer Group"), "fieldtype": "Link", "options": "Customer Group", - "width": 150 + "width": 150, }, { "fieldname": "item_no", "label": _("Item Sold"), "fieldtype": "Link", "options": "Item", - "width": 150 + "width": 150, }, { "fieldname": "name", "label": _("Sales Invoice"), "fieldtype": "Link", "options": "Sales Invoice", - "width": 150 - }, - { - "fieldname": "grand_total", - "label": _("Amount"), - "fieldtype": "Currency", - "width": 150 - }, - { - "fieldname": "quantity", - "label": _("Quantity"), - "fieldtype": "data", - "width": 150 - }, - { - "fieldname": "warehouse", - "label": _("Warehouse"), - "fieldtype": "Data", - "width": 150 + "width": 150, }, + {"fieldname": "grand_total", "label": _("Amount"), "fieldtype": "Currency", "width": 150}, + {"fieldname": "quantity", "label": _("Quantity"), "fieldtype": "data", "width": 150}, + {"fieldname": "warehouse", "label": _("Warehouse"), "fieldtype": "Data", "width": 150}, ] if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} - where = '' + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } + where = "" if filters.customer: - where += ' AND si.customer = %(customer)s' + where += " AND si.customer = %(customer)s" where_filter.update({"customer": filters.customer}) - + if filters.warehouse: - where += ' AND sit.warehouse = %(warehouse)s' + where += " AND sit.warehouse = %(warehouse)s" where_filter.update({"warehouse": filters.warehouse}) if filters.cust_group: - where += ' AND tc.customer_group = %(cust_group)s' + where += " AND tc.customer_group = %(cust_group)s" where_filter.update({"cust_group": filters.cust_group}) - - data = frappe.db.sql('''SELECT + + data = frappe.db.sql( + """SELECT si.customer, si.name, si.posting_date, @@ -100,33 +83,40 @@ def execute(filters=None): WHERE si.docstatus = 1 AND si.posting_date BETWEEN %(from_date)s AND %(to_date)s - GROUP BY si.name - ''' + where, - where_filter, as_dict=1 - ); + GROUP BY si.name + """ + + where, + where_filter, + as_dict=1, + ) for item in data: # # For item info # - itm_info = frappe.db.sql('''SELECT + itm_info = frappe.db.sql( + """SELECT sit.item_code as itm, sit.qty, sit.parent as si_name, si.customer as cust - FROM - (`tabSales Invoice Item` sit) + FROM + (`tabSales Invoice Item` sit) LEFT JOIN (`tabSales Invoice` si) ON (si.name = sit.parent) - WHERE - si.name = sit.parent AND si.docstatus = 1 ''' , - {"cust": item.customer,}, as_dict=1); - - item.item_no = '' - item.quantity='' + WHERE + si.name = sit.parent AND si.docstatus = 1 """, + { + "cust": item.customer, + }, + as_dict=1, + ) + + item.item_no = "" + item.quantity = "" for co in itm_info: if co.si_name == item.name: - item.item_no += '' + str(co.itm) + ',' + ' ' - item.quantity += str(co.qty) + ', ' + item.item_no += '' + str(co.itm) + "," + " " + item.quantity += str(co.qty) + ", " return columns, data diff --git a/csf_tz/sales_and_marketing/report/spare_sales_report/spare_sales_report.py b/csf_tz/sales_and_marketing/report/spare_sales_report/spare_sales_report.py index 7ef2a450..c87bb743 100644 --- a/csf_tz/sales_and_marketing/report/spare_sales_report/spare_sales_report.py +++ b/csf_tz/sales_and_marketing/report/spare_sales_report/spare_sales_report.py @@ -1,20 +1,29 @@ # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt -from __future__ import unicode_literals import frappe from frappe import _ def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() item_list = get_items(filters) data = [] for d in item_list: - row = [d.posting_date, d.itm,d.item_name, d.customer, d.warehouse, - d.quantity, d.amount, d.brand, d.name] + row = [ + d.posting_date, + d.itm, + d.item_name, + d.customer, + d.warehouse, + d.quantity, + d.amount, + d.brand, + d.name, + ] data.append(row) return columns, data @@ -27,29 +36,35 @@ def get_columns(): _("Item Name") + ":Data:120", _("Customer") + ":Link/Customer:120", _("Warehouse") + ":Link/Warehouse:120", - _("Qty") + ":Float:120", + _("Qty") + ":Float:120", _("Amount") + ":Float:120", _("Brand") + ":Link/Brand:120", - _("Invoice") + ":Link/Sales Invoice:120",] + _("Invoice") + ":Link/Sales Invoice:120", + ] return columns + def get_items(filters): if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date {}").format(filters.to_date)) - where_filter = {"from_date": filters.from_date,"to_date": filters.to_date,} - where = '' + where_filter = { + "from_date": filters.from_date, + "to_date": filters.to_date, + } + where = "" if filters.brand: - where += ' AND ti.brand = %(brand)s' + where += " AND ti.brand = %(brand)s" where_filter.update({"brand": filters.brand}) if filters.shop: - where += ' AND sit.warehouse = %(shop)s' + where += " AND sit.warehouse = %(shop)s" where_filter.update({"shop": filters.shop}) - - return frappe.db.sql('''SELECT + + return frappe.db.sql( + """SELECT si.customer, si.name, si.posting_date, @@ -60,7 +75,7 @@ def get_items(filters): sit.qty as quantity, sit.amount, ti.brand as brand, - sit.parent as si_name, + sit.parent as si_name, si.customer as cust FROM (`tabSales Invoice Item` sit) @@ -74,6 +89,8 @@ def get_items(filters): si.docstatus = 1 AND ti.item_group = 'Spare Parts' AND si.customer = 'Guest' AND si.posting_date BETWEEN %(from_date)s AND %(to_date)s ORDER BY si.posting_date - ''' + where, - where_filter, as_dict=1 - ); \ No newline at end of file + """ + + where, + where_filter, + as_dict=1, + ) diff --git a/csf_tz/stanbic/doctype/stanbic_payments_info/stanbic_payments_info.py b/csf_tz/stanbic/doctype/stanbic_payments_info/stanbic_payments_info.py index 84e55400..1f55a8b9 100644 --- a/csf_tz/stanbic/doctype/stanbic_payments_info/stanbic_payments_info.py +++ b/csf_tz/stanbic/doctype/stanbic_payments_info/stanbic_payments_info.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class StanbicPaymentsInfo(Document): pass diff --git a/csf_tz/stanbic/doctype/stanbic_payments_initiation/stanbic_payments_initiation.py b/csf_tz/stanbic/doctype/stanbic_payments_initiation/stanbic_payments_initiation.py index 0e6c595a..736447a6 100644 --- a/csf_tz/stanbic/doctype/stanbic_payments_initiation/stanbic_payments_initiation.py +++ b/csf_tz/stanbic/doctype/stanbic_payments_initiation/stanbic_payments_initiation.py @@ -1,194 +1,186 @@ # Copyright (c) 2023, Aakvatech and contributors # For license information, please see license.txt +import json + import frappe from frappe.model.document import Document + from csf_tz.stanbic.doctype.stanbic_payments_initiation.xml import get_xml from csf_tz.stanbic.pgp import encrypt_pgp -import json class StanbicPaymentsInitiation(Document): - def validate(self): - self.validate_data() - - def set_data(self): - payroll_entry_doc = frappe.get_cached_doc("Payroll Entry", self.payroll_entry) - self.set_entries(payroll_entry_doc) - self.insert(ignore_permissions=True) - self.reload() - self.xml = get_xml(self) - public_key, file_code = frappe.get_cached_value( - "Stanbic Setting", self.stanbic_setting, ["pgp_public_key", "file_code"] - ) - self.file_code = file_code - self.encrypted_xml = encrypt_pgp(self.xml, public_key) - self.save(ignore_permissions=True) - - def set_entries(self, payroll_entry_doc=None): - if not payroll_entry_doc: - payroll_entry_doc = frappe.get_cached_doc( - "Payroll Entry", self.payroll_entry - ) - self.number_of_transactions = 0 - self.control_sum = 0 - self.stanbic_payments_info = [] - # get all salay slips - salary_slips = self.get_salary_slips() - for slip in salary_slips: - entry = self.append("stanbic_payments_info", {}) - entry.salary_slip = slip.name - entry.employee = slip.employee - entry.transfer_currency = slip.currency - entry.transfer_amount = slip.net_pay - self.number_of_transactions += 1 - self.control_sum += slip.net_pay - - def validate_data(self): - self.validate_entries(self.payroll_entry) - - def validate_entries(self, payroll_entry_doc=None): - # to be add validation as it required - pass - - def get_salary_slips(self): - slips = frappe.get_all( - "Salary Slip", - filters={"payroll_entry": self.payroll_entry, "docstatus": ["in", [0, 1]]}, - fields=[ - "name", - "employee", - "employee_name", - "company", - "docstatus", - "currency", - "net_pay", - ], - ) - # check if all slips are submitted - for slip in slips: - if slip.docstatus == 0: - frappe.throw( - "Salary Slip {0} is not submitted".format(slip.name), - title="Salary Slip Not Submitted", - ) - return slips - - def on_submit(self): - import os - from csf_tz.stanbic.sftp import get_absolute_path - from frappe.utils import now, format_datetime - - timestamp = format_datetime(now(), "yyyyMMddHHmmss") + "000" - filename = f"WASCO_H2H_Pain001v3_TZ_{self.file_code}_{timestamp}.xml" - create_path = get_absolute_path("/private/files/stanbic/outbox") - file_path = os.path.join(create_path, filename) - os.makedirs(os.path.dirname(file_path), exist_ok=True) - with open(file_path, "w") as file: - file.write(self.encrypted_xml) - - def on_update_after_submit(self): - if self.stanbic_ack_change and self.stanbic_ack: - ack_dict = json.loads(self.stanbic_ack) - stanbic_ack_status = ack_dict["Document"]["CstmrPmtStsRpt"][ - "OrgnlGrpInfAndSts" - ]["GrpSts"] - if ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlGrpInfAndSts"].get( - "StsRsnInf" - ): - stanbic_ack_status = ( - stanbic_ack_status - + " " - + ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlGrpInfAndSts"][ - "StsRsnInf" - ]["AddtlInf"] - ) - - frappe.db.set_value( - "Stanbic Payments Initiation", - self.name, - "stanbic_ack_status", - stanbic_ack_status, - ) - frappe.db.set_value( - "Stanbic Payments Initiation", - self.name, - "stanbic_ack_change", - 0, - ) - if self.stanbic_intaud_change and self.stanbic_intaud: - ack_dict = json.loads(self.stanbic_intaud) - TxInfAndSts = ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlPmtInfAndSts"][ - "TxInfAndSts" - ] - if not isinstance(TxInfAndSts, list): - TxInfAndSts = [TxInfAndSts] - for OrgnlEndToEndId in TxInfAndSts: - try: - sal_slip_doc = frappe.get_doc( - "Stanbic Payments Info", - { - "parent": self.name, - "salary_slip": OrgnlEndToEndId["OrgnlEndToEndId"], - }, - ) - stanbic_intaud_status = frappe.as_json( - OrgnlEndToEndId["StsRsnInf"]["AddtlInf"] - if OrgnlEndToEndId.get("StsRsnInf").get("AddtlInf") - else "STATUS NOT FOUND" - ) - frappe.db.set_value( - "Stanbic Payments Info", - sal_slip_doc.name, - "stanbic_intaud_status", - stanbic_intaud_status, - ) - except Exception as e: - frappe.log_error( - f"Error in {self.name}", - f"Error {str(e)} {self.name} {OrgnlEndToEndId}", - ) - frappe.db.set_value( - "Stanbic Payments Initiation", - self.name, - "stanbic_intaud_change", - 0, - ) - if self.stanbic_finaud_change and self.stanbic_finaud: - ack_dict = json.loads(self.stanbic_finaud) - TxInfAndSts = ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlPmtInfAndSts"][ - "TxInfAndSts" - ] - if not isinstance(TxInfAndSts, list): - TxInfAndSts = [TxInfAndSts] - for OrgnlEndToEndId in TxInfAndSts: - try: - sal_slip_doc = frappe.get_doc( - "Stanbic Payments Info", - { - "parent": self.name, - "salary_slip": OrgnlEndToEndId["OrgnlEndToEndId"], - }, - ) - stanbic_finaud_status = frappe.as_json( - OrgnlEndToEndId["StsRsnInf"]["AddtlInf"] - if OrgnlEndToEndId.get("StsRsnInf").get("AddtlInf") - else "STATUS NOT FOUND" - ) - frappe.db.set_value( - "Stanbic Payments Info", - sal_slip_doc.name, - "stanbic_finaud_status", - stanbic_finaud_status, - ) - except Exception as e: - frappe.log_error( - f"Error in {self.name}", - f"Error {str(e)} {self.name} {str(OrgnlEndToEndId)}", - ) - frappe.db.set_value( - "Stanbic Payments Initiation", - self.name, - "stanbic_finaud_change", - 0, - ) + def validate(self): + self.validate_data() + + def set_data(self): + payroll_entry_doc = frappe.get_cached_doc("Payroll Entry", self.payroll_entry) + self.set_entries(payroll_entry_doc) + self.insert(ignore_permissions=True) + self.reload() + self.xml = get_xml(self) + public_key, file_code = frappe.get_cached_value( + "Stanbic Setting", self.stanbic_setting, ["pgp_public_key", "file_code"] + ) + self.file_code = file_code + self.encrypted_xml = encrypt_pgp(self.xml, public_key) + self.save(ignore_permissions=True) + + def set_entries(self, payroll_entry_doc=None): + if not payroll_entry_doc: + payroll_entry_doc = frappe.get_cached_doc("Payroll Entry", self.payroll_entry) + self.number_of_transactions = 0 + self.control_sum = 0 + self.stanbic_payments_info = [] + # get all salay slips + salary_slips = self.get_salary_slips() + for slip in salary_slips: + entry = self.append("stanbic_payments_info", {}) + entry.salary_slip = slip.name + entry.employee = slip.employee + entry.transfer_currency = slip.currency + entry.transfer_amount = slip.net_pay + self.number_of_transactions += 1 + self.control_sum += slip.net_pay + + def validate_data(self): + self.validate_entries(self.payroll_entry) + + def validate_entries(self, payroll_entry_doc=None): + # to be add validation as it required + pass + + def get_salary_slips(self): + slips = frappe.get_all( + "Salary Slip", + filters={"payroll_entry": self.payroll_entry, "docstatus": ["in", [0, 1]]}, + fields=[ + "name", + "employee", + "employee_name", + "company", + "docstatus", + "currency", + "net_pay", + ], + ) + # check if all slips are submitted + for slip in slips: + if slip.docstatus == 0: + frappe.throw( + f"Salary Slip {slip.name} is not submitted", + title="Salary Slip Not Submitted", + ) + return slips + + def on_submit(self): + import os + + from frappe.utils import format_datetime, now + + from csf_tz.stanbic.sftp import get_absolute_path + + timestamp = format_datetime(now(), "yyyyMMddHHmmss") + "000" + filename = f"WASCO_H2H_Pain001v3_TZ_{self.file_code}_{timestamp}.xml" + create_path = get_absolute_path("/private/files/stanbic/outbox") + file_path = os.path.join(create_path, filename) + os.makedirs(os.path.dirname(file_path), exist_ok=True) + with open(file_path, "w") as file: + file.write(self.encrypted_xml) + + def on_update_after_submit(self): + if self.stanbic_ack_change and self.stanbic_ack: + ack_dict = json.loads(self.stanbic_ack) + stanbic_ack_status = ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlGrpInfAndSts"]["GrpSts"] + if ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlGrpInfAndSts"].get("StsRsnInf"): + stanbic_ack_status = ( + stanbic_ack_status + + " " + + ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlGrpInfAndSts"]["StsRsnInf"]["AddtlInf"] + ) + + frappe.db.set_value( + "Stanbic Payments Initiation", + self.name, + "stanbic_ack_status", + stanbic_ack_status, + ) + frappe.db.set_value( + "Stanbic Payments Initiation", + self.name, + "stanbic_ack_change", + 0, + ) + if self.stanbic_intaud_change and self.stanbic_intaud: + ack_dict = json.loads(self.stanbic_intaud) + TxInfAndSts = ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlPmtInfAndSts"]["TxInfAndSts"] + if not isinstance(TxInfAndSts, list): + TxInfAndSts = [TxInfAndSts] + for OrgnlEndToEndId in TxInfAndSts: + try: + sal_slip_doc = frappe.get_doc( + "Stanbic Payments Info", + { + "parent": self.name, + "salary_slip": OrgnlEndToEndId["OrgnlEndToEndId"], + }, + ) + stanbic_intaud_status = frappe.as_json( + OrgnlEndToEndId["StsRsnInf"]["AddtlInf"] + if OrgnlEndToEndId.get("StsRsnInf").get("AddtlInf") + else "STATUS NOT FOUND" + ) + frappe.db.set_value( + "Stanbic Payments Info", + sal_slip_doc.name, + "stanbic_intaud_status", + stanbic_intaud_status, + ) + except Exception as e: + frappe.log_error( + f"Error in {self.name}", + f"Error {str(e)} {self.name} {OrgnlEndToEndId}", + ) + frappe.db.set_value( + "Stanbic Payments Initiation", + self.name, + "stanbic_intaud_change", + 0, + ) + if self.stanbic_finaud_change and self.stanbic_finaud: + ack_dict = json.loads(self.stanbic_finaud) + TxInfAndSts = ack_dict["Document"]["CstmrPmtStsRpt"]["OrgnlPmtInfAndSts"]["TxInfAndSts"] + if not isinstance(TxInfAndSts, list): + TxInfAndSts = [TxInfAndSts] + for OrgnlEndToEndId in TxInfAndSts: + try: + sal_slip_doc = frappe.get_doc( + "Stanbic Payments Info", + { + "parent": self.name, + "salary_slip": OrgnlEndToEndId["OrgnlEndToEndId"], + }, + ) + stanbic_finaud_status = frappe.as_json( + OrgnlEndToEndId["StsRsnInf"]["AddtlInf"] + if OrgnlEndToEndId.get("StsRsnInf").get("AddtlInf") + else "STATUS NOT FOUND" + ) + frappe.db.set_value( + "Stanbic Payments Info", + sal_slip_doc.name, + "stanbic_finaud_status", + stanbic_finaud_status, + ) + except Exception as e: + frappe.log_error( + f"Error in {self.name}", + f"Error {str(e)} {self.name} {str(OrgnlEndToEndId)}", + ) + frappe.db.set_value( + "Stanbic Payments Initiation", + self.name, + "stanbic_finaud_change", + 0, + ) diff --git a/csf_tz/stanbic/doctype/stanbic_payments_initiation/xml.py b/csf_tz/stanbic/doctype/stanbic_payments_initiation/xml.py index 906c100e..71ae90ef 100644 --- a/csf_tz/stanbic/doctype/stanbic_payments_initiation/xml.py +++ b/csf_tz/stanbic/doctype/stanbic_payments_initiation/xml.py @@ -6,15 +6,15 @@ def get_xml(doc): - xml = get_first_xml_part(doc) - xml += get_payments_xml_part(doc) - xml += get_last_xml_part(doc) - return xml + xml = get_first_xml_part(doc) + xml += get_payments_xml_part(doc) + xml += get_last_xml_part(doc) + return xml def get_first_xml_part(doc): - settings_doc = frappe.get_cached_doc("Stanbic Setting", doc.stanbic_setting) - part = f""" + settings_doc = frappe.get_cached_doc("Stanbic Setting", doc.stanbic_setting) + part = f""" @@ -75,11 +75,11 @@ def get_first_xml_part(doc): {settings_doc.charges_bearer}""" - return part + return part def get_payment_part(payment): - part = f""" + part = f""" {payment.salary_slip} @@ -137,20 +137,20 @@ def get_payment_part(payment): """ - return part + return part def get_payments_xml_part(doc): - parts = "" + parts = "" - for payment in doc.stanbic_payments_info: - parts += get_payment_part(payment) + for payment in doc.stanbic_payments_info: + parts += get_payment_part(payment) - return parts + return parts def get_last_xml_part(doc): - part = """ + part = """ """ - return part + return part diff --git a/csf_tz/stanbic/doctype/stanbic_setting/stanbic_setting.py b/csf_tz/stanbic/doctype/stanbic_setting/stanbic_setting.py index 0becfd08..707458e1 100644 --- a/csf_tz/stanbic/doctype/stanbic_setting/stanbic_setting.py +++ b/csf_tz/stanbic/doctype/stanbic_setting/stanbic_setting.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class StanbicSetting(Document): pass diff --git a/csf_tz/stanbic/payments.py b/csf_tz/stanbic/payments.py index ef736ecd..270710bd 100644 --- a/csf_tz/stanbic/payments.py +++ b/csf_tz/stanbic/payments.py @@ -1,49 +1,44 @@ import frappe -from frappe.utils import get_date_str from frappe import _ @frappe.whitelist() def make_payments_initiation(payroll_entry_name, currency, stanbic_setting_name=None): - if currency and not stanbic_setting_name: - company, cheque_number = frappe.get_cached_value( - "Payroll Entry", payroll_entry_name, ["company", "cheque_number"] - ) - if cheque_number: - frappe.throw( - _( - f"Payments initiation {cheque_number} already created for payroll entry {payroll_entry_name}. Please remove the cheque number and try again if you really want to create the payments initiation file." - ), - title="Payments initiation already created", - ) - stanbic_setting_doc = frappe.get_doc( - "Stanbic Setting", {"company": company, "currency": currency} - ) - stanbic_setting_name = stanbic_setting_doc.name + if currency and not stanbic_setting_name: + company, cheque_number = frappe.get_cached_value( + "Payroll Entry", payroll_entry_name, ["company", "cheque_number"] + ) + if cheque_number: + frappe.throw( + _( + f"Payments initiation {cheque_number} already created for payroll entry {payroll_entry_name}. Please remove the cheque number and try again if you really want to create the payments initiation file." + ), + title="Payments initiation already created", + ) + stanbic_setting_doc = frappe.get_doc("Stanbic Setting", {"company": company, "currency": currency}) + stanbic_setting_name = stanbic_setting_doc.name - if not stanbic_setting_name: - frappe.throw( - "Stanbic Setting not found for currency {0}".format(currency), - title="Stanbic Setting not found", - ) + if not stanbic_setting_name: + frappe.throw( + f"Stanbic Setting not found for currency {currency}", + title="Stanbic Setting not found", + ) - payments_initiation_doc = frappe.new_doc("Stanbic Payments Initiation") - payments_initiation_doc.payroll_entry = payroll_entry_name - payments_initiation_doc.stanbic_setting = stanbic_setting_name - payments_initiation_doc.set_data() - frappe.msgprint( - _(f"Payments Initiation {payments_initiation_doc.name} created successfully.") - ) - frappe.db.set_value( - "Payroll Entry", - payroll_entry_name, - "cheque_date", - payments_initiation_doc.posting_date, - ) - frappe.db.set_value( - "Payroll Entry", - payroll_entry_name, - "cheque_number", - payments_initiation_doc.name, - ) - return payments_initiation_doc + payments_initiation_doc = frappe.new_doc("Stanbic Payments Initiation") + payments_initiation_doc.payroll_entry = payroll_entry_name + payments_initiation_doc.stanbic_setting = stanbic_setting_name + payments_initiation_doc.set_data() + frappe.msgprint(_(f"Payments Initiation {payments_initiation_doc.name} created successfully.")) + frappe.db.set_value( + "Payroll Entry", + payroll_entry_name, + "cheque_date", + payments_initiation_doc.posting_date, + ) + frappe.db.set_value( + "Payroll Entry", + payroll_entry_name, + "cheque_number", + payments_initiation_doc.name, + ) + return payments_initiation_doc diff --git a/csf_tz/stanbic/pgp.py b/csf_tz/stanbic/pgp.py index c92b4606..7c1f0979 100644 --- a/csf_tz/stanbic/pgp.py +++ b/csf_tz/stanbic/pgp.py @@ -2,8 +2,8 @@ def encrypt_pgp(message, key): - """Encrypt a message with a public key""" - key, _ = pgpy.PGPKey.from_blob(key) - message = pgpy.PGPMessage.new(message) - message |= key.pubkey.encrypt(message) - return str(message) + """Encrypt a message with a public key""" + key, _ = pgpy.PGPKey.from_blob(key) + message = pgpy.PGPMessage.new(message) + message |= key.pubkey.encrypt(message) + return str(message) diff --git a/csf_tz/stanbic/sftp.py b/csf_tz/stanbic/sftp.py index b26cd469..ec361185 100644 --- a/csf_tz/stanbic/sftp.py +++ b/csf_tz/stanbic/sftp.py @@ -1,230 +1,224 @@ -import paramiko import os + import frappe +import paramiko + from csf_tz.stanbic.xml import parse_xml class Paramiko: - def __init__(self, hostname, user, key_path, port=22): - self.hostname = hostname - self.user = user - self.port = port - self.key_path = key_path - self.client = paramiko.SSHClient() - self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) - self.connect() - - def connect(self): - print("Connecting to the server") - pkey = paramiko.RSAKey.from_private_key_file(self.key_path) - self.client.connect( - self.hostname, - port=self.port, - username=self.user, - pkey=pkey, - look_for_keys=False, - disabled_algorithms={"pubkeys": ["rsa-sha2-512", "rsa-sha2-256"]}, - timeout=100, - ) - print("Connected to the server") - - def download(self, remote_path, local_path, cleanup=False): - create_dir_if_not_exists(local_path) - try: - sftp = self.client.open_sftp() - print("Connected to the sftp server") - rfiles = sftp.listdir(remote_path) - rfile = "" - for rfile in rfiles: - sftp.get(remote_path + "/" + rfile, local_path + "/" + rfile) - print( - f"Downloaded the file {rfile} from the remote folder {remote_path} to the local folder {local_path}" - ) - if cleanup: - sftp.remove(remote_path + "/" + rfile) - print(f"Removing the files from the remote folder: {rfile}") - sftp.close() - except Exception as e: - frappe.throw(str(e)) - # return list of names of the files downloaded - return os.listdir(local_path) - - def upload(self, local_path, remote_path, cleanup=False): - create_dir_if_not_exists(local_path) - try: - sftp = self.client.open_sftp() - print("Connected to the sftp server") - lfiles = os.listdir(local_path) - lfile = "" - for lfile in lfiles: - sftp.put(local_path + "/" + lfile, remote_path + "/" + lfile) - print( - f"Uploaded the file {lfile} to the local folder {local_path} from the remote folder {remote_path}" - ) - if cleanup: - os.remove(local_path + "/" + lfile) - print(f"Removing the files from the remote folder: {lfile}") - sftp.close() - except Exception as e: - frappe.throw(str(e)) - # return list of names of the files uploaded - return lfiles - - def close(self): - self.client.close() - print("Connection closed") - - def execute(self, command): - stdin, stdout, stderr = self.client.exec_command(command) - print("Executing the command") - return stdout.read().decode("utf-8"), stderr.read().decode("utf-8") + def __init__(self, hostname, user, key_path, port=22): + self.hostname = hostname + self.user = user + self.port = port + self.key_path = key_path + self.client = paramiko.SSHClient() + self.client.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + self.connect() + + def connect(self): + print("Connecting to the server") + pkey = paramiko.RSAKey.from_private_key_file(self.key_path) + self.client.connect( + self.hostname, + port=self.port, + username=self.user, + pkey=pkey, + look_for_keys=False, + disabled_algorithms={"pubkeys": ["rsa-sha2-512", "rsa-sha2-256"]}, + timeout=100, + ) + print("Connected to the server") + + def download(self, remote_path, local_path, cleanup=False): + create_dir_if_not_exists(local_path) + try: + sftp = self.client.open_sftp() + print("Connected to the sftp server") + rfiles = sftp.listdir(remote_path) + rfile = "" + for rfile in rfiles: + sftp.get(remote_path + "/" + rfile, local_path + "/" + rfile) + print( + f"Downloaded the file {rfile} from the remote folder {remote_path} to the local folder {local_path}" + ) + if cleanup: + sftp.remove(remote_path + "/" + rfile) + print(f"Removing the files from the remote folder: {rfile}") + sftp.close() + except Exception as e: + frappe.throw(str(e)) + # return list of names of the files downloaded + return os.listdir(local_path) + + def upload(self, local_path, remote_path, cleanup=False): + create_dir_if_not_exists(local_path) + try: + sftp = self.client.open_sftp() + print("Connected to the sftp server") + lfiles = os.listdir(local_path) + lfile = "" + for lfile in lfiles: + sftp.put(local_path + "/" + lfile, remote_path + "/" + lfile) + print( + f"Uploaded the file {lfile} to the local folder {local_path} from the remote folder {remote_path}" + ) + if cleanup: + os.remove(local_path + "/" + lfile) + print(f"Removing the files from the remote folder: {lfile}") + sftp.close() + except Exception as e: + frappe.throw(str(e)) + # return list of names of the files uploaded + return lfiles + + def close(self): + self.client.close() + print("Connection closed") + + def execute(self, command): + stdin, stdout, stderr = self.client.exec_command(command) + print("Executing the command") + return stdout.read().decode("utf-8"), stderr.read().decode("utf-8") def create_dir_if_not_exists(path): - if not os.path.exists(path): - os.makedirs(path) + if not os.path.exists(path): + os.makedirs(path) def get_site_path(): - return frappe.get_site_path("private", "files") + return frappe.get_site_path("private", "files") -def get_local_path(folders_name=[]): - path = get_absolute_path("/" + "/".join(folders_name)) - return path +def get_local_path(folders_name=None): + if folders_name is None: + folders_name = [] + path = get_absolute_path("/" + "/".join(folders_name)) + return path def download_stanbank_files(settings_name): - # get the files from the stanbic remote folder - # download the files to the local folder + # get the files from the stanbic remote folder + # download the files to the local folder - # get the remote path - remote_path = "/Inbox" - # get the local path - local_path = get_local_path(["private", "files", "stanbic", "inbox"]) + # get the remote path + remote_path = "/Inbox" + # get the local path + local_path = get_local_path(["private", "files", "stanbic", "inbox"]) - # get the settings - settings = frappe.get_cached_doc("Stanbic Setting", settings_name) + # get the settings + settings = frappe.get_cached_doc("Stanbic Setting", settings_name) - key_file_path = get_absolute_path(settings.private_key) - print("key_file_path", key_file_path) + key_file_path = get_absolute_path(settings.private_key) + print("key_file_path", key_file_path) - # create the paramiko object - paramiko_obj = Paramiko( - settings.sftp_url, settings.sftp_user, key_file_path, settings.port - ) + # create the paramiko object + paramiko_obj = Paramiko(settings.sftp_url, settings.sftp_user, key_file_path, settings.port) - # download the files - files = paramiko_obj.download(remote_path, local_path, cleanup=True) + # download the files + files = paramiko_obj.download(remote_path, local_path, cleanup=True) - # close the connection - paramiko_obj.close() + # close the connection + paramiko_obj.close() - return files + return files def upload_stanbank_files(settings_name): - # get the files from the local folder - # upload the files to the stanbic remot folder + # get the files from the local folder + # upload the files to the stanbic remot folder - # get the remote path - remote_path = "/Outbox" + # get the remote path + remote_path = "/Outbox" - # get the local path - local_path = get_local_path(["private", "files", "stanbic", "outbox"]) + # get the local path + local_path = get_local_path(["private", "files", "stanbic", "outbox"]) - # get the settings - settings = frappe.get_cached_doc("Stanbic Setting", settings_name) + # get the settings + settings = frappe.get_cached_doc("Stanbic Setting", settings_name) - key_file_path = get_absolute_path(settings.private_key) - print("key_file_path", key_file_path) + key_file_path = get_absolute_path(settings.private_key) + print("key_file_path", key_file_path) - # create the paramiko object - paramiko_obj = Paramiko( - settings.sftp_url, settings.sftp_user, key_file_path, settings.port - ) + # create the paramiko object + paramiko_obj = Paramiko(settings.sftp_url, settings.sftp_user, key_file_path, settings.port) - # upload the files - files = paramiko_obj.upload(local_path, remote_path, cleanup=True) + # upload the files + files = paramiko_obj.upload(local_path, remote_path, cleanup=True) - # close the connection - paramiko_obj.close() + # close the connection + paramiko_obj.close() - return files + return files def sync_stanbank_files(settings_name, is_test=False): - # upload the files to the stanbic remot folder - # get the files from the stanbic remot folder - # download the files to the local folder + # upload the files to the stanbic remot folder + # get the files from the stanbic remot folder + # download the files to the local folder - # upload the files - upload_files = upload_stanbank_files(settings_name) + # upload the files + upload_files = upload_stanbank_files(settings_name) - # download the files - download_files = download_stanbank_files(settings_name) + # download the files + download_files = download_stanbank_files(settings_name) - return upload_files, download_files + return upload_files, download_files def sync_all_stanbank_files(): - # get all the settings - settings = frappe.get_all("Stanbic Setting", filters={"enabled": 1}) - for setting in settings: - sync_stanbank_files(setting.name, setting.is_test) + # get all the settings + settings = frappe.get_all("Stanbic Setting", filters={"enabled": 1}) + for setting in settings: + sync_stanbank_files(setting.name, setting.is_test) def get_absolute_path(file_path): - from frappe.utils import cstr + from frappe.utils import cstr - site_name = cstr(frappe.local.site) - bench_path = frappe.utils.get_bench_path() + site_name = cstr(frappe.local.site) + bench_path = frappe.utils.get_bench_path() - if file_path.startswith("/files/"): - return bench_path + "/sites/" + site_name + "/public/" + file_path - elif file_path.startswith("/private/files/"): - return bench_path + "/sites/" + site_name + file_path + if file_path.startswith("/files/"): + return bench_path + "/sites/" + site_name + "/public/" + file_path + elif file_path.startswith("/private/files/"): + return bench_path + "/sites/" + site_name + file_path - else: - return file_path + else: + return file_path def process_download_files(): - inbox_file_path = get_local_path(["private", "files", "stanbic", "inbox"]) - os.makedirs(os.path.dirname(inbox_file_path), exist_ok=True) - inbox_files_list = os.listdir(inbox_file_path) - for file in inbox_files_list: - doc_changed = 0 - try: - if file.endswith(".xml"): - if "ACK" in file or "INTAUD" in file or "FINAUD" in file: - path = get_local_path( - ["private", "files", "stanbic", "inbox", file] - ) - - file_dict = parse_xml(path) - file_json = frappe.as_json(file_dict) - print("filename", path) - pain_doc_name = file_dict["Document"]["CstmrPmtStsRpt"][ - "OrgnlGrpInfAndSts" - ]["OrgnlMsgId"] - pain_doc = frappe.get_doc( - "Stanbic Payments Initiation", pain_doc_name - ) - if "ACK" in file and not pain_doc.stanbic_ack: - pain_doc.stanbic_ack = file_json - pain_doc.stanbic_ack_change = 1 - doc_changed = 1 - elif "INTAUD" in file and not pain_doc.stanbic_intaud: - pain_doc.stanbic_intaud = file_json - pain_doc.stanbic_intaud_change = 1 - doc_changed = 1 - elif "FINAUD" in file and not pain_doc.stanbic_finaud: - pain_doc.stanbic_finaud = file_json - pain_doc.stanbic_finaud_change = 1 - doc_changed = 1 - except Exception as e: - print("Error processing the file", file, str(e)) - if doc_changed: - pain_doc.save() - frappe.db.commit() + inbox_file_path = get_local_path(["private", "files", "stanbic", "inbox"]) + os.makedirs(os.path.dirname(inbox_file_path), exist_ok=True) + inbox_files_list = os.listdir(inbox_file_path) + for file in inbox_files_list: + doc_changed = 0 + try: + if file.endswith(".xml"): + if "ACK" in file or "INTAUD" in file or "FINAUD" in file: + path = get_local_path(["private", "files", "stanbic", "inbox", file]) + + file_dict = parse_xml(path) + file_json = frappe.as_json(file_dict) + print("filename", path) + pain_doc_name = file_dict["Document"]["CstmrPmtStsRpt"]["OrgnlGrpInfAndSts"]["OrgnlMsgId"] + pain_doc = frappe.get_doc("Stanbic Payments Initiation", pain_doc_name) + if "ACK" in file and not pain_doc.stanbic_ack: + pain_doc.stanbic_ack = file_json + pain_doc.stanbic_ack_change = 1 + doc_changed = 1 + elif "INTAUD" in file and not pain_doc.stanbic_intaud: + pain_doc.stanbic_intaud = file_json + pain_doc.stanbic_intaud_change = 1 + doc_changed = 1 + elif "FINAUD" in file and not pain_doc.stanbic_finaud: + pain_doc.stanbic_finaud = file_json + pain_doc.stanbic_finaud_change = 1 + doc_changed = 1 + except Exception as e: + print("Error processing the file", file, str(e)) + if doc_changed: + pain_doc.save() + frappe.db.commit() diff --git a/csf_tz/stanbic/xml.py b/csf_tz/stanbic/xml.py index 7472facc..89a7b9e7 100644 --- a/csf_tz/stanbic/xml.py +++ b/csf_tz/stanbic/xml.py @@ -2,6 +2,6 @@ def parse_xml(path): - with open(path, "r") as f: - xml = f.read() - return xmltodict.parse(xml) + with open(path) as f: + xml = f.read() + return xmltodict.parse(xml) diff --git a/csf_tz/utils/authority_notification_settings_fields.py b/csf_tz/utils/authority_notification_settings_fields.py index a42f5543..49420c03 100644 --- a/csf_tz/utils/authority_notification_settings_fields.py +++ b/csf_tz/utils/authority_notification_settings_fields.py @@ -1,4 +1,3 @@ -import frappe from frappe.custom.doctype.custom_field.custom_field import create_custom_fields diff --git a/csf_tz/utils/create_custom_fields.py b/csf_tz/utils/create_custom_fields.py index fedac8cf..1d7f9ec6 100644 --- a/csf_tz/utils/create_custom_fields.py +++ b/csf_tz/utils/create_custom_fields.py @@ -8,73 +8,67 @@ def load_json(file): - CURR_DIR = os.path.abspath(os.path.dirname(__file__)) - json_file_path = os.path.join(CURR_DIR, folder, file) - # TODO do not load the file if already applied - with open(json_file_path, "r") as file: - data = json.load(file) - return data + CURR_DIR = os.path.abspath(os.path.dirname(__file__)) + json_file_path = os.path.join(CURR_DIR, folder, file) + # TODO do not load the file if already applied + with open(json_file_path) as file: + data = json.load(file) + return data def create_fields_from_json(custom_fields_obj): - disallowed_fields = [ - "name", - "owner", - "creation", - "modified", - "modified_by", - "docstatus", - "idx", - "is_system_generated", - "__last_sync_on", - ] - doctype_custom_fields_dict = {} - - for custom_field in custom_fields_obj: - doctype = custom_field["dt"] - if not frappe.db.exists("DocType", doctype): - continue - all_fields = frappe.get_meta("Custom Field").get_valid_columns() - field_list = set(all_fields).difference(disallowed_fields) - custom_field_dict = {} - for field_name in field_list: - custom_field_dict[field_name] = custom_field.get(field_name) - - # Ensure the list for the doctype is initialized - if doctype not in doctype_custom_fields_dict: - doctype_custom_fields_dict[doctype] = [] - - doctype_custom_fields_dict[doctype].append(custom_field_dict) - - create_custom_fields(doctype_custom_fields_dict, update=False) + disallowed_fields = [ + "name", + "owner", + "creation", + "modified", + "modified_by", + "docstatus", + "idx", + "is_system_generated", + "__last_sync_on", + ] + doctype_custom_fields_dict = {} + + for custom_field in custom_fields_obj: + doctype = custom_field["dt"] + if not frappe.db.exists("DocType", doctype): + continue + all_fields = frappe.get_meta("Custom Field").get_valid_columns() + field_list = set(all_fields).difference(disallowed_fields) + custom_field_dict = {} + for field_name in field_list: + custom_field_dict[field_name] = custom_field.get(field_name) + + # Ensure the list for the doctype is initialized + if doctype not in doctype_custom_fields_dict: + doctype_custom_fields_dict[doctype] = [] + + doctype_custom_fields_dict[doctype].append(custom_field_dict) + + create_custom_fields(doctype_custom_fields_dict, update=False) def execute(): - # read names of only json files in this folder and put it into files list - files = list( - filter( - lambda x: x.endswith(".json"), - os.listdir( - os.path.join(os.path.abspath(os.path.dirname(__file__)), folder) - ), - ) - ) - for file in files: - data = load_json(file) - create_fields_from_json(data) + # read names of only json files in this folder and put it into files list + files = list( + filter( + lambda x: x.endswith(".json"), + os.listdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), folder)), + ) + ) + for file in files: + data = load_json(file) + create_fields_from_json(data) @frappe.whitelist() def export_custom_fields(docnames): - docnames = frappe.parse_json(docnames) - custom_fields = [] - - for docname in docnames: - doc = frappe.get_doc("Custom Field", docname) - custom_fields.append( - doc.as_dict( - convert_dates_to_str=True, no_default_fields=True, no_nulls=True - ) - ) - - return str(custom_fields) + docnames = frappe.parse_json(docnames) + custom_fields = [] + + for docname in docnames: + doc = frappe.get_doc("Custom Field", docname) + custom_fields.append(doc.as_dict(convert_dates_to_str=True, no_default_fields=True, no_nulls=True)) + + return str(custom_fields) diff --git a/csf_tz/utils/create_property_setter.py b/csf_tz/utils/create_property_setter.py index 17a5919a..b4920f20 100644 --- a/csf_tz/utils/create_property_setter.py +++ b/csf_tz/utils/create_property_setter.py @@ -8,64 +8,67 @@ def load_json(file): - CURR_DIR = os.path.abspath(os.path.dirname(__file__)) - json_file_path = os.path.join(CURR_DIR, folder, file) - with open(json_file_path, "r") as file: - data = json.load(file) - return data + CURR_DIR = os.path.abspath(os.path.dirname(__file__)) + json_file_path = os.path.join(CURR_DIR, folder, file) + with open(json_file_path) as file: + data = json.load(file) + return data def create_property_setter_from_json(property_setters_obj): - disallowed_fields = [ - "name", - "owner", - "creation", - "modified", - "modified_by", - "docstatus", - "idx", - "is_system_generated", - "__last_sync_on", - ] + disallowed_fields = [ + "name", + "owner", + "creation", + "modified", + "modified_by", + "docstatus", + "idx", + "is_system_generated", + "__last_sync_on", + ] - existing_setters = {d.name for d in frappe.db.get_all("Property Setter", fields=["name"],page_length=10000)} + existing_setters = { + d.name for d in frappe.db.get_all("Property Setter", fields=["name"], page_length=10000) + } - for property_setter in property_setters_obj: - if property_setter.get('name') in existing_setters: - continue + for property_setter in property_setters_obj: + if property_setter.get("name") in existing_setters: + continue - if not frappe.db.exists("DocType", property_setter.get("doc_type")): - continue + if not frappe.db.exists("DocType", property_setter.get("doc_type")): + continue - if property_setter.get('doctype_or_field') == "DocType": - for_doctype = True - else: - for_doctype = False + if property_setter.get("doctype_or_field") == "DocType": + for_doctype = True + else: + for_doctype = False + + all_fields = frappe.get_meta("Property Setter").get_valid_columns() + field_list = set(all_fields).difference(disallowed_fields) + + property_setter_dict = { + field: property_setter.get(field) for field in field_list if field in property_setter + } + + make_property_setter( + doctype=property_setter_dict["doc_type"], + fieldname=property_setter_dict.get("field_name", None), + property=property_setter_dict["property"], + value=property_setter_dict["value"], + property_type=property_setter_dict["property_type"], + for_doctype=for_doctype, + ) - all_fields = frappe.get_meta("Property Setter").get_valid_columns() - field_list = set(all_fields).difference(disallowed_fields) - - property_setter_dict = {field: property_setter.get(field) for field in field_list if field in property_setter} - - make_property_setter( - doctype=property_setter_dict['doc_type'], - fieldname=property_setter_dict.get('field_name', None), - property=property_setter_dict['property'], - value=property_setter_dict['value'], - property_type=property_setter_dict['property_type'], - for_doctype=for_doctype - ) def execute(): - # read names of only json files in this folder and put it into files list - files = list( - filter( - lambda x: x.endswith(".json"), - os.listdir( - os.path.join(os.path.abspath(os.path.dirname(__file__)), folder) - ), - ) - ) - for file in files: - data = load_json(file) - create_property_setter_from_json(data) + # read names of only json files in this folder and put it into files list + files = list( + filter( + lambda x: x.endswith(".json"), + os.listdir(os.path.join(os.path.abspath(os.path.dirname(__file__)), folder)), + ) + ) + for file in files: + data = load_json(file) + create_property_setter_from_json(data) diff --git a/csf_tz/utils/fix_balance_qty.py b/csf_tz/utils/fix_balance_qty.py index 9f052888..d039462a 100644 --- a/csf_tz/utils/fix_balance_qty.py +++ b/csf_tz/utils/fix_balance_qty.py @@ -1,7 +1,8 @@ import frappe -from frappe.utils import add_to_date, now -from frappe.query_builder.functions import Coalesce, CombineDatetime from erpnext.stock.stock_ledger import get_previous_sle_of_current_voucher +from frappe.query_builder.functions import CombineDatetime +from frappe.utils import add_to_date, now + def has_correct_balance_qty(previous_sle, sles): balance_qty = previous_sle.qty_after_transaction @@ -17,6 +18,7 @@ def has_correct_balance_qty(previous_sle, sles): return True + def create_repost_item_valuation_entry(args): args = frappe._dict(args) repost_entry = frappe.new_doc("Repost Item Valuation") @@ -32,31 +34,38 @@ def create_repost_item_valuation_entry(args): repost_entry.save() repost_entry.submit() -from_time = add_to_date(now(), hours=-2) -table = frappe.qb.DocType("Stock Ledger Entry") -sles = ( - frappe.qb.from_(table) - .select(table.item_code, table.warehouse, - table.voucher_type, table.voucher_no, - table.posting_date, table.posting_time, table.qty_after_transaction) - .where( - (table.is_cancelled == 0) - & (CombineDatetime(table.posting_date, table.posting_time) >= from_time) - ) - .orderby(CombineDatetime(table.posting_date, table.posting_time)) - .orderby(table.creation) -).run(as_dict=True) +def execute(): + from_time = add_to_date(now(), hours=-2) + + table = frappe.qb.DocType("Stock Ledger Entry") + sles = ( + frappe.qb.from_(table) + .select( + table.item_code, + table.warehouse, + table.voucher_type, + table.voucher_no, + table.posting_date, + table.posting_time, + table.qty_after_transaction, + ) + .where( + (table.is_cancelled == 0) & (CombineDatetime(table.posting_date, table.posting_time) >= from_time) + ) + .orderby(CombineDatetime(table.posting_date, table.posting_time)) + .orderby(table.creation) + ).run(as_dict=True) -checked_item_warehouse = [] + checked_item_warehouse = [] -for sle in sles: - if [sle.item_code, sle.warehouse] in checked_item_warehouse: - continue + for sle in sles: + if [sle.item_code, sle.warehouse] in checked_item_warehouse: + continue - checked_item_warehouse.append([sle.item_code, sle.warehouse]) + checked_item_warehouse.append([sle.item_code, sle.warehouse]) - previous_sle = get_previous_sle_of_current_voucher(sle, exclude_current_voucher=True) + previous_sle = get_previous_sle_of_current_voucher(sle, exclude_current_voucher=True) - if not has_correct_balance_qty(previous_sle, sles): - create_repost_item_valuation_entry(previous_sle) \ No newline at end of file + if not has_correct_balance_qty(previous_sle, sles): + create_repost_item_valuation_entry(previous_sle) diff --git a/csf_tz/utils/setup.py b/csf_tz/utils/setup.py index a61f2a93..9c5586f7 100644 --- a/csf_tz/utils/setup.py +++ b/csf_tz/utils/setup.py @@ -5,7 +5,6 @@ import frappe - SETUP_SPECS = ( { "file": "accounts.json", diff --git a/csf_tz/vehicle_authority.py b/csf_tz/vehicle_authority.py index 6d4349fc..4aaa6043 100644 --- a/csf_tz/vehicle_authority.py +++ b/csf_tz/vehicle_authority.py @@ -1,6 +1,5 @@ import frappe - AUTHORITY_REFERENCE_TYPES = ( "LATRA License", "LATRA Offence", @@ -46,11 +45,7 @@ def get_vehicle_plate(doc_or_values, meta=None): def has_vehicle_plate_field(meta): - return any( - meta.has_field(fieldname) - for fieldname in PLATE_FIELD_CANDIDATES - if fieldname != "name" - ) + return any(meta.has_field(fieldname) for fieldname in PLATE_FIELD_CANDIDATES if fieldname != "name") def get_vehicle_like_doctypes(): diff --git a/csf_tz/vfd_providers/doctype/simplify_vfd_settings/simplify_vfd_settings.py b/csf_tz/vfd_providers/doctype/simplify_vfd_settings/simplify_vfd_settings.py index 79fad736..9505fb5e 100644 --- a/csf_tz/vfd_providers/doctype/simplify_vfd_settings/simplify_vfd_settings.py +++ b/csf_tz/vfd_providers/doctype/simplify_vfd_settings/simplify_vfd_settings.py @@ -1,414 +1,385 @@ # Copyright (c) 2023, Aakvatech Limited and contributors # For license information, please see license.txt -import frappe -from frappe.model.document import Document +import json +from datetime import datetime from time import sleep -import frappe, json, requests + +import frappe +import requests from frappe import _ +from frappe.model.document import Document from frappe.utils import ( - nowdate, - nowtime, - format_datetime, - flt, - now_datetime, - add_to_date, + add_to_date, + flt, + format_datetime, + now_datetime, + nowdate, + nowtime, ) -from datetime import datetime -from csf_tz.vfd_providers.utils import get_vat_amount +from csf_tz.vfd_providers.utils import get_vat_amount # The process of getting the access token and refresh token is as follows: - # 1. Call the login endpoint with username and password to get the access token and refresh token. - # 2. Store the access token and refresh token in the Simplify VFD Settings - # 3. Set the token expiration time to 20 minutes from now for the access token - # 5. If the access token is expired, call the refresh token endpoint with the refresh token - # 6. If the refresh token is expired (after 24 hours), call the login endpoint again to get a new refresh token. +# 1. Call the login endpoint with username and password to get the access token and refresh token. +# 2. Store the access token and refresh token in the Simplify VFD Settings +# 3. Set the token expiration time to 20 minutes from now for the access token +# 5. If the access token is expired, call the refresh token endpoint with the refresh token +# 6. If the refresh token is expired (after 24 hours), call the login endpoint again to get a new refresh token. class SimplifyVFDSettings(Document): - @frappe.whitelist() - def get_bearer_token(self): - """Get bearer token from Simplify VFD""" + @frappe.whitelist() + def get_bearer_token(self): + """Get bearer token from Simplify VFD""" - if not self.username or not self.password: - frappe.throw(_("Username and Password are required!")) + if not self.username or not self.password: + frappe.throw(_("Username and Password are required!")) - payload = { - "username": self.username, - "password": self.get_password(), - } + payload = { + "username": self.username, + "password": self.get_password(), + } - data = send_simplify_vfd_request( - "login", self.company, json.dumps(payload), "POST" - ) + data = send_simplify_vfd_request("login", self.company, json.dumps(payload), "POST") - token = data.get("token") - if not token: - frappe.throw(_("Invalid username or password!")) + token = data.get("token") + if not token: + frappe.throw(_("Invalid username or password!")) - refresh_token = data.get("refresh_token") - token_expires = add_to_date(now_datetime(), minutes=20) + refresh_token = data.get("refresh_token") + token_expires = add_to_date(now_datetime(), minutes=20) - self.db_set("bearer_token", token) - self.db_set("refresh_token", refresh_token) - self.db_set("token_expires", token_expires) + self.db_set("bearer_token", token) + self.db_set("refresh_token", refresh_token) + self.db_set("token_expires", token_expires) - self.clear_cache() - self.reload() - return True + self.clear_cache() + self.reload() + return True - def refresh_bearer_token(self): - """Refresh bearer token from Simplify VFD""" + def refresh_bearer_token(self): + """Refresh bearer token from Simplify VFD""" - if not self.refresh_token: - frappe.throw( - _( - "Refresh Token is not found, Please set username and password and generate the token!" - ) - ) + if not self.refresh_token: + frappe.throw( + _("Refresh Token is not found, Please set username and password and generate the token!") + ) - payload = { - "refresh_token": self.get_password("refresh_token"), - } + payload = { + "refresh_token": self.get_password("refresh_token"), + } - data = send_simplify_vfd_request( - "refresh", self.company, json.dumps(payload), "POST" - ) - token = data.get("token") - refresh_token = data.get("refresh_token") + data = send_simplify_vfd_request("refresh", self.company, json.dumps(payload), "POST") + token = data.get("token") + refresh_token = data.get("refresh_token") - if not token or not refresh_token: - frappe.throw(_("Invalid refresh token!")) + if not token or not refresh_token: + frappe.throw(_("Invalid refresh token!")) - token_expires = add_to_date(now_datetime(), minutes=20) - self.db_set("bearer_token", token) - self.db_set("refresh_token", refresh_token) - self.db_set("token_expires", token_expires) + token_expires = add_to_date(now_datetime(), minutes=20) + self.db_set("bearer_token", token) + self.db_set("refresh_token", refresh_token) + self.db_set("token_expires", token_expires) - self.clear_cache() - self.reload() - return True + self.clear_cache() + self.reload() + return True def get_access_token(): - """Refresh access token from Simplify VFD""" + """Refresh access token from Simplify VFD""" - setting_companies = frappe.get_all( - "Simplify VFD Settings", fields=["name"], pluck="name" - ) + setting_companies = frappe.get_all("Simplify VFD Settings", fields=["name"], pluck="name") - for company in setting_companies: - doc = None - if frappe.db.exists("Simplify VFD Settings", company): - doc = frappe.get_cached_doc("Simplify VFD Settings", company) - else: - continue + for company in setting_companies: + doc = None + if frappe.db.exists("Simplify VFD Settings", company): + doc = frappe.get_cached_doc("Simplify VFD Settings", company) + else: + continue - if doc.token_expires and doc.token_expires <= now_datetime(): - doc.refresh_bearer_token() + if doc.token_expires and doc.token_expires <= now_datetime(): + doc.refresh_bearer_token() def get_refresh_token(): - """Fetch refresh token from Simplify VFD""" + """Fetch refresh token from Simplify VFD""" - setting_companies = frappe.get_all( - "Simplify VFD Settings", fields=["name"], pluck="name" - ) + setting_companies = frappe.get_all("Simplify VFD Settings", fields=["name"], pluck="name") - for company in setting_companies: - doc = None - if frappe.db.exists("Simplify VFD Settings", company): - doc = frappe.get_cached_doc("Simplify VFD Settings", company) - else: - continue + for company in setting_companies: + doc = None + if frappe.db.exists("Simplify VFD Settings", company): + doc = frappe.get_cached_doc("Simplify VFD Settings", company) + else: + continue - doc.get_bearer_token() + doc.get_bearer_token() @frappe.whitelist() def get_payload(doc): - """Generate payload for Simplify VFD""" - - items = [] - total_amount = 0 - - tax_map = { - "1": "STANDARD", - "2": "SPECIAL_RATE", - "3": "ZERO_RATED", - "4": "SPECIAL_RELIEF", - "5": "EXEMPTED", - } - vfd_cust_id_type_map = { - "1": "TAX_IDENTIFICATION_NUMBER", - "2": "DRIVING_LICENCE", - "3": "VOTERS_NUMBER", - "4": "PASSPORT", - "5": "NATIONAL_IDENTIFICATION_AUTHORITY", - "6": "NO_IDENTIFICATION", - } - - for item in doc.items: - vfd_taxcode = frappe.get_cached_value( - "Item Tax Template", item.item_tax_template, "vfd_taxcode" - ) - - vat_rate_id = vfd_taxcode[:1] if vfd_taxcode else "1" - - vat_group = tax_map[vat_rate_id] - - price = get_vat_amount(item, vat_rate_id, precision=2) - - unit_price = flt((price / item.qty), 2) - - items.append( - { - "description": f"{item.item_code} - {item.item_name}" if item.item_name != item.item_code else item.item_code, - "quantity": item.qty, - "unitAmount": unit_price, - "discountRate": 0.0, - "taxType": vat_group, - } - ) - - total_amount += flt((unit_price * item.qty), 2) - - payments = [ - { - "type": "INVOICE", - "amount": flt(total_amount, 2) - } - ] - - vfd_cust_id_type = doc.vfd_cust_id_type[:1] if doc.vfd_cust_id_type else "6" - payload = { - "dateTime": str(doc.vfd_date or nowdate()), - "customer": { - "identificationType": vfd_cust_id_type_map[vfd_cust_id_type], - "identificationNumber": doc.vfd_cust_id if vfd_cust_id_type != "6" else "", - "vatRegistrationNumber": doc.tax_id or "", - "name": doc.customer_name, - "mobileNumber": "", - "email": "", - }, - "invoiceAmountType": "INCLUSIVE", - "items": items, - "payments": payments, - "partnerInvoiceId": doc.name, - } - - return payload + """Generate payload for Simplify VFD""" + + items = [] + total_amount = 0 + + tax_map = { + "1": "STANDARD", + "2": "SPECIAL_RATE", + "3": "ZERO_RATED", + "4": "SPECIAL_RELIEF", + "5": "EXEMPTED", + } + vfd_cust_id_type_map = { + "1": "TAX_IDENTIFICATION_NUMBER", + "2": "DRIVING_LICENCE", + "3": "VOTERS_NUMBER", + "4": "PASSPORT", + "5": "NATIONAL_IDENTIFICATION_AUTHORITY", + "6": "NO_IDENTIFICATION", + } + + for item in doc.items: + vfd_taxcode = frappe.get_cached_value("Item Tax Template", item.item_tax_template, "vfd_taxcode") + + vat_rate_id = vfd_taxcode[:1] if vfd_taxcode else "1" + + vat_group = tax_map[vat_rate_id] + + price = get_vat_amount(item, vat_rate_id, precision=2) + + unit_price = flt((price / item.qty), 2) + + items.append( + { + "description": f"{item.item_code} - {item.item_name}" + if item.item_name != item.item_code + else item.item_code, + "quantity": item.qty, + "unitAmount": unit_price, + "discountRate": 0.0, + "taxType": vat_group, + } + ) + + total_amount += flt((unit_price * item.qty), 2) + + payments = [{"type": "INVOICE", "amount": flt(total_amount, 2)}] + + vfd_cust_id_type = doc.vfd_cust_id_type[:1] if doc.vfd_cust_id_type else "6" + payload = { + "dateTime": str(doc.vfd_date or nowdate()), + "customer": { + "identificationType": vfd_cust_id_type_map[vfd_cust_id_type], + "identificationNumber": doc.vfd_cust_id if vfd_cust_id_type != "6" else "", + "vatRegistrationNumber": doc.tax_id or "", + "name": doc.customer_name, + "mobileNumber": "", + "email": "", + }, + "invoiceAmountType": "INCLUSIVE", + "items": items, + "payments": payments, + "partnerInvoiceId": doc.name, + } + + return payload @frappe.whitelist() -def post_fiscal_receipt( - doc=None, - method="POST", - payload={}, - invoice_id=None, - preview=False -): - """Post fiscal receipt to Simplify VFD - Parameters - ---------- - doc : object - Python object which is expected to be from Sales Invoice doctype. - method : str - Method name which is calling this function. e.g. POST, validate, on_update, etc. - payload : dict - Payload to send to Simplify VFD API - invoice_id : str - Sales Invoice ID to post fiscal receipt for. If doc is not provided, this parameter is required. - - Returns - ------- - res_data : dict - Dictionary with response from Simplify VFD API - """ - - if not doc and not invoice_id: - frappe.throw(_("Sales Invoice is required!")) - - if not doc and invoice_id: - doc = frappe.get_doc("Sales Invoice", invoice_id) - - simplify_vfd_settings = frappe.get_doc("Simplify VFD Settings", doc.company) - if ( - simplify_vfd_settings.token_expires - and simplify_vfd_settings.token_expires <= now_datetime() - ): - simplify_vfd_settings.refresh_bearer_token() - - doc.vfd_date = doc.vfd_date or nowdate() - doc.vfd_time = format_datetime(str(nowtime()), "HH:mm:ss") - - if not payload: - payload = get_payload(doc) - - # Convert the payload to JSON string format because it is not comming from frontend where it is already in JSON string format - payload = json.dumps(payload) - - vfd_provider_posting_doc = frappe.new_doc("VFD Provider Posting") - - data = send_simplify_vfd_request( - "createIssuedInvoice", - doc.company, - payload, - "POST", - for_vfd_posting=True, - ) - - res_data = data.get("message") - if res_data.get("success"): - dt_object = datetime.strptime(res_data.get("issuedAt"), "%Y-%m-%d %H:%M:%S") - - # Extract date and time - date_part = dt_object.date() - time_part = dt_object.time() - - else: - date_part = nowdate() - time_part = nowtime() - - vfd_provider_posting_doc.sales_invoice = doc.name - vfd_provider_posting_doc.rctnum = doc.vfd_rctvnum - vfd_provider_posting_doc.req_headers = str(data.get("headers")) - vfd_provider_posting_doc.ackmsg = str(res_data) - vfd_provider_posting_doc.ackcode = data.get("status_code") - vfd_provider_posting_doc.date = date_part - vfd_provider_posting_doc.time = time_part - vfd_provider_posting_doc.req_data = payload - - vfd_provider_posting_doc.save(ignore_permissions=True) - - if method == "on_submit": - doc.vfd_status = "Success" if res_data.get("success") else "Failed" - doc.vfd_verification_url = res_data.get("verificationUrl") - doc.vfd_rctvnum = res_data.get("verificationCode") - doc.vfd_date = date_part - doc.vfd_time = time_part - doc.vfd_posting_info = vfd_provider_posting_doc.name - doc.save(ignore_permissions=True) - doc.add_comment( - "Comment", - f"VFD Invoice ID: {res_data.get('invoiceId')}", - ) - - elif method == "POST": - frappe.db.set_value( - "Sales Invoice", - doc.name, - { - "vfd_rctvnum": res_data.get("verificationCode"), - "vfd_status": "Success", - "vfd_verification_url": res_data.get("verificationUrl"), - "vfd_date": date_part, - "vfd_time": time_part, - "vfd_posting_info": vfd_provider_posting_doc.name, - }, - ) - doc.add_comment( - "Comment", - f"VFD Invoice ID: {res_data.get('invoiceId')}", - ) - frappe.db.commit() - - return {"data": res_data, "vfd_provider": "SimplifyVFD", "preview": preview} +def post_fiscal_receipt(doc=None, method="POST", payload=None, invoice_id=None, preview=False): + """Post fiscal receipt to Simplify VFD + Parameters + ---------- + doc : object + Python object which is expected to be from Sales Invoice doctype. + method : str + Method name which is calling this function. e.g. POST, validate, on_update, etc. + payload : dict + Payload to send to Simplify VFD API + invoice_id : str + Sales Invoice ID to post fiscal receipt for. If doc is not provided, this parameter is required. + + Returns + ------- + res_data : dict + Dictionary with response from Simplify VFD API + """ + + if payload is None: + payload = {} + if not doc and not invoice_id: + frappe.throw(_("Sales Invoice is required!")) + + if not doc and invoice_id: + doc = frappe.get_doc("Sales Invoice", invoice_id) + + simplify_vfd_settings = frappe.get_doc("Simplify VFD Settings", doc.company) + if simplify_vfd_settings.token_expires and simplify_vfd_settings.token_expires <= now_datetime(): + simplify_vfd_settings.refresh_bearer_token() + + doc.vfd_date = doc.vfd_date or nowdate() + doc.vfd_time = format_datetime(str(nowtime()), "HH:mm:ss") + + if not payload: + payload = get_payload(doc) + + # Convert the payload to JSON string format because it is not comming from frontend where it is already in JSON string format + payload = json.dumps(payload) + + vfd_provider_posting_doc = frappe.new_doc("VFD Provider Posting") + + data = send_simplify_vfd_request( + "createIssuedInvoice", + doc.company, + payload, + "POST", + for_vfd_posting=True, + ) + + res_data = data.get("message") + if res_data.get("success"): + dt_object = datetime.strptime(res_data.get("issuedAt"), "%Y-%m-%d %H:%M:%S") + + # Extract date and time + date_part = dt_object.date() + time_part = dt_object.time() + + else: + date_part = nowdate() + time_part = nowtime() + + vfd_provider_posting_doc.sales_invoice = doc.name + vfd_provider_posting_doc.rctnum = doc.vfd_rctvnum + vfd_provider_posting_doc.req_headers = str(data.get("headers")) + vfd_provider_posting_doc.ackmsg = str(res_data) + vfd_provider_posting_doc.ackcode = data.get("status_code") + vfd_provider_posting_doc.date = date_part + vfd_provider_posting_doc.time = time_part + vfd_provider_posting_doc.req_data = payload + + vfd_provider_posting_doc.save(ignore_permissions=True) + + if method == "on_submit": + doc.vfd_status = "Success" if res_data.get("success") else "Failed" + doc.vfd_verification_url = res_data.get("verificationUrl") + doc.vfd_rctvnum = res_data.get("verificationCode") + doc.vfd_date = date_part + doc.vfd_time = time_part + doc.vfd_posting_info = vfd_provider_posting_doc.name + doc.save(ignore_permissions=True) + doc.add_comment( + "Comment", + f"VFD Invoice ID: {res_data.get('invoiceId')}", + ) + + elif method == "POST": + frappe.db.set_value( + "Sales Invoice", + doc.name, + { + "vfd_rctvnum": res_data.get("verificationCode"), + "vfd_status": "Success", + "vfd_verification_url": res_data.get("verificationUrl"), + "vfd_date": date_part, + "vfd_time": time_part, + "vfd_posting_info": vfd_provider_posting_doc.name, + }, + ) + doc.add_comment( + "Comment", + f"VFD Invoice ID: {res_data.get('invoiceId')}", + ) + frappe.db.commit() + + return {"data": res_data, "vfd_provider": "SimplifyVFD", "preview": preview} def send_simplify_vfd_request( - call_type, - company, - payload=None, - type="GET", - simplify_vfd_settings=None, - for_vfd_posting=False, + call_type, + company, + payload=None, + type="GET", + simplify_vfd_settings=None, + for_vfd_posting=False, ): - """Send request to Simplify VFD API - Parameters - ---------- - call_type : str - Type of call to make. e.g. "get_serial_info", "post_fiscal_receipt", "account_info", etc. - company : str - Company to get Simplify VFD settings from - payload : dict - Payload to send to Simplify VFD API - type : str - Type of request to make. e.g. "GET", "POST", "PUT", etc. - simplify_vfd_settings : object - Python object which is expected to be from Simplify VFD Settings doctype. - for_vfd_posting : Boolean - If True, will return headers along with response data. Default is False. - - Returns - ------- - data : dict - Dictionary with response from Simplify VFD API - """ - simplify_vfd = frappe.get_cached_doc("VFD Provider", "SimplifyVFD") - - if not simplify_vfd_settings: - simplify_vfd_settings = frappe.get_cached_doc( - simplify_vfd.vfd_provider_settings, company - ) - - simplify_vfd_endpoint = [ - row for row in simplify_vfd.attributes if row.key == call_type - ][0].value - - url = f"{simplify_vfd.base_url.strip()}{simplify_vfd_endpoint.strip()}" - - headers = { - "accept": "application/json", - "Content-Type": "application/json", - } - if call_type not in ["login", "refresh"]: - headers["Authorization"] = ( - f"Bearer {simplify_vfd_settings.get_password('bearer_token')}" - ) - - data = None - status_code = None - for i in range(3): - try: - res = requests.request( - method=type, - url=url, - data=payload if payload else None, - headers=headers, - timeout=500, - ) - if res.ok: - data = json.loads(res.text) - status_code = res.status_code - else: - data = [] - status_code = res.status_code - frappe.log_error( - title="Send Request Error", - message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}\n{payload}", - ) - frappe.throw(f"Error is {res.text}") - - break - except Exception as e: - sleep(3 * i + 1) - if i != 2: - continue - else: - frappe.log_error( - message=frappe.get_traceback(), - title=str(e)[:140] if e else "Send Simplify VFD Request Error", - ) - raise e - - if for_vfd_posting: - data = { - "message": data, - "headers": headers, - "status_code": res.status_code, - } - return data - - return data + """Send request to Simplify VFD API + Parameters + ---------- + call_type : str + Type of call to make. e.g. "get_serial_info", "post_fiscal_receipt", "account_info", etc. + company : str + Company to get Simplify VFD settings from + payload : dict + Payload to send to Simplify VFD API + type : str + Type of request to make. e.g. "GET", "POST", "PUT", etc. + simplify_vfd_settings : object + Python object which is expected to be from Simplify VFD Settings doctype. + for_vfd_posting : Boolean + If True, will return headers along with response data. Default is False. + + Returns + ------- + data : dict + Dictionary with response from Simplify VFD API + """ + simplify_vfd = frappe.get_cached_doc("VFD Provider", "SimplifyVFD") + + if not simplify_vfd_settings: + simplify_vfd_settings = frappe.get_cached_doc(simplify_vfd.vfd_provider_settings, company) + + simplify_vfd_endpoint = [row for row in simplify_vfd.attributes if row.key == call_type][0].value + + url = f"{simplify_vfd.base_url.strip()}{simplify_vfd_endpoint.strip()}" + + headers = { + "accept": "application/json", + "Content-Type": "application/json", + } + if call_type not in ["login", "refresh"]: + headers["Authorization"] = f"Bearer {simplify_vfd_settings.get_password('bearer_token')}" + + data = None + for i in range(3): + try: + res = requests.request( + method=type, + url=url, + data=payload if payload else None, + headers=headers, + timeout=500, + ) + if res.ok: + data = json.loads(res.text) + else: + data = [] + frappe.log_error( + title="Send Request Error", + message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}\n{payload}", + ) + frappe.throw(f"Error is {res.text}") + + break + except Exception as e: + sleep(3 * i + 1) + if i != 2: + continue + else: + frappe.log_error( + message=frappe.get_traceback(), + title=str(e)[:140] if e else "Send Simplify VFD Request Error", + ) + raise e + + if for_vfd_posting: + data = { + "message": data, + "headers": headers, + "status_code": res.status_code, + } + return data + + return data diff --git a/csf_tz/vfd_providers/doctype/simplify_vfd_settings/test_simplify_vfd_settings.py b/csf_tz/vfd_providers/doctype/simplify_vfd_settings/test_simplify_vfd_settings.py index d6bc95c8..c76d076b 100644 --- a/csf_tz/vfd_providers/doctype/simplify_vfd_settings/test_simplify_vfd_settings.py +++ b/csf_tz/vfd_providers/doctype/simplify_vfd_settings/test_simplify_vfd_settings.py @@ -6,4 +6,4 @@ class TestSimplifyVFDSettings(FrappeTestCase): - pass + pass diff --git a/csf_tz/vfd_providers/doctype/total_vfd_setting/total_vfd_setting.py b/csf_tz/vfd_providers/doctype/total_vfd_setting/total_vfd_setting.py index 498869a9..155d153a 100644 --- a/csf_tz/vfd_providers/doctype/total_vfd_setting/total_vfd_setting.py +++ b/csf_tz/vfd_providers/doctype/total_vfd_setting/total_vfd_setting.py @@ -2,299 +2,279 @@ # For license information, please see license.txt # import frappe -from frappe.model.document import Document +import json from time import sleep -import frappe, json, requests + +import frappe +import requests from frappe import _ -from frappe.utils import nowdate, nowtime, format_datetime, flt +from frappe.model.document import Document +from frappe.utils import flt, format_datetime, nowdate, nowtime + from csf_tz.vfd_providers.utils import get_vat_amount class TotalVFDSetting(Document): - pass + pass def get_payload(doc): - """Generate payload for Total VFD - Parameters - ---------- - doc : object - Python object which is expected to be from Sales Invoice doctype. - - Returns - ------- - payload : dict - Dictionary with payload for Total VFD - """ - total_vfd_setting = frappe.get_doc("Total VFD Setting", doc.company) - - if total_vfd_setting.is_vat_grouped: - vat_grouped = 1 - else: - vat_grouped = 0 - - items = [] - total_amount = 0 - vat_group_totals = {} - tax_map = {"1": "A", "2": "B", "3": "C", "4": "D", "5": "E"} - - for item in doc.items: - vat_rate_id = frappe.get_cached_value( - "Item Tax Template", item.item_tax_template, "vfd_taxcode" - )[:1] - - vat_group = tax_map[vat_rate_id] - - price = get_vat_amount(item, vat_group, precision=2) - - # Check if the VAT group already exists in the dictionary; if not, initialize it - if vat_group not in vat_group_totals: - vat_group_totals[vat_group] = 0 - - # Add the calculated price to the respective VAT group's total - vat_group_totals[vat_group] += price - items.append( - { - "id": item.item_code, - "name": item.item_name, - "price": price, - "qty": item.qty, - "vatGroup": vat_group, - "discount": 0.0, - } - ) - total_amount += price - - # Convert the aggregated totals into a list of dictionaries - vat_group_totals_list = [ - {"vat_group": vat_group, "total_price": flt(total_price, 2)} - for vat_group, total_price in vat_group_totals.items() - ] - - if vat_grouped: - # Re-create items list based on VAT group totals - items = [] - total_amount = 0 - for vat_group_entry in vat_group_totals_list: - items.append( - { - "id": f"""Items in VAT Group {vat_group_entry["vat_group"]}""", - "name": f"""Items in VAT Group {vat_group_entry["vat_group"]}""", - "price": flt(vat_group_entry["total_price"], 2), - "qty": 1, - "vatGroup": vat_group_entry["vat_group"], - "discount": 0.0, - } - ) - - total_amount += flt(vat_group_entry["total_price"], 2) - - vfd_cust_id_type = doc.vfd_cust_id_type[:1] or "6" - - payload = { - "serial": total_vfd_setting.serial_id, - "referenceNumber": doc.name, - "customer": { - "name": doc.customer_name, - "idType": vfd_cust_id_type, - "idValue": doc.vfd_cust_id if vfd_cust_id_type != "6" else "", - "mobile": "", - }, - "payments": [ - { - "type": "invoice", - "amount": flt(total_amount, 2) - } - ], - "items": items, - } - return payload + """Generate payload for Total VFD + Parameters + ---------- + doc : object + Python object which is expected to be from Sales Invoice doctype. + + Returns + ------- + payload : dict + Dictionary with payload for Total VFD + """ + total_vfd_setting = frappe.get_doc("Total VFD Setting", doc.company) + + if total_vfd_setting.is_vat_grouped: + vat_grouped = 1 + else: + vat_grouped = 0 + + items = [] + total_amount = 0 + vat_group_totals = {} + tax_map = {"1": "A", "2": "B", "3": "C", "4": "D", "5": "E"} + + for item in doc.items: + vat_rate_id = frappe.get_cached_value("Item Tax Template", item.item_tax_template, "vfd_taxcode")[:1] + + vat_group = tax_map[vat_rate_id] + + price = get_vat_amount(item, vat_group, precision=2) + + # Check if the VAT group already exists in the dictionary; if not, initialize it + if vat_group not in vat_group_totals: + vat_group_totals[vat_group] = 0 + + # Add the calculated price to the respective VAT group's total + vat_group_totals[vat_group] += price + items.append( + { + "id": item.item_code, + "name": item.item_name, + "price": price, + "qty": item.qty, + "vatGroup": vat_group, + "discount": 0.0, + } + ) + total_amount += price + + # Convert the aggregated totals into a list of dictionaries + vat_group_totals_list = [ + {"vat_group": vat_group, "total_price": flt(total_price, 2)} + for vat_group, total_price in vat_group_totals.items() + ] + + if vat_grouped: + # Re-create items list based on VAT group totals + items = [] + total_amount = 0 + for vat_group_entry in vat_group_totals_list: + items.append( + { + "id": f"""Items in VAT Group {vat_group_entry["vat_group"]}""", + "name": f"""Items in VAT Group {vat_group_entry["vat_group"]}""", + "price": flt(vat_group_entry["total_price"], 2), + "qty": 1, + "vatGroup": vat_group_entry["vat_group"], + "discount": 0.0, + } + ) + + total_amount += flt(vat_group_entry["total_price"], 2) + + vfd_cust_id_type = doc.vfd_cust_id_type[:1] or "6" + + payload = { + "serial": total_vfd_setting.serial_id, + "referenceNumber": doc.name, + "customer": { + "name": doc.customer_name, + "idType": vfd_cust_id_type, + "idValue": doc.vfd_cust_id if vfd_cust_id_type != "6" else "", + "mobile": "", + }, + "payments": [{"type": "invoice", "amount": flt(total_amount, 2)}], + "items": items, + } + return payload @frappe.whitelist() -def post_fiscal_receipt( - doc=None, - method="POST", - payload={}, - invoice_id=None, - preview=False -): - """Post fiscal receipt to Total VFD - Parameters - ---------- - doc : object - Python object which is expected to be from Sales Invoice doctype. - method : str - Method name which is calling this function. e.g. POST, validate, on_update, etc. - - Returns - ------- - Nothing - """ - - if not doc and not invoice_id: - frappe.throw(_("Sales Invoice is required!")) - - if not doc and invoice_id: - doc = frappe.get_doc("Sales Invoice", invoice_id) - - doc.vfd_date = doc.vfd_date or nowdate() - doc.vfd_time = format_datetime(str(nowtime()), "HH:mm:ss") - - if not payload: - payload = get_payload(doc) - - # Convert the payload to JSON string format because it is not comming from frontend where it is already in JSON string format - payload = json.dumps(payload) - - vfd_provider_posting_doc = frappe.new_doc("VFD Provider Posting") - - data = send_total_vfd_request( - "sales", - doc.company, - payload, - "POST", - vfd_provider_posting_doc=vfd_provider_posting_doc, - ) - - vfd_provider_posting_doc.sales_invoice = doc.name - vfd_provider_posting_doc.rctnum = doc.vfd_rctvnum - vfd_provider_posting_doc.date = doc.vfd_date - vfd_provider_posting_doc.time = doc.vfd_time - vfd_provider_posting_doc.ackmsg = str(data) - vfd_provider_posting_doc.save(ignore_permissions=True) - - if method == "on_submit": - doc.vfd_status = "Success" - doc.vfd_verification_url = data.get("verificationLink") - doc.vfd_rctvnum = data.get("rctvnum") - doc.vfd_date = data.get("localDate") - doc.vfd_time = data.get("localTime") - doc.vfd_posting_info = vfd_provider_posting_doc.name - doc.save(ignore_permissions=True) - elif method == "POST": - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_rctvnum", data.get("rctvnum") - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_status", "Success" - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_date", data.get("localDate") - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_time", data.get("localTime") - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_posting_info", vfd_provider_posting_doc.name - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_verification_url", data.get("verificationLink"), - ) - frappe.db.commit() - - return {"data": data, "vfd_provider": "TotalVFD", "preview": preview} +def post_fiscal_receipt(doc=None, method="POST", payload=None, invoice_id=None, preview=False): + """Post fiscal receipt to Total VFD + Parameters + ---------- + doc : object + Python object which is expected to be from Sales Invoice doctype. + method : str + Method name which is calling this function. e.g. POST, validate, on_update, etc. + + Returns + ------- + Nothing + """ + + if payload is None: + payload = {} + if not doc and not invoice_id: + frappe.throw(_("Sales Invoice is required!")) + + if not doc and invoice_id: + doc = frappe.get_doc("Sales Invoice", invoice_id) + + doc.vfd_date = doc.vfd_date or nowdate() + doc.vfd_time = format_datetime(str(nowtime()), "HH:mm:ss") + + if not payload: + payload = get_payload(doc) + + # Convert the payload to JSON string format because it is not comming from frontend where it is already in JSON string format + payload = json.dumps(payload) + + vfd_provider_posting_doc = frappe.new_doc("VFD Provider Posting") + + data = send_total_vfd_request( + "sales", + doc.company, + payload, + "POST", + vfd_provider_posting_doc=vfd_provider_posting_doc, + ) + + vfd_provider_posting_doc.sales_invoice = doc.name + vfd_provider_posting_doc.rctnum = doc.vfd_rctvnum + vfd_provider_posting_doc.date = doc.vfd_date + vfd_provider_posting_doc.time = doc.vfd_time + vfd_provider_posting_doc.ackmsg = str(data) + vfd_provider_posting_doc.save(ignore_permissions=True) + + if method == "on_submit": + doc.vfd_status = "Success" + doc.vfd_verification_url = data.get("verificationLink") + doc.vfd_rctvnum = data.get("rctvnum") + doc.vfd_date = data.get("localDate") + doc.vfd_time = data.get("localTime") + doc.vfd_posting_info = vfd_provider_posting_doc.name + doc.save(ignore_permissions=True) + elif method == "POST": + frappe.db.set_value("Sales Invoice", doc.name, "vfd_rctvnum", data.get("rctvnum")) + frappe.db.set_value("Sales Invoice", doc.name, "vfd_status", "Success") + frappe.db.set_value("Sales Invoice", doc.name, "vfd_date", data.get("localDate")) + frappe.db.set_value("Sales Invoice", doc.name, "vfd_time", data.get("localTime")) + frappe.db.set_value("Sales Invoice", doc.name, "vfd_posting_info", vfd_provider_posting_doc.name) + frappe.db.set_value( + "Sales Invoice", + doc.name, + "vfd_verification_url", + data.get("verificationLink"), + ) + frappe.db.commit() + + return {"data": data, "vfd_provider": "TotalVFD", "preview": preview} def send_total_vfd_request( - call_type, - company, - payload=None, - type="GET", - total_vfd_setting=None, - vfd_provider_posting_doc=None, + call_type, + company, + payload=None, + type="GET", + total_vfd_setting=None, + vfd_provider_posting_doc=None, ): - """Send request to Total VFD API - Parameters - ---------- - call_type : str - Type of call to make. e.g. "get_serial_info", "post_fiscal_receipt", "account_info", etc. - company : str - Company to get Total VFD settings from - payload : dict - Payload to send to Total VFD API - type : str - Type of request to make. e.g. "GET", "POST", "PUT", etc. - total_vfd_setting : object - Python object which is expected to be from Total VFD Setting doctype. - vfd_provider_posting_doc : object - Python object which is expected to be from VFD Provider Posting doctype. - - Returns - ------- - data : dict - Dictionary with response from Total VFD API - """ - total_vfd = frappe.get_doc("VFD Provider", "TotalVFD") - if not total_vfd: - frappe.throw(_("Total VFD is not setup!")) - if not total_vfd_setting: - total_vfd_setting = frappe.get_cached_doc("Total VFD Setting", company) - url = ( - total_vfd.base_url - + frappe.get_list( - "VFD Provider Attribute", - filters={"parent": "TotalVFD", "key": call_type}, - fields=["value"], - ignore_permissions=True, - )[0].value - ) - headers = { - "Authorization": "Bearer " + total_vfd_setting.get_password("bearer_token"), - "x-active-business": total_vfd_setting.get_password("x_active_business"), - "Content-Type": "application/json", - } - - data = None - for i in range(3): - try: - res = requests.request( - method=type, - url=url, - data=payload if payload else None, - headers=headers, - timeout=500, - ) - if res.ok or res.status_code == 409: - data = json.loads(res.text) if res.ok else json.loads(res.text)["data"] - frappe.log_error( - title="Send Request OK", - message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}", - ) - else: - data = [] - frappe.log_error( - title="Send Request Error", - message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}\n{payload}", - ) - frappe.throw(f"Error is {res.status_code}: {res.text}") - - if vfd_provider_posting_doc: - vfd_provider_posting_doc.req_headers = ( - json.dumps(headers, ensure_ascii=False) - .replace("\\'", "'") - .replace('\\"', '"') - ) - vfd_provider_posting_doc.req_data = ( - json.dumps(payload, ensure_ascii=False) - .replace("\\'", "'") - .replace('\\"', '"') - ) - vfd_provider_posting_doc.ackcode = data["status"] or 0 - vfd_provider_posting_doc.ackmsg = ( - str(data).replace("\\'", "'").replace('\\"', '"') - ) - - break - except Exception as e: - sleep(3 * i + 1) - if i != 2: - continue - else: - frappe.log_error( - message=frappe.get_traceback(), - title=str(e)[:140] if e else "Send Total VFD Request Error", - ) - frappe.throw(f"Connection failure is {res.text}") - raise e - return data + """Send request to Total VFD API + Parameters + ---------- + call_type : str + Type of call to make. e.g. "get_serial_info", "post_fiscal_receipt", "account_info", etc. + company : str + Company to get Total VFD settings from + payload : dict + Payload to send to Total VFD API + type : str + Type of request to make. e.g. "GET", "POST", "PUT", etc. + total_vfd_setting : object + Python object which is expected to be from Total VFD Setting doctype. + vfd_provider_posting_doc : object + Python object which is expected to be from VFD Provider Posting doctype. + + Returns + ------- + data : dict + Dictionary with response from Total VFD API + """ + total_vfd = frappe.get_doc("VFD Provider", "TotalVFD") + if not total_vfd: + frappe.throw(_("Total VFD is not setup!")) + if not total_vfd_setting: + total_vfd_setting = frappe.get_cached_doc("Total VFD Setting", company) + url = ( + total_vfd.base_url + + frappe.get_list( + "VFD Provider Attribute", + filters={"parent": "TotalVFD", "key": call_type}, + fields=["value"], + ignore_permissions=True, + )[0].value + ) + headers = { + "Authorization": "Bearer " + total_vfd_setting.get_password("bearer_token"), + "x-active-business": total_vfd_setting.get_password("x_active_business"), + "Content-Type": "application/json", + } + + data = None + for i in range(3): + try: + res = requests.request( + method=type, + url=url, + data=payload if payload else None, + headers=headers, + timeout=500, + ) + if res.ok or res.status_code == 409: + data = json.loads(res.text) if res.ok else json.loads(res.text)["data"] + frappe.log_error( + title="Send Request OK", + message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}", + ) + else: + data = [] + frappe.log_error( + title="Send Request Error", + message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}\n{payload}", + ) + frappe.throw(f"Error is {res.status_code}: {res.text}") + + if vfd_provider_posting_doc: + vfd_provider_posting_doc.req_headers = ( + json.dumps(headers, ensure_ascii=False).replace("\\'", "'").replace('\\"', '"') + ) + vfd_provider_posting_doc.req_data = ( + json.dumps(payload, ensure_ascii=False).replace("\\'", "'").replace('\\"', '"') + ) + vfd_provider_posting_doc.ackcode = data["status"] or 0 + vfd_provider_posting_doc.ackmsg = str(data).replace("\\'", "'").replace('\\"', '"') + + break + except Exception as e: + sleep(3 * i + 1) + if i != 2: + continue + else: + frappe.log_error( + message=frappe.get_traceback(), + title=str(e)[:140] if e else "Send Total VFD Request Error", + ) + frappe.throw(f"Connection failure is {res.text}") + raise e + return data diff --git a/csf_tz/vfd_providers/doctype/vfd_provider/vfd_provider.py b/csf_tz/vfd_providers/doctype/vfd_provider/vfd_provider.py index 148bd333..ae00d248 100644 --- a/csf_tz/vfd_providers/doctype/vfd_provider/vfd_provider.py +++ b/csf_tz/vfd_providers/doctype/vfd_provider/vfd_provider.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class VFDProvider(Document): pass diff --git a/csf_tz/vfd_providers/doctype/vfd_provider_attribute/vfd_provider_attribute.py b/csf_tz/vfd_providers/doctype/vfd_provider_attribute/vfd_provider_attribute.py index 640abde9..b5fde86b 100644 --- a/csf_tz/vfd_providers/doctype/vfd_provider_attribute/vfd_provider_attribute.py +++ b/csf_tz/vfd_providers/doctype/vfd_provider_attribute/vfd_provider_attribute.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class VFDProviderAttribute(Document): pass diff --git a/csf_tz/vfd_providers/doctype/vfd_provider_posting/vfd_provider_posting.py b/csf_tz/vfd_providers/doctype/vfd_provider_posting/vfd_provider_posting.py index f9cea9bd..4e8f188d 100644 --- a/csf_tz/vfd_providers/doctype/vfd_provider_posting/vfd_provider_posting.py +++ b/csf_tz/vfd_providers/doctype/vfd_provider_posting/vfd_provider_posting.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class VFDProviderPosting(Document): pass diff --git a/csf_tz/vfd_providers/doctype/vfdplus_settings/vfdplus_settings.py b/csf_tz/vfd_providers/doctype/vfdplus_settings/vfdplus_settings.py index 668bf665..cea1737e 100644 --- a/csf_tz/vfd_providers/doctype/vfdplus_settings/vfdplus_settings.py +++ b/csf_tz/vfd_providers/doctype/vfdplus_settings/vfdplus_settings.py @@ -2,374 +2,351 @@ # For license information, please see license.txt # import frappe -from frappe.model.document import Document +import json from time import sleep -import frappe, json, requests + +import frappe +import requests from frappe import _ -from frappe.utils import nowdate, nowtime, format_datetime, flt +from frappe.model.document import Document +from frappe.utils import flt, format_datetime, nowdate, nowtime + from csf_tz.vfd_providers.utils import get_vat_amount class VFDPlusSettings(Document): - def validate(self): - get_serial_info(self, method="validate") + def validate(self): + get_serial_info(self, method="validate") # Below are the status codes returned by VFDPlus API vfdplus_status_codes = { - "4000": "VFDPLUS-API-KEY not found in header!", - "4001": "Invalid VFDPLUS-API-KEY!", - "4002": "VFDPLUS-API-KEY expired!", - "4003": "VFDPLUS-API-KEY not enabled!", - "4004": "VFDPLUS-API-KEY is deleted!", - "4005": "VFDPLUS-API-KEY does not match any vfd-plus-account!", - "4006": "VFDPLUS-API-KEY does not match any vfd-plus-serial/credential!", - "4007": "Serial/Credential is not active!", - "4008": "TRA Serial Supplied is not activated!", - "4009": "Device Cannot generate receipt,Device is still off", - "4010": "TRA Supplied serial is expired; Please contact Our Customer-Service Team for Renewal Process.", - "4011": "VFDPlus Accpount Expired", - "4012": "Invalid Receipt JSON Format,check missing fields,read all instructions supplied per each error line", - "4013": "Only single receipt can be posted at a time", - "4014": "Discount setting on device is not enabled", - "4015": "Invoice/Receipt for a given serial is already posted", - "2000": "Receipt Posted OK, OK Response", + "4000": "VFDPLUS-API-KEY not found in header!", + "4001": "Invalid VFDPLUS-API-KEY!", + "4002": "VFDPLUS-API-KEY expired!", + "4003": "VFDPLUS-API-KEY not enabled!", + "4004": "VFDPLUS-API-KEY is deleted!", + "4005": "VFDPLUS-API-KEY does not match any vfd-plus-account!", + "4006": "VFDPLUS-API-KEY does not match any vfd-plus-serial/credential!", + "4007": "Serial/Credential is not active!", + "4008": "TRA Serial Supplied is not activated!", + "4009": "Device Cannot generate receipt,Device is still off", + "4010": "TRA Supplied serial is expired; Please contact Our Customer-Service Team for Renewal Process.", + "4011": "VFDPlus Accpount Expired", + "4012": "Invalid Receipt JSON Format,check missing fields,read all instructions supplied per each error line", + "4013": "Only single receipt can be posted at a time", + "4014": "Discount setting on device is not enabled", + "4015": "Invoice/Receipt for a given serial is already posted", + "2000": "Receipt Posted OK, OK Response", } def send_vfdplus_request( - call_type, - company, - payload=None, - type="GET", - vfdplus_settings=None, - vfd_provider_posting_doc=None, + call_type, + company, + payload=None, + type="GET", + vfdplus_settings=None, + vfd_provider_posting_doc=None, ): - """Send request to VFDPlus API - Parameters - ---------- - call_type : str - Type of call to make. e.g. "get_serial_info", "post_fiscal_receipt", "account_info", etc. - company : str - Company to get VFDPlus settings from - payload : dict - Payload to send to VFDPlus API - type : str - Type of request to make. e.g. "GET", "POST", "PUT", etc. - vfdplus_settings : object - Python object which is expected to be from VFDPlus Settings doctype. - vfd_provider_posting_doc : object - Python object which is expected to be from VFD Provider Posting doctype. - - Returns - ------- - data : dict - Dictionary with response from VFDPlus API - """ - vfdplus = frappe.get_cached_doc("VFD Provider", "VFDPlus") - if not vfdplus: - frappe.throw(_("VFDPlus is not setup!")) - if not vfdplus_settings: - vfdplus_settings = frappe.get_doc("VFDPlus Settings", company) - url = ( - vfdplus.base_url - + frappe.get_all( - "VFD Provider Attribute", - filters={"parent": "VFDPlus", "key": call_type}, - fields=["value"], - ignore_permissions=True, - )[0].value - ) - headers = { - "VFDPLUS-API-KEY": vfdplus_settings.vfdplus_api_key, - "Content-Type": "application/json", - } - - data = None - for i in range(3): - try: - res = requests.request( - method=type, - url=url, - data=payload if payload else None, - headers=headers, - timeout=500, - ) - if res.ok: - data = json.loads(res.text) - if data.get("msg_status") != "OK" and not ( - data.get("msg_status") == "WARNING" and data.get("msg_code") == 4015 - ): - frappe.throw( - _(f"Error returned from VFDPlus: {data.get('msg_code')}") - ) - else: - frappe.log_error( - title="Send Request OK", - message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}", - ) - else: - data = [] - frappe.log_error( - title="Send Request Error", - message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}", - ) - frappe.throw(f"Error is {res.text}") - if vfd_provider_posting_doc: - vfd_provider_posting_doc.req_headers = ( - json.dumps(headers, ensure_ascii=False) - .replace("\\'", "'") - .replace('\\"', '"') - ) - vfd_provider_posting_doc.req_data = ( - json.dumps(payload, ensure_ascii=False) - .replace("\\'", "'") - .replace('\\"', '"') - ) - vfd_provider_posting_doc.ackcode = data["msg_code"] - vfd_provider_posting_doc.ackmsg = ( - str(data["msg_data"]).replace("\\'", "'").replace('\\"', '"') - ) - - break - except Exception as e: - sleep(3 * i + 1) - if i != 2: - continue - else: - frappe.log_error( - message=frappe.get_traceback(), - title=str(e)[:140] if e else "Send VFDPLus Request Error", - ) - frappe.throw(f"Connection failure is {res.text}") - raise e - return data + """Send request to VFDPlus API + Parameters + ---------- + call_type : str + Type of call to make. e.g. "get_serial_info", "post_fiscal_receipt", "account_info", etc. + company : str + Company to get VFDPlus settings from + payload : dict + Payload to send to VFDPlus API + type : str + Type of request to make. e.g. "GET", "POST", "PUT", etc. + vfdplus_settings : object + Python object which is expected to be from VFDPlus Settings doctype. + vfd_provider_posting_doc : object + Python object which is expected to be from VFD Provider Posting doctype. + + Returns + ------- + data : dict + Dictionary with response from VFDPlus API + """ + vfdplus = frappe.get_cached_doc("VFD Provider", "VFDPlus") + if not vfdplus: + frappe.throw(_("VFDPlus is not setup!")) + if not vfdplus_settings: + vfdplus_settings = frappe.get_doc("VFDPlus Settings", company) + url = ( + vfdplus.base_url + + frappe.get_all( + "VFD Provider Attribute", + filters={"parent": "VFDPlus", "key": call_type}, + fields=["value"], + ignore_permissions=True, + )[0].value + ) + headers = { + "VFDPLUS-API-KEY": vfdplus_settings.vfdplus_api_key, + "Content-Type": "application/json", + } + + data = None + for i in range(3): + try: + res = requests.request( + method=type, + url=url, + data=payload if payload else None, + headers=headers, + timeout=500, + ) + if res.ok: + data = json.loads(res.text) + if data.get("msg_status") != "OK" and not ( + data.get("msg_status") == "WARNING" and data.get("msg_code") == 4015 + ): + frappe.throw(_(f"Error returned from VFDPlus: {data.get('msg_code')}")) + else: + frappe.log_error( + title="Send Request OK", + message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}", + ) + else: + data = [] + frappe.log_error( + title="Send Request Error", + message=f"Send Request: {url} - Status Code: {res.status_code}\n{res.text}", + ) + frappe.throw(f"Error is {res.text}") + if vfd_provider_posting_doc: + vfd_provider_posting_doc.req_headers = ( + json.dumps(headers, ensure_ascii=False).replace("\\'", "'").replace('\\"', '"') + ) + vfd_provider_posting_doc.req_data = ( + json.dumps(payload, ensure_ascii=False).replace("\\'", "'").replace('\\"', '"') + ) + vfd_provider_posting_doc.ackcode = data["msg_code"] + vfd_provider_posting_doc.ackmsg = ( + str(data["msg_data"]).replace("\\'", "'").replace('\\"', '"') + ) + + break + except Exception as e: + sleep(3 * i + 1) + if i != 2: + continue + else: + frappe.log_error( + message=frappe.get_traceback(), + title=str(e)[:140] if e else "Send VFDPLus Request Error", + ) + frappe.throw(f"Connection failure is {res.text}") + raise e + return data def get_payload(doc): - """Generate payload for VFDPlus API - Parameters - ---------- - doc : object - Python object which is expected to be from Sales Invoice doctype. - - Returns - ------- - payload : dict - Dictionary with payload for VFDPlus API - """ - - cart_items = [] - total_amount = 0 - tax_map = {"1": "A", "2": "B", "3": "C", "4": "D", "5": "E"} - - for item in doc.items: - vat_rate_id = frappe.get_cached_value( - "Item Tax Template", item.item_tax_template, "vfd_taxcode" - )[:1] - - vat_rate_code = tax_map[vat_rate_id] - - sp = get_vat_amount(item, vat_rate_code, precision=2) - - cart_items.append( - { - "vat_rate_code": vat_rate_code, - "vat_rate_id": vat_rate_id, - "item_name": item.item_code, - "item_barcode": "-1", - "item_qty": item.qty, - "usp": flt(sp / item.qty, 2), - "sp": sp, - "unit_discount_perc": 0.0, - "unit_discount_amt": 0.0, - "total_item_discount": 0.0, - } - ) - total_amount += sp - - vfdplus_settings = frappe.get_doc("VFDPlus Settings", doc.company) - - payload = { - "credential_code": vfdplus_settings.serial_code, - "branch_id": "", - "depart_id": "", - "trans_no": doc.name, - "idate": str(doc.vfd_date or nowdate()), - "itime": format_datetime(str(doc.vfd_time or nowtime()), "HH:mm:ss"), - "customer_info": { - "cust_name": doc.customer_name, - "cust_id_type": doc.vfd_cust_id_type or "6", - "cust_id": doc.vfd_cust_id or "NIL", - "cust_phone": "", - "cust_vrn": "", - "cust_addr": "", - "id_for": "", - }, - "payment_methods": [ - { - "pmt_type": "INVOICE", - "pmt_amount": flt(total_amount, 2) - } - ], - "cart_totals": { - "item_counts": len(doc.items), - "total_amount": flt(total_amount, 2), - "total_amount_exclude_discount": flt(total_amount, 2), - "discount": 0.0, - }, - "cart_items": cart_items, - "user_info": { - "user_id": "1", - "username": doc.modified_by.split("@")[0], - "till_id": "1", - }, - } - - return payload + """Generate payload for VFDPlus API + Parameters + ---------- + doc : object + Python object which is expected to be from Sales Invoice doctype. + + Returns + ------- + payload : dict + Dictionary with payload for VFDPlus API + """ + + cart_items = [] + total_amount = 0 + tax_map = {"1": "A", "2": "B", "3": "C", "4": "D", "5": "E"} + + for item in doc.items: + vat_rate_id = frappe.get_cached_value("Item Tax Template", item.item_tax_template, "vfd_taxcode")[:1] + + vat_rate_code = tax_map[vat_rate_id] + + sp = get_vat_amount(item, vat_rate_code, precision=2) + + cart_items.append( + { + "vat_rate_code": vat_rate_code, + "vat_rate_id": vat_rate_id, + "item_name": item.item_code, + "item_barcode": "-1", + "item_qty": item.qty, + "usp": flt(sp / item.qty, 2), + "sp": sp, + "unit_discount_perc": 0.0, + "unit_discount_amt": 0.0, + "total_item_discount": 0.0, + } + ) + total_amount += sp + + vfdplus_settings = frappe.get_doc("VFDPlus Settings", doc.company) + + payload = { + "credential_code": vfdplus_settings.serial_code, + "branch_id": "", + "depart_id": "", + "trans_no": doc.name, + "idate": str(doc.vfd_date or nowdate()), + "itime": format_datetime(str(doc.vfd_time or nowtime()), "HH:mm:ss"), + "customer_info": { + "cust_name": doc.customer_name, + "cust_id_type": doc.vfd_cust_id_type or "6", + "cust_id": doc.vfd_cust_id or "NIL", + "cust_phone": "", + "cust_vrn": "", + "cust_addr": "", + "id_for": "", + }, + "payment_methods": [{"pmt_type": "INVOICE", "pmt_amount": flt(total_amount, 2)}], + "cart_totals": { + "item_counts": len(doc.items), + "total_amount": flt(total_amount, 2), + "total_amount_exclude_discount": flt(total_amount, 2), + "discount": 0.0, + }, + "cart_items": cart_items, + "user_info": { + "user_id": "1", + "username": doc.modified_by.split("@")[0], + "till_id": "1", + }, + } + + return payload @frappe.whitelist() -def post_fiscal_receipt( - doc=None, - method="POST", - payload={}, - invoice_id=None, - preview=False -): - """Post fiscal receipt to VFDPlus - Parameters - ---------- - doc : object - Python object which is expected to be from VFDPlus Settings doctype. - method : str - Method name which is calling this function. e.g. POST, validate, on_update, etc. - - Returns - ------- - data : dict - Dictionary with response from VFDPlus API - """ - - if not doc and not invoice_id: - frappe.throw(_("Sales Invoice is required!")) - - if not doc and invoice_id: - doc = frappe.get_doc("Sales Invoice", invoice_id) - - doc.vfd_date = doc.vfd_date or nowdate() - doc.vfd_time = format_datetime(str(nowtime()), "HH:mm:ss") - - if not payload: - payload = get_payload(doc) - - # Convert the payload to JSON string format because it is not comming from frontend where it is already in JSON string format - payload = json.dumps(payload) - - vfd_provider_posting_doc = frappe.new_doc("VFD Provider Posting") - - data = send_vfdplus_request( - "post_fiscal_receipt", - doc.company, - payload, - "POST", - vfd_provider_posting_doc=vfd_provider_posting_doc, - ) - - vfd_provider_posting_doc.sales_invoice = doc.name - vfd_provider_posting_doc.rctnum = doc.vfd_rctvnum - vfd_provider_posting_doc.date = doc.vfd_date - vfd_provider_posting_doc.time = doc.vfd_time - vfd_provider_posting_doc.save(ignore_permissions=True) - - rctvnum = data["msg_data"].get("rctvnum") - verification_url = f"https://verify.tra.go.tz/{rctvnum}_{str(data['msg_data'].get('itime')).replace(':','')}" - - if method == "on_submit": - doc.vfd_status = "Success" - doc.vfd_rctvnum = rctvnum - doc.vfd_date = data["msg_data"].get("idate") - doc.vfd_time = data["msg_data"].get("itime") - doc.vfd_verification_url = verification_url - doc.vfd_posting_info = vfd_provider_posting_doc.name - - doc.save() - - elif method == "POST": - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_rctvnum", rctvnum - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_status", "Success" - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_date", data["msg_data"].get("idate") - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_time", data["msg_data"].get("itime") - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_posting_info", vfd_provider_posting_doc.name - ) - frappe.db.set_value( - "Sales Invoice", doc.name, "vfd_verification_url", verification_url - ) - frappe.db.commit() - - return {"data": data, "vfd_provider": "VFDPlus", "preview": preview} +def post_fiscal_receipt(doc=None, method="POST", payload=None, invoice_id=None, preview=False): + """Post fiscal receipt to VFDPlus + Parameters + ---------- + doc : object + Python object which is expected to be from VFDPlus Settings doctype. + method : str + Method name which is calling this function. e.g. POST, validate, on_update, etc. + + Returns + ------- + data : dict + Dictionary with response from VFDPlus API + """ + + if payload is None: + payload = {} + if not doc and not invoice_id: + frappe.throw(_("Sales Invoice is required!")) + + if not doc and invoice_id: + doc = frappe.get_doc("Sales Invoice", invoice_id) + + doc.vfd_date = doc.vfd_date or nowdate() + doc.vfd_time = format_datetime(str(nowtime()), "HH:mm:ss") + + if not payload: + payload = get_payload(doc) + + # Convert the payload to JSON string format because it is not comming from frontend where it is already in JSON string format + payload = json.dumps(payload) + + vfd_provider_posting_doc = frappe.new_doc("VFD Provider Posting") + + data = send_vfdplus_request( + "post_fiscal_receipt", + doc.company, + payload, + "POST", + vfd_provider_posting_doc=vfd_provider_posting_doc, + ) + + vfd_provider_posting_doc.sales_invoice = doc.name + vfd_provider_posting_doc.rctnum = doc.vfd_rctvnum + vfd_provider_posting_doc.date = doc.vfd_date + vfd_provider_posting_doc.time = doc.vfd_time + vfd_provider_posting_doc.save(ignore_permissions=True) + + rctvnum = data["msg_data"].get("rctvnum") + verification_url = ( + f"https://verify.tra.go.tz/{rctvnum}_{str(data['msg_data'].get('itime')).replace(':', '')}" + ) + + if method == "on_submit": + doc.vfd_status = "Success" + doc.vfd_rctvnum = rctvnum + doc.vfd_date = data["msg_data"].get("idate") + doc.vfd_time = data["msg_data"].get("itime") + doc.vfd_verification_url = verification_url + doc.vfd_posting_info = vfd_provider_posting_doc.name + + doc.save() + + elif method == "POST": + frappe.db.set_value("Sales Invoice", doc.name, "vfd_rctvnum", rctvnum) + frappe.db.set_value("Sales Invoice", doc.name, "vfd_status", "Success") + frappe.db.set_value("Sales Invoice", doc.name, "vfd_date", data["msg_data"].get("idate")) + frappe.db.set_value("Sales Invoice", doc.name, "vfd_time", data["msg_data"].get("itime")) + frappe.db.set_value("Sales Invoice", doc.name, "vfd_posting_info", vfd_provider_posting_doc.name) + frappe.db.set_value("Sales Invoice", doc.name, "vfd_verification_url", verification_url) + frappe.db.commit() + + return {"data": data, "vfd_provider": "VFDPlus", "preview": preview} def get_serial_info(doc, method): - """Get serial info from VFDPlus - Parameters - ---------- - doc : object - Python object which is expected to be from VFDPlus Settings doctype. - method : str - Method name which is calling this function. e.g. validate, on_update, etc. - - Returns - ------- - Nothing - """ - data = send_vfdplus_request( - call_type="serial_info", - company=doc.company, - type="GET", - vfdplus_settings=doc, - vfd_provider_posting_doc=None, - ) - if data: - doc.response = str(data["msg_data"]) - for key, value in data["msg_data"].items(): - try: - setattr(doc, key, value) - except Exception as e: - frappe.log_error( - message=frappe.get_traceback(), - title="Error in set attribute for VFDPlus", - ) - raise e - if method != "validate": - doc.save(ignore_permissions=True) + """Get serial info from VFDPlus + Parameters + ---------- + doc : object + Python object which is expected to be from VFDPlus Settings doctype. + method : str + Method name which is calling this function. e.g. validate, on_update, etc. + + Returns + ------- + Nothing + """ + data = send_vfdplus_request( + call_type="serial_info", + company=doc.company, + type="GET", + vfdplus_settings=doc, + vfd_provider_posting_doc=None, + ) + if data: + doc.response = str(data["msg_data"]) + for key, value in data["msg_data"].items(): + try: + setattr(doc, key, value) + except Exception as e: + frappe.log_error( + message=frappe.get_traceback(), + title="Error in set attribute for VFDPlus", + ) + raise e + if method != "validate": + doc.save(ignore_permissions=True) @frappe.whitelist() def get_account_info(company): - """Get serial info from VFDPlus - Parameters - ---------- - company : str - String having Company name - - Returns - ------- - data : dict - Dictionary of account info - """ - # TODO - data = send_vfdplus_request(call_type="account_info", company=company, type="GET") - if data: - return data - else: - frappe.throw(_(f"No data returned from VFDPlus for company: {company}")) + """Get serial info from VFDPlus + Parameters + ---------- + company : str + String having Company name + + Returns + ------- + data : dict + Dictionary of account info + """ + # TODO + data = send_vfdplus_request(call_type="account_info", company=company, type="GET") + if data: + return data + else: + frappe.throw(_(f"No data returned from VFDPlus for company: {company}")) diff --git a/csf_tz/vfd_providers/utils.py b/csf_tz/vfd_providers/utils.py index 3d452508..3f91c44d 100644 --- a/csf_tz/vfd_providers/utils.py +++ b/csf_tz/vfd_providers/utils.py @@ -1,28 +1,26 @@ -import frappe from frappe.utils import flt + def get_vat_amount(item, vat_group, precision=0): - vat_amount = 0 + vat_amount = 0 + + if str(vat_group) in ["A", "1"]: + if (item.base_net_amount + item.get("distributed_discount_amount", 0)) == item.base_amount: + # both base amounts are same if the amount is exclusive of VAT + amount = item.base_amount * 1.18 + if precision > 0: + vat_amount = flt(amount, precision) + else: + vat_amount = amount + else: + if precision > 0: + vat_amount = flt(item.base_amount, precision=2) + else: + vat_amount = item.base_amount + else: + if precision > 0: + vat_amount = flt(item.base_amount, precision=2) + else: + vat_amount = item.base_amount - if str(vat_group) in ["A", "1"]: - if ( - (item.base_net_amount + item.get("distributed_discount_amount", 0)) == item.base_amount - ): - # both base amounts are same if the amount is exclusive of VAT - amount = item.base_amount * 1.18 - if precision > 0: - vat_amount = flt(amount, precision) - else: - vat_amount = amount - else: - if precision > 0: - vat_amount = flt(item.base_amount, precision=2) - else: - vat_amount = item.base_amount - else: - if precision > 0: - vat_amount = flt(item.base_amount, precision=2) - else: - vat_amount = item.base_amount - - return vat_amount + return vat_amount diff --git a/csf_tz/vfd_settings/doctype/company_vfd_provider/company_vfd_provider.py b/csf_tz/vfd_settings/doctype/company_vfd_provider/company_vfd_provider.py index 43f823d9..3e99a052 100644 --- a/csf_tz/vfd_settings/doctype/company_vfd_provider/company_vfd_provider.py +++ b/csf_tz/vfd_settings/doctype/company_vfd_provider/company_vfd_provider.py @@ -4,5 +4,6 @@ # import frappe from frappe.model.document import Document + class CompanyVFDProvider(Document): pass diff --git a/csf_tz/vfd_support/__init__.py b/csf_tz/vfd_support/__init__.py index 8b137891..e69de29b 100644 --- a/csf_tz/vfd_support/__init__.py +++ b/csf_tz/vfd_support/__init__.py @@ -1 +0,0 @@ - diff --git a/csf_tz/vfd_support/customer.js b/csf_tz/vfd_support/customer.js index ed956a8d..9b20c488 100644 --- a/csf_tz/vfd_support/customer.js +++ b/csf_tz/vfd_support/customer.js @@ -26,4 +26,4 @@ frappe.ui.form.on("Customer", { } }); }, -}) \ No newline at end of file +}) diff --git a/csf_tz/vfd_support/sales_invoice.js b/csf_tz/vfd_support/sales_invoice.js index 6557cada..2a906f85 100644 --- a/csf_tz/vfd_support/sales_invoice.js +++ b/csf_tz/vfd_support/sales_invoice.js @@ -172,7 +172,7 @@ function show_vfd_preview_dialog(frm, payload, vfd_provider) { if (taxRate) { const netLineTotal = flt(lineTotal / (1 + taxRate)); taxAmount += lineTotal - netLineTotal; - } + } }); let totalExcl = totalIncl - taxAmount; @@ -294,7 +294,7 @@ function show_vfd_preview_dialog(frm, payload, vfd_provider) { `; - + let method = '' if (vfd_provider === "VFDPlus") { method = "csf_tz.vfd_providers.doctype.vfdplus_settings.vfdplus_settings.post_fiscal_receipt" @@ -361,4 +361,3 @@ function show_vfd_preview_dialog(frm, payload, vfd_provider) { //
CUSTOMER
//
INVOICE
- diff --git a/csf_tz/vfd_support/sales_invoice.py b/csf_tz/vfd_support/sales_invoice.py index d58f4522..d3015ca8 100644 --- a/csf_tz/vfd_support/sales_invoice.py +++ b/csf_tz/vfd_support/sales_invoice.py @@ -1,238 +1,185 @@ -# -*- coding: utf-8 -*- # Copyright (c) 2020, Aakvatech and contributors # For license information, please see license.txt -from __future__ import unicode_literals -import frappe -import erpnext -from frappe import _ - -from frappe.utils import flt, nowdate, nowtime, format_datetime -from frappe.utils.background_jobs import enqueue import json import re +import erpnext +import frappe +from frappe import _ +from frappe.utils import flt + def vfd_validation(doc, method): - if doc.is_return or doc.is_not_vfd_invoice: - return - if doc.base_net_total == 0: - frappe.throw(_("Base net amount is zero. Correct the invoice and retry.")) - - vfdplus_settings = None - if frappe.db.exists("VFDPlus Settings", {"name": doc.company}): - vfdplus_settings = frappe.get_doc("VFDPlus Settings", doc.company) - - tax_data = get_itemised_tax_breakup_html(doc) - if not tax_data: - frappe.throw(_("Taxes not set correctly")) - - for item in doc.items: - if not item.item_code: - frappe.throw(_("Item Code not set for item {0}".format(item.item_name))) - if not item.item_tax_template: - item_tax_template = frappe.get_value( - "Item", item.item_code, "default_tax_template" - ) - if not item_tax_template: - frappe.throw( - _("Item Taxes Template not set for item {0}".format(item.item_code)) - ) - else: - item.item_tax_template = item_tax_template - item_taxcode = get_item_taxcode( - item.item_tax_template, item.item_code, doc.name - ) - - with_tax = 0 - other_tax = 0 - - for tax_name, tax_value in tax_data.get(item.item_code).items(): - if tax_value.get("tax_rate") == 18: - with_tax += 1 - else: - other_tax += tax_value.get("tax_amount") - - if other_tax: - frappe.throw( - _( - "Taxes not set correctly for Other Tax item {0}".format( - item.item_code - ) - ) - ) - if item_taxcode == 1 and with_tax != 1: - if vfdplus_settings and vfdplus_settings.vat_enabled: - frappe.msgprint( - _( - "Taxes is not set to 18pct for Standard Rate item {0}".format( - item.item_code - ) - ) - ) - else: - frappe.throw( - _( - "Taxes not set correctly for Standard Rate item {0}".format( - item.item_code - ) - ) - ) - elif item_taxcode != 1 and with_tax != 0: - frappe.throw( - _( - "Taxes not set correctly for Non Standard Rate item {0}".format( - item.item_code - ) - ) - ) - - if not doc.vfd_cust_id_type or not doc.vfd_cust_id: - data = get_customer_id_info(doc.customer) - if data.get("cust_id"): - doc.vfd_cust_id = data.get("cust_id") - if data.get("cust_id_type"): - doc.vfd_cust_id_type = data.get("cust_id_type") + if doc.is_return or doc.is_not_vfd_invoice: + return + if doc.base_net_total == 0: + frappe.throw(_("Base net amount is zero. Correct the invoice and retry.")) + + vfdplus_settings = None + if frappe.db.exists("VFDPlus Settings", {"name": doc.company}): + vfdplus_settings = frappe.get_doc("VFDPlus Settings", doc.company) + + tax_data = get_itemised_tax_breakup_html(doc) + if not tax_data: + frappe.throw(_("Taxes not set correctly")) + + for item in doc.items: + if not item.item_code: + frappe.throw(_(f"Item Code not set for item {item.item_name}")) + if not item.item_tax_template: + item_tax_template = frappe.get_value("Item", item.item_code, "default_tax_template") + if not item_tax_template: + frappe.throw(_(f"Item Taxes Template not set for item {item.item_code}")) + else: + item.item_tax_template = item_tax_template + item_taxcode = get_item_taxcode(item.item_tax_template, item.item_code, doc.name) + + with_tax = 0 + other_tax = 0 + + for _tax_name, tax_value in tax_data.get(item.item_code).items(): + if tax_value.get("tax_rate") == 18: + with_tax += 1 + else: + other_tax += tax_value.get("tax_amount") + + if other_tax: + frappe.throw(_(f"Taxes not set correctly for Other Tax item {item.item_code}")) + if item_taxcode == 1 and with_tax != 1: + if vfdplus_settings and vfdplus_settings.vat_enabled: + frappe.msgprint(_(f"Taxes is not set to 18pct for Standard Rate item {item.item_code}")) + else: + frappe.throw(_(f"Taxes not set correctly for Standard Rate item {item.item_code}")) + elif item_taxcode != 1 and with_tax != 0: + frappe.throw(_(f"Taxes not set correctly for Non Standard Rate item {item.item_code}")) + + if not doc.vfd_cust_id_type or not doc.vfd_cust_id: + data = get_customer_id_info(doc.customer) + if data.get("cust_id"): + doc.vfd_cust_id = data.get("cust_id") + if data.get("cust_id_type"): + doc.vfd_cust_id_type = data.get("cust_id_type") def get_customer_id_info(customer): - data = {} - cust_id, cust_id_type, mobile_no = frappe.get_value( - "Customer", customer, ["vfd_cust_id", "vfd_cust_id_type", "mobile_no"] - ) - if not cust_id: - data["cust_id"] = "" - data["cust_id_type"] = 6 - elif cust_id and not cust_id_type: - frappe.throw( - _("Please make sure to set VFD Customer ID Type in Customer Master") - ) - else: - data["cust_id"] = cust_id - data["cust_id_type"] = int(cust_id_type[:1]) - - data["mobile_no"] = remove_all_except_numbers(mobile_no) or "" - return data + data = {} + cust_id, cust_id_type, mobile_no = frappe.get_value( + "Customer", customer, ["vfd_cust_id", "vfd_cust_id_type", "mobile_no"] + ) + if not cust_id: + data["cust_id"] = "" + data["cust_id_type"] = 6 + elif cust_id and not cust_id_type: + frappe.throw(_("Please make sure to set VFD Customer ID Type in Customer Master")) + else: + data["cust_id"] = cust_id + data["cust_id_type"] = int(cust_id_type[:1]) + + data["mobile_no"] = remove_all_except_numbers(mobile_no) or "" + return data def get_item_taxcode(item_tax_template=None, item_code=None, invoice_name=None): - if not item_tax_template: - if item_code and invoice_name: - frappe.throw( - _( - "Item Taxes Template not set for item {0} in invoice {1}".format( - item_code, invoice_name - ) - ) - ) - elif item_code: - frappe.throw( - _("Item Taxes Template not set for item {0}".format(item_code)) - ) - else: - frappe.throw(_("Item Taxes Template not set")) - - taxcode = None - if item_tax_template: - vfd_taxcode = frappe.get_value( - "Item Tax Template", item_tax_template, "vfd_taxcode" - ) - if vfd_taxcode: - taxcode = int(vfd_taxcode[:1]) - else: - frappe.throw(_("VFD Tax Code not setup in {0}".format(item_tax_template))) - return taxcode + if not item_tax_template: + if item_code and invoice_name: + frappe.throw(_(f"Item Taxes Template not set for item {item_code} in invoice {invoice_name}")) + elif item_code: + frappe.throw(_(f"Item Taxes Template not set for item {item_code}")) + else: + frappe.throw(_("Item Taxes Template not set")) + + taxcode = None + if item_tax_template: + vfd_taxcode = frappe.get_value("Item Tax Template", item_tax_template, "vfd_taxcode") + if vfd_taxcode: + taxcode = int(vfd_taxcode[:1]) + else: + frappe.throw(_(f"VFD Tax Code not setup in {item_tax_template}")) + return taxcode def validate_cancel(doc, method): - if doc.vfd_rctvnum: - frappe.throw( - _( - "This invoice cannot be canceled as it is already sent to TRA. Please cancel it on TRA portal during VAT Filing." - ) - ) + if doc.vfd_rctvnum: + frappe.throw( + _( + "This invoice cannot be canceled as it is already sent to TRA. Please cancel it on TRA portal during VAT Filing." + ) + ) def get_itemised_tax_breakup_html(doc): - if not doc.taxes: - return + if not doc.taxes: + return - itemised_tax = get_itemised_tax_breakup_data(doc) - get_rounded_tax_amount(itemised_tax, doc.precision("tax_amount", "taxes")) - return itemised_tax + itemised_tax = get_itemised_tax_breakup_data(doc) + get_rounded_tax_amount(itemised_tax, doc.precision("tax_amount", "taxes")) + return itemised_tax def get_item_inclusive_amount(item): - if item.base_net_amount == item.base_amount: - # this is basic rate included - item_tax_rate = json.loads(item.item_tax_rate) - if not item_tax_rate or item_tax_rate == {}: - return item.base_amount - else: - for key, value in item_tax_rate.items(): - if not value or value == 0.00: - return flt(item.base_amount, 2) - return flt( - item.base_amount * (1 + (value / 100)), 2 - ) # 118% for 18% VAT - else: - return flt(item.base_amount, 2) + if item.base_net_amount == item.base_amount: + # this is basic rate included + item_tax_rate = json.loads(item.item_tax_rate) + if not item_tax_rate or item_tax_rate == {}: + return item.base_amount + else: + for _key, value in item_tax_rate.items(): + if not value or value == 0.00: + return flt(item.base_amount, 2) + return flt(item.base_amount * (1 + (value / 100)), 2) # 118% for 18% VAT + else: + return flt(item.base_amount, 2) @erpnext.allow_regional def get_itemised_tax_breakup_data(doc): - itemised_tax = get_itemised_tax(doc.taxes) - return itemised_tax + itemised_tax = get_itemised_tax(doc.taxes) + return itemised_tax def get_itemised_tax(taxes, with_tax_account=False): - itemised_tax = {} - for tax in taxes: - if getattr(tax, "category", None) and tax.category == "Valuation": - continue + itemised_tax = {} + for tax in taxes: + if getattr(tax, "category", None) and tax.category == "Valuation": + continue - item_tax_map = ( - json.loads(tax.item_wise_tax_detail) if tax.item_wise_tax_detail else {} - ) - if item_tax_map: - for item_code, tax_data in item_tax_map.items(): - itemised_tax.setdefault(item_code, frappe._dict()) + item_tax_map = json.loads(tax.item_wise_tax_detail) if tax.item_wise_tax_detail else {} + if item_tax_map: + for item_code, tax_data in item_tax_map.items(): + itemised_tax.setdefault(item_code, frappe._dict()) - tax_rate = 0.0 - tax_amount = 0.0 + tax_rate = 0.0 + tax_amount = 0.0 - if isinstance(tax_data, list): - tax_rate = flt(tax_data[0]) - tax_amount = flt(tax_data[1]) - else: - tax_rate = flt(tax_data) + if isinstance(tax_data, list): + tax_rate = flt(tax_data[0]) + tax_amount = flt(tax_data[1]) + else: + tax_rate = flt(tax_data) - itemised_tax[item_code][tax.description] = frappe._dict( - dict(tax_rate=tax_rate, tax_amount=tax_amount) - ) + itemised_tax[item_code][tax.description] = frappe._dict( + dict(tax_rate=tax_rate, tax_amount=tax_amount) + ) - if with_tax_account: - itemised_tax[item_code][ - tax.description - ].tax_account = tax.account_head + if with_tax_account: + itemised_tax[item_code][tax.description].tax_account = tax.account_head - return itemised_tax + return itemised_tax def get_rounded_tax_amount(itemised_tax, precision): - # Rounding based on tax_amount precision - for taxes in itemised_tax.values(): - for tax_account in taxes: - taxes[tax_account]["tax_amount"] = flt( - taxes[tax_account]["tax_amount"], precision - ) + # Rounding based on tax_amount precision + for taxes in itemised_tax.values(): + for tax_account in taxes: + taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision) def remove_special_characters(text): - return re.sub("[^A-Za-z0-9 ]+", "", text) + return re.sub("[^A-Za-z0-9 ]+", "", text) def remove_all_except_numbers(text=None): - if not text: - return "" - return re.sub("[^0-9]+", "", text) + if not text: + return "" + return re.sub("[^0-9]+", "", text) diff --git a/csf_tz/vfd_support/utils.py b/csf_tz/vfd_support/utils.py index f1e302d7..d881c70d 100644 --- a/csf_tz/vfd_support/utils.py +++ b/csf_tz/vfd_support/utils.py @@ -1,160 +1,157 @@ -import click import frappe from frappe import _ -from csf_tz.vfd_providers.doctype.vfdplus_settings.vfdplus_settings import ( - get_payload as get_vfdplus_payload, - post_fiscal_receipt as vfdplus_post_fiscal_receipt + +from csf_tz.vfd_providers.doctype.simplify_vfd_settings.simplify_vfd_settings import ( + get_payload as get_simplify_payload, +) +from csf_tz.vfd_providers.doctype.simplify_vfd_settings.simplify_vfd_settings import ( + post_fiscal_receipt as simplify_vfd_post_fiscal_receipt, ) from csf_tz.vfd_providers.doctype.total_vfd_setting.total_vfd_setting import ( - get_payload as get_total_vfd_payload, - post_fiscal_receipt as total_vfd_post_fiscal_receipt + get_payload as get_total_vfd_payload, ) -from csf_tz.vfd_providers.doctype.simplify_vfd_settings.simplify_vfd_settings import ( - get_payload as get_simplify_payload, - post_fiscal_receipt as simplify_vfd_post_fiscal_receipt +from csf_tz.vfd_providers.doctype.total_vfd_setting.total_vfd_setting import ( + post_fiscal_receipt as total_vfd_post_fiscal_receipt, +) +from csf_tz.vfd_providers.doctype.vfdplus_settings.vfdplus_settings import get_payload as get_vfdplus_payload +from csf_tz.vfd_providers.doctype.vfdplus_settings.vfdplus_settings import ( + post_fiscal_receipt as vfdplus_post_fiscal_receipt, ) @frappe.whitelist() def generate_tra_vfd(docname, sinv_doc=None, method="POST", caller="Frontend"): - if not sinv_doc: - sinv_doc = frappe.get_doc("Sales Invoice", docname) - - if sinv_doc.is_not_vfd_invoice or sinv_doc.vfd_status == "Success" or sinv_doc.is_return == 1: - return - - comp_vfd_provider = frappe.get_cached_doc("Company VFD Provider", sinv_doc.company) - if not comp_vfd_provider: - return - - vfd_provider = frappe.get_cached_doc("VFD Provider", comp_vfd_provider.vfd_provider) - if not vfd_provider: - return - - vfd_provider_settings = vfd_provider.vfd_provider_settings - if not vfd_provider_settings: - return - - settings_info = frappe.get_cached_value( - vfd_provider_settings, - sinv_doc.company, - ["enable_vfd_preview", "vfd_start_date"], - as_dict=True - ) - - if not settings_info.get("vfd_start_date"): - frappe.throw(_(f"Please set VFD Start Date in {vfd_provider_settings}")) - - if frappe.utils.getdate(sinv_doc.posting_date) < settings_info.get("vfd_start_date"): - frappe.throw( - _( - f"VFD cannot be generated for Invoice before {settings_info.get('vfd_start_date')} \ + if not sinv_doc: + sinv_doc = frappe.get_doc("Sales Invoice", docname) + + if sinv_doc.is_not_vfd_invoice or sinv_doc.vfd_status == "Success" or sinv_doc.is_return == 1: + return + + comp_vfd_provider = frappe.get_cached_doc("Company VFD Provider", sinv_doc.company) + if not comp_vfd_provider: + return + + vfd_provider = frappe.get_cached_doc("VFD Provider", comp_vfd_provider.vfd_provider) + if not vfd_provider: + return + + vfd_provider_settings = vfd_provider.vfd_provider_settings + if not vfd_provider_settings: + return + + settings_info = frappe.get_cached_value( + vfd_provider_settings, sinv_doc.company, ["enable_vfd_preview", "vfd_start_date"], as_dict=True + ) + + if not settings_info.get("vfd_start_date"): + frappe.throw(_(f"Please set VFD Start Date in {vfd_provider_settings}")) + + if frappe.utils.getdate(sinv_doc.posting_date) < settings_info.get("vfd_start_date"): + frappe.throw( + _( + f"VFD cannot be generated for Invoice before {settings_info.get('vfd_start_date')} \ as per the settings in {vfd_provider_settings}" - ) - ) - - if settings_info.get("enable_vfd_preview") == 1 and caller == "Frontend": - payload = {} - if vfd_provider.name == "VFDPlus": - payload = get_vfdplus_payload(sinv_doc) - - elif vfd_provider.name == "TotalVFD": - payload = get_total_vfd_payload(sinv_doc) - - elif vfd_provider.name == "SimplifyVFD": - payload = get_simplify_payload(sinv_doc) - else: - frappe.throw(_("VFD Provider not supported")) - - return {"data": payload, "vfd_provider": vfd_provider.name, "preview": True} - - else: - if vfd_provider.name == "VFDPlus": - return vfdplus_post_fiscal_receipt(doc=sinv_doc, method=method) - - elif vfd_provider.name == "TotalVFD": - return total_vfd_post_fiscal_receipt(doc=sinv_doc, method=method) - - elif vfd_provider.name == "SimplifyVFD": - return simplify_vfd_post_fiscal_receipt(doc=sinv_doc, method=method) - else: - frappe.throw(_("VFD Provider not supported")) + ) + ) + + if settings_info.get("enable_vfd_preview") == 1 and caller == "Frontend": + payload = {} + if vfd_provider.name == "VFDPlus": + payload = get_vfdplus_payload(sinv_doc) + + elif vfd_provider.name == "TotalVFD": + payload = get_total_vfd_payload(sinv_doc) + + elif vfd_provider.name == "SimplifyVFD": + payload = get_simplify_payload(sinv_doc) + else: + frappe.throw(_("VFD Provider not supported")) + + return {"data": payload, "vfd_provider": vfd_provider.name, "preview": True} + + else: + if vfd_provider.name == "VFDPlus": + return vfdplus_post_fiscal_receipt(doc=sinv_doc, method=method) + + elif vfd_provider.name == "TotalVFD": + return total_vfd_post_fiscal_receipt(doc=sinv_doc, method=method) + + elif vfd_provider.name == "SimplifyVFD": + return simplify_vfd_post_fiscal_receipt(doc=sinv_doc, method=method) + else: + frappe.throw(_("VFD Provider not supported")) def autogenerate_vfd(doc, method): - if doc.is_not_vfd_invoice or doc.vfd_status == "Success" or doc.is_return == 1: - return - - if doc.is_auto_generate_vfd and doc.docstatus == 1: - generate_tra_vfd(docname=doc.name, sinv_doc=doc, method=method, caller="Scheduler") + if doc.is_not_vfd_invoice or doc.vfd_status == "Success" or doc.is_return == 1: + return + + if doc.is_auto_generate_vfd and doc.docstatus == 1: + generate_tra_vfd(docname=doc.name, sinv_doc=doc, method=method, caller="Scheduler") def posting_all_vfd_invoices(): - if frappe.local.flags.vfd_posting: - frappe.log_error(_("VFD Posting Flag found", "VFD Posting Flag found")) - return - - frappe.local.flags.vfd_posting = True - - companies = frappe.get_all("Company", pluck="name") - for company in companies: - comp_vfd_provider = None - if frappe.db.exists("Company VFD Provider", company): - comp_vfd_provider = frappe.get_cached_doc("Company VFD Provider", company) - else: - continue - - vfd_provider = frappe.get_cached_doc("VFD Provider", comp_vfd_provider.vfd_provider) - - vfd_provider_settings = vfd_provider.vfd_provider_settings - if not vfd_provider_settings: - continue - - vfd_start_date = frappe.get_cached_value( - vfd_provider_settings, - company, - "vfd_start_date" - ) - - if not vfd_start_date: - continue - - invoices = frappe.db.get_all( - "Sales Invoice", - filters={ - "docstatus": 1, - "company": company, - "is_not_vfd_invoice": 0, - "is_return": 0, - "vfd_status": ["not in", ["Not Sent", "Success"]], - "posting_date": [">=", vfd_start_date] - } - ) - - for invoice in invoices: - doc = frappe.get_doc("Sales Invoice", invoice.name) - - if vfd_provider.name == "VFDPlus": - vfdplus_post_fiscal_receipt(doc=doc, method="POST") - - elif vfd_provider.name == "TotalVFD": - total_vfd_post_fiscal_receipt(doc=doc, method="POST") - - elif vfd_provider.name == "SimplifyVFD": - simplify_vfd_post_fiscal_receipt(doc=doc, method="POST") - - else: - continue - - frappe.local.flags.vfd_posting = False + if frappe.local.flags.vfd_posting: + frappe.log_error(_("VFD Posting Flag found", "VFD Posting Flag found")) + return + + frappe.local.flags.vfd_posting = True + + companies = frappe.get_all("Company", pluck="name") + for company in companies: + comp_vfd_provider = None + if frappe.db.exists("Company VFD Provider", company): + comp_vfd_provider = frappe.get_cached_doc("Company VFD Provider", company) + else: + continue + + vfd_provider = frappe.get_cached_doc("VFD Provider", comp_vfd_provider.vfd_provider) + + vfd_provider_settings = vfd_provider.vfd_provider_settings + if not vfd_provider_settings: + continue + + vfd_start_date = frappe.get_cached_value(vfd_provider_settings, company, "vfd_start_date") + + if not vfd_start_date: + continue + + invoices = frappe.db.get_all( + "Sales Invoice", + filters={ + "docstatus": 1, + "company": company, + "is_not_vfd_invoice": 0, + "is_return": 0, + "vfd_status": ["not in", ["Not Sent", "Success"]], + "posting_date": [">=", vfd_start_date], + }, + ) + + for invoice in invoices: + doc = frappe.get_doc("Sales Invoice", invoice.name) + + if vfd_provider.name == "VFDPlus": + vfdplus_post_fiscal_receipt(doc=doc, method="POST") + + elif vfd_provider.name == "TotalVFD": + total_vfd_post_fiscal_receipt(doc=doc, method="POST") + + elif vfd_provider.name == "SimplifyVFD": + simplify_vfd_post_fiscal_receipt(doc=doc, method="POST") + + else: + continue + + frappe.local.flags.vfd_posting = False def clean_and_update_tax_id_info(doc, method): - cleaned_tax_id = "".join(char for char in (doc.tax_id or "") if char.isdigit()) - doc.tax_id = cleaned_tax_id - if doc.tax_id: - doc.vfd_cust_id_type = "1- TIN" - doc.vfd_cust_id = doc.tax_id - else: - doc.vfd_cust_id_type = "6- Other" - doc.vfd_cust_id = "999999999" + cleaned_tax_id = "".join(char for char in (doc.tax_id or "") if char.isdigit()) + doc.tax_id = cleaned_tax_id + if doc.tax_id: + doc.vfd_cust_id_type = "1- TIN" + doc.vfd_cust_id = doc.tax_id + else: + doc.vfd_cust_id_type = "6- Other" + doc.vfd_cust_id = "999999999" diff --git a/license.txt b/license.txt index 2fdf7c7c..d3eefa54 100755 --- a/license.txt +++ b/license.txt @@ -1 +1 @@ -License: MIT \ No newline at end of file +License: MIT From cd8183395cb702066dc4c30b73cddb50854d55b9 Mon Sep 17 00:00:00 2001 From: MariamMabele Date: Thu, 6 Aug 2026 15:01:52 +0300 Subject: [PATCH 08/36] fix(vehicle-sync): defer failed retries until pending queue is processed (cherry picked from commit d83931e5070e04c3208c13103a39fdfcc1ba989e) --- .../doctype/vehicle_sync_task/processor.py | 4 +- .../csf_tz/doctype/vehicle_sync_task/queue.py | 59 ++++++++++++++++--- 2 files changed, 53 insertions(+), 10 deletions(-) diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py index e1e1c01d..6cf0960a 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py @@ -85,11 +85,11 @@ def run_vehicle_batch(): continue if status in {"rate_limited", "retryable_error"}: - attempts, _ = queue.bump_attempts(TASK_DOCTYPE, task) + current_attempts = frappe.db.get_value(TASK_DOCTYPE, task["name"], "attempts") or 0 queue.schedule_next( TASK_DOCTYPE, task, - _backoff_seconds(attempts), + _backoff_seconds(current_attempts + 1), result.get("message") or status, ) errors += 1 diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py index 30c7c680..c724221d 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py @@ -4,7 +4,7 @@ # ------------ CONFIGURATION ------------ BATCH_SIZE = 1 TIME_BUDGET_SEC = 50 -MAX_ATTEMPTS = 4 +MAX_ATTEMPTS = 2 BASE_BACKOFF = 300 BACKOFF_JITTER = 0.2 SUCCESS_INTERVAL_SECONDS = 60 * 60 * 2 @@ -22,6 +22,21 @@ def _jitter(seconds): jitter_factor = 1 + (random_factor * BACKOFF_JITTER) return int(seconds * jitter_factor) +def get_pending_cycle_delay(doctype): + try: + Task = frappe.qb.DocType(doctype) + pending = ( + frappe.qb.from_(Task) + .select(Task.name) + .where( + (Task.status == "Pending") & + ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) + ) + ).run() + return max(len(pending), 1) * 60 + except Exception: + return 60 + # ------------ CORE QUEUE OPERATIONS ------------ def claim_batch(doctype, limit=BATCH_SIZE): try: @@ -34,13 +49,27 @@ def claim_batch(doctype, limit=BATCH_SIZE): .where( (Task.status == "Pending") & ((Task.next_run_at.isnull()) | (Task.next_run_at <= now)) & - ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) # ← IGNORE DELETED TASKS + ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) ) .orderby(Task.priority, order=frappe.qb.terms.Order.desc) .orderby(Task.name) .limit(limit) ).run(as_dict=True) + if not rows: + rows = ( + frappe.qb.from_(Task) + .select(Task.name) + .where( + (Task.status == "Failed") & + (Task.next_run_at <= now) & + ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) + ) + .orderby(Task.priority, order=frappe.qb.terms.Order.desc) + .orderby(Task.name) + .limit(limit) + ).run(as_dict=True) + if not rows: return [] @@ -80,16 +109,22 @@ def mark_done(doctype, task): message=f"Error marking task {task.get('name')} as done in {doctype}: {str(e)}" ) -def mark_failed(doctype, task, err_msg): +def mark_failed(doctype, task, err_msg, next_run_at=None, reset_attempts=False): try: - frappe.db.set_value(doctype, task["name"], { + values = { "status": "Failed", "last_error": err_msg[:1000], "last_run_at": _now(), "claimed_by": "", "claimed_at": None, - "next_run_at": None, - }) + "next_run_at": next_run_at, + } + if reset_attempts: + values.update({ + "attempts": 0, + "backoff_exp": 0, + }) + frappe.db.set_value(doctype, task["name"], values) except Exception as e: frappe.log_error( title="Queue Mark Failed Error", @@ -119,10 +154,18 @@ def bump_attempts(doctype, task): def schedule_next(doctype, task, backoff_seconds, error_msg=""): try: attempts, _ = bump_attempts(doctype, task) + cycle_delay = get_pending_cycle_delay(doctype) + next_delay = max(backoff_seconds, cycle_delay) + next_run = frappe.utils.add_to_date(_now(), seconds=_jitter(next_delay)) if attempts >= MAX_ATTEMPTS: - mark_failed(doctype, task, error_msg or "Max attempts exceeded") + mark_failed( + doctype, + task, + error_msg or "Max attempts exceeded", + next_run_at=next_run, + reset_attempts=True, + ) return - next_run = frappe.utils.add_to_date(_now(), seconds=_jitter(backoff_seconds)) frappe.db.set_value(doctype, task["name"], { "status": "Pending", "claimed_by": "", From fdcf892bd028e0dfcf02adae713626a393b52075 Mon Sep 17 00:00:00 2001 From: MariamMabele Date: Fri, 21 Aug 2026 15:51:19 +0300 Subject: [PATCH 09/36] fix(vehicle-fines): simplify daily sync queue and failed task handling (cherry picked from commit 63abe8b6188b1e0467207a3fe9ce6b46b5083341) --- .../vehicle_fine_record.py | 76 ++-------------- .../doctype/vehicle_sync_task/processor.py | 29 +----- .../csf_tz/doctype/vehicle_sync_task/queue.py | 91 +------------------ 3 files changed, 16 insertions(+), 180 deletions(-) diff --git a/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py b/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py index 60e3d737..d82565aa 100644 --- a/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py +++ b/csf_tz/csf_tz/doctype/vehicle_fine_record/vehicle_fine_record.py @@ -18,7 +18,6 @@ send_authority_notification, ) import re -from time import sleep from frappe.utils import now_datetime from cryptography.hazmat.primitives import padding from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes @@ -144,77 +143,20 @@ def sync_vehicle_fines(number_plate): } payload = {"vehicle": number_plate} - max_retries = 3 - response = None - - for attempt in range(max_retries): - try: - if attempt > 0: - sleep(5 * attempt) - - response = requests.post(url, json=payload, headers=headers, timeout=30) - if response.status_code == 429: - return { - "status": "rate_limited", - "message": f"TPF rate limited {number_plate}", - "fine_list": [], - } - response.raise_for_status() - break - - except (requests.exceptions.Timeout, requests.exceptions.ConnectionError) as exc: - if attempt < max_retries - 1: - continue - frappe.logger().warning( - f"[VehicleFine] Connection timeout for {number_plate} " - f"after {max_retries} retries" - ) - return { - "status": "retryable_error", - "message": str(exc), - "fine_list": [], - } - - except requests.exceptions.HTTPError: - status = response.status_code if response is not None else 0 - if status in (408,) or status >= 500: - if attempt < max_retries - 1: - continue - frappe.logger().warning( - f"[VehicleFine] HTTP {status} for {number_plate} " - f"after {max_retries} retries" - ) - return { - "status": "retryable_error", - "message": f"HTTP {status}", - "fine_list": [], - } - - frappe.log_error( - title="TPF API Error", - message=( - f"HTTP {status} for {number_plate}: " - f"{response.text[:500] if response is not None else ''}" - ), - ) - return { - "status": "error", - "message": f"HTTP {status}", - "fine_list": [], - } - - except requests.exceptions.RequestException as exc: - frappe.log_error(title="TPF API Error", message=str(exc)) + try: + response = requests.post(url, json=payload, headers=headers, timeout=30) + if response.status_code == 429: return { - "status": "error", - "message": str(exc), + "status": "rate_limited", + "message": f"TPF rate limited {number_plate}", "fine_list": [], } - - if response is None: + response.raise_for_status() + except requests.exceptions.RequestException as exc: + frappe.logger().warning(f"[VehicleFine] TPF request failed for {number_plate}: {exc}") return { "status": "retryable_error", - "message": "No response from TPF", + "message": str(exc), "fine_list": [], } diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py index 6cf0960a..cf458012 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/processor.py @@ -46,14 +46,8 @@ def _acquire_rate_limit_slot(): return True -def _backoff_seconds(attempts): - exponent = max(attempts - 1, 0) - return queue.BASE_BACKOFF * (2 ** exponent) - - @frappe.whitelist() def run_vehicle_batch(): - started_at = time.monotonic() processed = 0 errors = 0 @@ -64,16 +58,8 @@ def run_vehicle_batch(): return {"status": "no_tasks", "message": "No pending vehicle sync tasks"} for task in tasks: - if (time.monotonic() - started_at) >= queue.TIME_BUDGET_SEC: - break - if not _acquire_rate_limit_slot(): - queue.schedule_next( - TASK_DOCTYPE, - task, - 60, - "TPF per-minute limit reached for this site", - ) + queue.mark_failed(TASK_DOCTYPE, task, "TPF per-minute limit reached for this site") continue result = sync_vehicle_fines(task["vehicle_no"]) @@ -84,21 +70,10 @@ def run_vehicle_batch(): processed += 1 continue - if status in {"rate_limited", "retryable_error"}: - current_attempts = frappe.db.get_value(TASK_DOCTYPE, task["name"], "attempts") or 0 - queue.schedule_next( - TASK_DOCTYPE, - task, - _backoff_seconds(current_attempts + 1), - result.get("message") or status, - ) - errors += 1 - continue - queue.mark_failed( TASK_DOCTYPE, task, - result.get("message") or "Unhandled sync error", + result.get("message") or status or "Unhandled sync error", ) errors += 1 diff --git a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py index c724221d..f2ad85d2 100644 --- a/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py +++ b/csf_tz/csf_tz/doctype/vehicle_sync_task/queue.py @@ -1,43 +1,13 @@ -import secrets import frappe -# ------------ CONFIGURATION ------------ BATCH_SIZE = 1 -TIME_BUDGET_SEC = 50 -MAX_ATTEMPTS = 2 -BASE_BACKOFF = 300 -BACKOFF_JITTER = 0.2 -SUCCESS_INTERVAL_SECONDS = 60 * 60 * 2 +SUCCESS_INTERVAL_SECONDS = 60 * 60 * 24 MAX_CALLS_PER_MINUTE = 1 WORKER_ID = frappe.local.site -# ------------ INTERNAL HELPERS ------------ def _now(): return frappe.utils.now_datetime() -def _jitter(seconds): - # Generate cryptographically secure random jitter for backoff timing - # Range: -BACKOFF_JITTER to +BACKOFF_JITTER - random_factor = (secrets.randbelow(10000) / 10000.0) * 2 - 1 # -1 to 1 - jitter_factor = 1 + (random_factor * BACKOFF_JITTER) - return int(seconds * jitter_factor) - -def get_pending_cycle_delay(doctype): - try: - Task = frappe.qb.DocType(doctype) - pending = ( - frappe.qb.from_(Task) - .select(Task.name) - .where( - (Task.status == "Pending") & - ((Task.is_deleted.isnull()) | (Task.is_deleted == 0)) - ) - ).run() - return max(len(pending), 1) * 60 - except Exception: - return 60 - -# ------------ CORE QUEUE OPERATIONS ------------ def claim_batch(doctype, limit=BATCH_SIZE): try: now = _now() @@ -109,21 +79,18 @@ def mark_done(doctype, task): message=f"Error marking task {task.get('name')} as done in {doctype}: {str(e)}" ) -def mark_failed(doctype, task, err_msg, next_run_at=None, reset_attempts=False): +def mark_failed(doctype, task, err_msg): try: values = { "status": "Failed", + "attempts": 0, + "backoff_exp": 0, "last_error": err_msg[:1000], "last_run_at": _now(), "claimed_by": "", "claimed_at": None, - "next_run_at": next_run_at, + "next_run_at": _now(), } - if reset_attempts: - values.update({ - "attempts": 0, - "backoff_exp": 0, - }) frappe.db.set_value(doctype, task["name"], values) except Exception as e: frappe.log_error( @@ -131,54 +98,6 @@ def mark_failed(doctype, task, err_msg, next_run_at=None, reset_attempts=False): message=f"Error marking task {task.get('name')} as failed in {doctype}: {str(e)}" ) -def bump_attempts(doctype, task): - try: - current = frappe.db.get_value( - doctype, task["name"], ["attempts", "backoff_exp"], as_dict=True - ) - attempts = (current.attempts or 0) + 1 - backoff_exp = min((current.backoff_exp or 0) + 1, 6) - frappe.db.set_value(doctype, task["name"], { - "attempts": attempts, - "backoff_exp": backoff_exp, - "last_run_at": _now() - }) - return attempts, backoff_exp - except Exception as e: - frappe.log_error( - title="Queue Bump Attempts Failed", - message=f"Error bumping attempts for task {task.get('name')} in {doctype}: {str(e)}" - ) - return 1, 1 # Return default values - -def schedule_next(doctype, task, backoff_seconds, error_msg=""): - try: - attempts, _ = bump_attempts(doctype, task) - cycle_delay = get_pending_cycle_delay(doctype) - next_delay = max(backoff_seconds, cycle_delay) - next_run = frappe.utils.add_to_date(_now(), seconds=_jitter(next_delay)) - if attempts >= MAX_ATTEMPTS: - mark_failed( - doctype, - task, - error_msg or "Max attempts exceeded", - next_run_at=next_run, - reset_attempts=True, - ) - return - frappe.db.set_value(doctype, task["name"], { - "status": "Pending", - "claimed_by": "", - "claimed_at": None, - "next_run_at": next_run, - "last_error": error_msg[:500] if error_msg else "", - }) - except Exception as e: - frappe.log_error( - title="Queue Schedule Next Failed", - message=f"Error scheduling next run for task {task.get('name')} in {doctype}: {str(e)}" - ) - def reset_stuck_tasks(doctype, timeout_minutes=10): try: timeout_time = frappe.utils.add_to_date(_now(), minutes=-timeout_minutes) From 0d6b7489a8fc34504fa688b2fa635b40b6a00dea Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:05 +0300 Subject: [PATCH 10/36] fix: drop field creation now defined in CSF TZ Settings JSON --- .../authority_notification_settings_fields.py | 106 ------------------ 1 file changed, 106 deletions(-) delete mode 100644 csf_tz/utils/authority_notification_settings_fields.py diff --git a/csf_tz/utils/authority_notification_settings_fields.py b/csf_tz/utils/authority_notification_settings_fields.py deleted file mode 100644 index 49420c03..00000000 --- a/csf_tz/utils/authority_notification_settings_fields.py +++ /dev/null @@ -1,106 +0,0 @@ -from frappe.custom.doctype.custom_field.custom_field import create_custom_fields - - -def execute(): - fields = { - "CSF TZ Settings": [ - { - "fieldname": "authority_notification_section", - "fieldtype": "Section Break", - "label": "Authority Notifications", - "insert_after": "tz_regions_populated", - }, - { - "fieldname": "enable_latra_license_notifications", - "fieldtype": "Check", - "label": "Enable LATRA License Notifications", - "default": "0", - "insert_after": "authority_notification_section", - }, - { - "fieldname": "enable_latra_offence_notifications", - "fieldtype": "Check", - "label": "Enable LATRA Offence Notifications", - "default": "0", - "insert_after": "enable_latra_license_notifications", - }, - { - "fieldname": "enable_tira_notifications", - "fieldtype": "Check", - "label": "Enable TIRA Notifications", - "default": "0", - "insert_after": "enable_latra_offence_notifications", - }, - { - "fieldname": "enable_vehicle_fine_notifications", - "fieldtype": "Check", - "label": "Enable Vehicle Fine Notifications", - "default": "0", - "insert_after": "enable_tira_notifications", - }, - { - "fieldname": "column_break_authority_notification", - "fieldtype": "Column Break", - "insert_after": "enable_vehicle_fine_notifications", - }, - { - "fieldname": "latra_license_notify_before_days", - "fieldtype": "Int", - "label": "LATRA License Notify Before Days", - "default": "7", - "depends_on": "eval:doc.enable_latra_license_notifications", - "mandatory_depends_on": "eval:doc.enable_latra_license_notifications", - "insert_after": "column_break_authority_notification", - }, - { - "fieldname": "latra_offence_notify_on_new", - "fieldtype": "Check", - "label": "LATRA Offence Notify On New", - "default": "1", - "depends_on": "eval:doc.enable_latra_offence_notifications", - "insert_after": "latra_license_notify_before_days", - }, - { - "fieldname": "latra_offence_notify_on_status_change", - "fieldtype": "Check", - "label": "LATRA Offence Notify On Status Change", - "default": "0", - "depends_on": "eval:doc.enable_latra_offence_notifications", - "insert_after": "latra_offence_notify_on_new", - }, - { - "fieldname": "tira_notify_before_days", - "fieldtype": "Int", - "label": "TIRA Notify Before Days", - "default": "7", - "depends_on": "eval:doc.enable_tira_notifications", - "mandatory_depends_on": "eval:doc.enable_tira_notifications", - "insert_after": "latra_offence_notify_on_status_change", - }, - { - "fieldname": "vehicle_fine_notify_on_new", - "fieldtype": "Check", - "label": "Vehicle Fine Notify On New", - "default": "1", - "depends_on": "eval:doc.enable_vehicle_fine_notifications", - "insert_after": "tira_notify_before_days", - }, - { - "fieldname": "vehicle_fine_notify_on_status_change", - "fieldtype": "Check", - "label": "Vehicle Fine Notify On Status Change", - "default": "0", - "depends_on": "eval:doc.enable_vehicle_fine_notifications", - "insert_after": "vehicle_fine_notify_on_new", - }, - { - "fieldname": "authority_notification_roles", - "fieldtype": "Table", - "label": "Authority Notification Roles", - "options": "Authority Notification Role", - "insert_after": "vehicle_fine_notify_on_status_change", - }, - ] - } - - create_custom_fields(fields, update=True) From 8481fcf43354051de6fb276a9390e6ebd547ba21 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:06 +0300 Subject: [PATCH 11/36] refactor: move NMB Callback doctype to edu_tz --- .../doctype/nmb_callback/nmb_callback.json | 165 ------------------ 1 file changed, 165 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.json diff --git a/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.json b/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.json deleted file mode 100644 index 266b194a..00000000 --- a/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.json +++ /dev/null @@ -1,165 +0,0 @@ -{ - "actions": [], - "allow_copy": 1, - "autoname": "NMBC-.YY.-.######", - "creation": "2020-07-21 21:19:58.817941", - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", - "field_order": [ - "timestamp", - "reference", - "column_break_3", - "receipt", - "amount", - "section_break_6", - "customer_name", - "column_break_8", - "account_number", - "section_break_10", - "token", - "column_break_12", - "fees_token", - "channel", - "section_break_13", - "payment_entry", - "section_break_16", - "api_key", - "api_secret" - ], - "fields": [ - { - "fieldname": "reference", - "fieldtype": "Data", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Reference", - "read_only": 1 - }, - { - "fieldname": "timestamp", - "fieldtype": "Datetime", - "label": "Timestamp", - "read_only": 1 - }, - { - "fieldname": "receipt", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Receipt", - "read_only": 1 - }, - { - "fieldname": "customer_name", - "fieldtype": "Data", - "in_list_view": 1, - "label": "Customer Name", - "read_only": 1 - }, - { - "fieldname": "account_number", - "fieldtype": "Data", - "label": "Account Number", - "read_only": 1 - }, - { - "fieldname": "token", - "fieldtype": "Data", - "label": "Token", - "read_only": 1 - }, - { - "fieldname": "amount", - "fieldtype": "Float", - "in_list_view": 1, - "label": "Amount", - "read_only": 1 - }, - { - "fieldname": "api_key", - "fieldtype": "Data", - "hidden": 1, - "label": "Api Key", - "read_only": 1 - }, - { - "fieldname": "api_secret", - "fieldtype": "Data", - "hidden": 1, - "label": "Api Secret", - "read_only": 1 - }, - { - "fieldname": "fees_token", - "fieldtype": "Data", - "label": "Fees Token", - "read_only": 1 - }, - { - "fieldname": "payment_entry", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Payment Entry", - "options": "Payment Entry", - "read_only": 1 - }, - { - "fieldname": "channel", - "fieldtype": "Data", - "label": "Channel", - "read_only": 1 - }, - { - "fieldname": "column_break_3", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_6", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_8", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_10", - "fieldtype": "Section Break" - }, - { - "fieldname": "section_break_13", - "fieldtype": "Section Break" - }, - { - "fieldname": "column_break_12", - "fieldtype": "Column Break" - }, - { - "fieldname": "section_break_16", - "fieldtype": "Section Break" - } - ], - "links": [], - "modified": "2020-07-29 18:09:06.415004", - "modified_by": "Administrator", - "module": "CSF TZ", - "name": "NMB Callback", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "write": 1 - } - ], - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "reference", - "track_changes": 1 -} \ No newline at end of file From ae36179b628da2e0073508bbc4ae16c8342f3e2a Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:07 +0300 Subject: [PATCH 12/36] refactor: move NMB Callback controller to edu_tz --- csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py diff --git a/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py b/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py deleted file mode 100644 index f35e4d32..00000000 --- a/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2020, Aakvatech and contributors -# For license information, please see license.txt - -# import frappe -from frappe.model.document import Document - - -class NMBCallback(Document): - pass From 44d798b8b50e68a238cb1cc08975917bf7118033 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:08 +0300 Subject: [PATCH 13/36] refactor: move NMB Callback form script to edu_tz --- .../csf_tz/doctype/nmb_callback/nmb_callback.js | 17 ----------------- 1 file changed, 17 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.js diff --git a/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.js b/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.js deleted file mode 100644 index cd13c810..00000000 --- a/csf_tz/csf_tz/doctype/nmb_callback/nmb_callback.js +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright (c) 2020, Aakvatech and contributors -// For license information, please see license.txt - -frappe.ui.form.on('NMB Callback', { - refresh: function(frm) { - frm.add_custom_button(__('Make Payment Entry'), - function () { - frappe.call({ - method: "csf_tz.bank_api.make_payment_entry_from_call", - args: { - docname: frm.doc.name, - }, - }); - } - ); - } -}); From 7f6f97337bb14f2e9d96c0354be40f88051ed8f1 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:09 +0300 Subject: [PATCH 14/36] test: move NMB Callback test stub to edu_tz --- csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py diff --git a/csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py b/csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py deleted file mode 100644 index 736abd53..00000000 --- a/csf_tz/csf_tz/doctype/nmb_callback/test_nmb_callback.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2020, Aakvatech and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestNMBCallback(unittest.TestCase): - pass From 46f2436feab36057d5bdf4d1d455aaaa2fa5533b Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:10 +0300 Subject: [PATCH 15/36] refactor: remove NMB Callback package from csf_tz --- csf_tz/csf_tz/doctype/nmb_callback/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/nmb_callback/__init__.py diff --git a/csf_tz/csf_tz/doctype/nmb_callback/__init__.py b/csf_tz/csf_tz/doctype/nmb_callback/__init__.py deleted file mode 100644 index e69de29b..00000000 From 69527d8e56c570ff6fdd6da95d30e142a41d5ad3 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:11 +0300 Subject: [PATCH 16/36] refactor: move Student Applicant Fees doctype to edu_tz --- .../student_applicant_fees.json | 420 ------------------ 1 file changed, 420 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.json diff --git a/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.json b/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.json deleted file mode 100644 index b860273e..00000000 --- a/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.json +++ /dev/null @@ -1,420 +0,0 @@ -{ - "allow_import": 1, - "autoname": "naming_series:", - "creation": "2020-08-22 23:55:57.796292", - "doctype": "DocType", - "document_type": "Document", - "engine": "InnoDB", - "field_order": [ - "naming_series", - "student", - "student_name", - "fee_schedule", - "include_payment", - "send_payment_request", - "bank_reference", - "column_break_4", - "company", - "abbr", - "posting_date", - "posting_time", - "set_posting_time", - "due_date", - "student_details", - "program_enrollment", - "program", - "student_batch", - "student_email", - "column_break_16", - "student_category", - "academic_term", - "academic_year", - "section_break_7", - "currency", - "fee_structure", - "section_break_10", - "grand_total", - "grand_total_in_words", - "column_break_11", - "outstanding_amount", - "edit_printing_settings", - "letter_head", - "column_break_32", - "select_print_heading", - "account", - "receivable_account", - "column_break_39", - "income_account", - "accounting_dimensions_section", - "callback_token", - "cost_center", - "dimension_col_break", - "amended_from" - ], - "fields": [ - { - "fieldname": "naming_series", - "fieldtype": "Select", - "label": "Naming Series", - "options": "RFEE-.abbr.-.YY.-", - "print_hide": 1, - "set_only_once": 1 - }, - { - "fieldname": "student", - "fieldtype": "Link", - "in_global_search": 1, - "label": "Student Applicant", - "options": "Student Applicant", - "reqd": 1 - }, - { - "fetch_from": "student.title", - "fieldname": "student_name", - "fieldtype": "Data", - "in_global_search": 1, - "label": "Student Applicant Name", - "read_only": 1 - }, - { - "fieldname": "fee_schedule", - "fieldtype": "Link", - "in_global_search": 1, - "label": "Fee Schedule", - "options": "Fee Schedule", - "print_hide": 1, - "read_only": 1 - }, - { - "default": "0", - "fieldname": "include_payment", - "fieldtype": "Check", - "hidden": 1, - "label": "Include Payment", - "print_hide": 1 - }, - { - "default": "0", - "fieldname": "send_payment_request", - "fieldtype": "Check", - "label": "Send Payment Request", - "no_copy": 1, - "print_hide": 1 - }, - { - "fieldname": "column_break_4", - "fieldtype": "Column Break" - }, - { - "fieldname": "company", - "fieldtype": "Link", - "label": "Institution", - "options": "Company", - "remember_last_selected_value": 1, - "reqd": 1 - }, - { - "bold": 1, - "default": "Today", - "fieldname": "posting_date", - "fieldtype": "Date", - "label": "Date", - "no_copy": 1, - "reqd": 1, - "search_index": 1 - }, - { - "fieldname": "posting_time", - "fieldtype": "Time", - "label": "Posting Time", - "no_copy": 1 - }, - { - "default": "0", - "depends_on": "eval:doc.docstatus==0", - "fieldname": "set_posting_time", - "fieldtype": "Check", - "label": "Edit Posting Date and Time", - "print_hide": 1 - }, - { - "fieldname": "due_date", - "fieldtype": "Date", - "label": "Due Date", - "reqd": 1 - }, - { - "collapsible": 1, - "fieldname": "student_details", - "fieldtype": "Section Break", - "label": "Student Details" - }, - { - "fieldname": "program_enrollment", - "fieldtype": "Link", - "label": "Program Enrollment", - "options": "Program Enrollment" - }, - { - "fieldname": "program", - "fieldtype": "Link", - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Program", - "options": "Program" - }, - { - "fieldname": "student_batch", - "fieldtype": "Link", - "label": "Student Batch", - "options": "Student Batch Name", - "print_hide": 1 - }, - { - "allow_on_submit": 1, - "fieldname": "student_email", - "fieldtype": "Data", - "label": "Student Email", - "options": "Email", - "print_hide": 1 - }, - { - "fieldname": "column_break_16", - "fieldtype": "Column Break" - }, - { - "fieldname": "student_category", - "fieldtype": "Link", - "label": "Student Category", - "options": "Student Category" - }, - { - "fieldname": "academic_term", - "fieldtype": "Link", - "label": "Academic Term", - "options": "Academic Term" - }, - { - "fieldname": "academic_year", - "fieldtype": "Link", - "label": "Academic Year", - "options": "Academic Year" - }, - { - "fieldname": "section_break_7", - "fieldtype": "Section Break" - }, - { - "fieldname": "currency", - "fieldtype": "Link", - "hidden": 1, - "label": "Currency", - "options": "Currency", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "fee_structure", - "fieldtype": "Link", - "label": "Fee Structure", - "options": "Fee Structure", - "print_hide": 1 - }, - { - "fieldname": "section_break_10", - "fieldtype": "Section Break" - }, - { - "default": "0", - "fieldname": "grand_total", - "fieldtype": "Currency", - "label": "Grand Total" - }, - { - "fieldname": "grand_total_in_words", - "fieldtype": "Data", - "label": "In Words", - "read_only": 1 - }, - { - "fieldname": "column_break_11", - "fieldtype": "Column Break" - }, - { - "default": "0", - "fieldname": "outstanding_amount", - "fieldtype": "Currency", - "label": "Outstanding Amount", - "no_copy": 1, - "read_only": 1 - }, - { - "collapsible": 1, - "fieldname": "edit_printing_settings", - "fieldtype": "Section Break", - "label": "Printing Settings", - "print_hide": 1 - }, - { - "allow_on_submit": 1, - "fieldname": "letter_head", - "fieldtype": "Link", - "label": "Letter Head", - "options": "Letter Head", - "print_hide": 1 - }, - { - "fieldname": "column_break_32", - "fieldtype": "Column Break" - }, - { - "allow_on_submit": 1, - "fieldname": "select_print_heading", - "fieldtype": "Link", - "label": "Print Heading", - "no_copy": 1, - "options": "Print Heading", - "print_hide": 1, - "report_hide": 1 - }, - { - "fieldname": "account", - "fieldtype": "Section Break", - "label": "Accounting", - "print_hide": 1 - }, - { - "fieldname": "receivable_account", - "fieldtype": "Link", - "label": "Receivable Account", - "options": "Account", - "print_hide": 1, - "reqd": 1 - }, - { - "fieldname": "column_break_39", - "fieldtype": "Column Break", - "print_hide": 1 - }, - { - "fieldname": "income_account", - "fieldtype": "Link", - "label": "Income Account", - "options": "Account", - "print_hide": 1 - }, - { - "fieldname": "accounting_dimensions_section", - "fieldtype": "Section Break", - "label": "Accounting Dimensions" - }, - { - "fieldname": "cost_center", - "fieldtype": "Link", - "label": "Cost Center", - "options": "Cost Center", - "print_hide": 1 - }, - { - "fieldname": "dimension_col_break", - "fieldtype": "Column Break" - }, - { - "fieldname": "amended_from", - "fieldtype": "Link", - "label": "Amended From", - "no_copy": 1, - "options": "Student Applicant Fees", - "print_hide": 1, - "read_only": 1 - }, - { - "fieldname": "bank_reference", - "fieldtype": "Data", - "label": "Bank Reference", - "no_copy": 1, - "read_only": 1 - }, - { - "fetch_from": "company.abbr", - "fieldname": "abbr", - "fieldtype": "Data", - "label": "Abbr", - "read_only": 1 - }, - { - "fieldname": "callback_token", - "fieldtype": "Data", - "label": "Callback Token", - "no_copy": 1, - "read_only": 1 - } - ], - "is_submittable": 1, - "modified": "2020-08-24 16:45:12.635352", - "modified_by": "Administrator", - "module": "CSF TZ", - "name": "Student Applicant Fees", - "owner": "Administrator", - "permissions": [ - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Academics User", - "share": 1, - "write": 1 - }, - { - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "share": 1, - "submit": 1, - "write": 1 - }, - { - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "share": 1, - "submit": 1, - "write": 1 - } - ], - "restrict_to_domain": "Education", - "search_fields": "student, student_name", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "student_name" -} \ No newline at end of file From 0fa8028057549f77770cbffbe985717fbe868ef4 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:12 +0300 Subject: [PATCH 17/36] refactor: move Student Applicant Fees controller to edu_tz --- .../student_applicant_fees.py | 50 ------------------- 1 file changed, 50 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.py diff --git a/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.py b/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.py deleted file mode 100644 index fe2c6493..00000000 --- a/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.py +++ /dev/null @@ -1,50 +0,0 @@ -# Copyright (c) 2020, Aakvatech and contributors -# For license information, please see license.txt - -import binascii -import os - -import frappe -from frappe import _ -from frappe.model.document import Document - -from csf_tz.bank_api import cancel_invoice, invoice_submission - - -class StudentApplicantFees(Document): - def after_insert(self): - if not check_send_fee_details_to_bank(self.company): - return - self.callback_token = binascii.hexlify(os.urandom(14)).decode() - self.db_set("callback_token", self.callback_token) - series = frappe.get_value("Company", self.company, "nmb_series") or "" - if not series: - frappe.throw(_(f"Please set NMB User Series in Company {self.company}")) - reference = str(series) + "R" + str(self.name) - if not self.abbr: - self.abbr = frappe.get_value("Company", self.company, "abbr") or "" - self.db_set("abbr", self.abbr) - self.bank_reference = reference.replace("-", "").replace("RFEE" + self.abbr, "") - self.db_set("bank_reference", self.bank_reference) - - def on_submit(self): - if not check_send_fee_details_to_bank(self.company): - return - invoice_submission(self) - - def on_cancel(self): - if check_send_fee_details_to_bank(self.company): - cancel_invoice(self, "on_cancel") - doc = frappe.get_doc("Student Applicant", self.student) - doc.bank_reference = None - doc.student_applicant_fee = None - doc.application_status = "Applied" - doc.db_update() - - -def check_send_fee_details_to_bank(company): - send_fee_details_to_bank = frappe.get_value("Company", company, "send_fee_details_to_bank") or 0 - if not send_fee_details_to_bank: - return False - else: - return True From 8be027f63206985ab4ce2d8e37d9375911ac4f9f Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:13 +0300 Subject: [PATCH 18/36] refactor: move Student Applicant Fees form script to edu_tz --- .../student_applicant_fees.js | 85 ------------------- 1 file changed, 85 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.js diff --git a/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.js b/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.js deleted file mode 100644 index c6026d90..00000000 --- a/csf_tz/csf_tz/doctype/student_applicant_fees/student_applicant_fees.js +++ /dev/null @@ -1,85 +0,0 @@ -// Copyright (c) 2020, Aakvatech and contributors -// For license information, please see license.txt - -frappe.ui.form.on('Student Applicant Fees', { - setup: function(frm) { - frm.add_fetch("fee_structure", "receivable_account", "receivable_account"); - frm.add_fetch("fee_structure", "income_account", "income_account"); - frm.add_fetch("fee_structure", "cost_center", "cost_center"); - }, - - onload: function(frm){ - frm.set_query("academic_term",function(){ - return{ - "filters":{ - "academic_year": (frm.doc.academic_year) - } - }; - }); - frm.set_query("fee_structure",function(){ - return{ - "filters":{ - "academic_year": (frm.doc.academic_year) - } - }; - }); - frm.set_query("receivable_account", function(doc) { - return { - filters: { - 'account_type': 'Receivable', - 'is_group': 0, - 'company': doc.company - } - }; - }); - frm.set_query("income_account", function(doc) { - return { - filters: { - 'account_type': 'Income Account', - 'is_group': 0, - 'company': doc.company - } - }; - }); - if (!frm.doc.posting_date) { - frm.doc.posting_date = frappe.datetime.get_today(); - } - }, - - refresh: function(frm) { - if(frm.doc.docstatus == 0 && frm.doc.set_posting_time) { - frm.set_df_property('posting_date', 'read_only', 0); - frm.set_df_property('posting_time', 'read_only', 0); - } else { - frm.set_df_property('posting_date', 'read_only', 1); - frm.set_df_property('posting_time', 'read_only', 1); - } - }, - - student: function(frm) { - if (frm.doc.student) { - frappe.call({ - method:"erpnext.education.api.get_current_enrollment", - args: { - "student": frm.doc.student, - "academic_year": frm.doc.academic_year - }, - callback: function(r) { - if(r){ - $.each(r.message, function(i, d) { - frm.set_value(i,d); - }); - } - } - }); - } - }, - - set_posting_time: function(frm) { - frm.refresh(); - }, - - academic_term: function() { - frappe.ui.form.trigger("Fees", "program"); - }, -}); From 5f492b66162b5d326b99630f540befb53c3cd914 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:14 +0300 Subject: [PATCH 19/36] test: move Student Applicant Fees test stub to edu_tz --- .../test_student_applicant_fees.py | 9 --------- 1 file changed, 9 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/student_applicant_fees/test_student_applicant_fees.py diff --git a/csf_tz/csf_tz/doctype/student_applicant_fees/test_student_applicant_fees.py b/csf_tz/csf_tz/doctype/student_applicant_fees/test_student_applicant_fees.py deleted file mode 100644 index 90dc2f17..00000000 --- a/csf_tz/csf_tz/doctype/student_applicant_fees/test_student_applicant_fees.py +++ /dev/null @@ -1,9 +0,0 @@ -# Copyright (c) 2020, Aakvatech and Contributors -# See license.txt - -# import frappe -import unittest - - -class TestStudentApplicantFees(unittest.TestCase): - pass From 8b084a2632a5543590829b315f9dafdd70774ec0 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:15 +0300 Subject: [PATCH 20/36] refactor: remove Student Applicant Fees package from csf_tz --- csf_tz/csf_tz/doctype/student_applicant_fees/__init__.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 csf_tz/csf_tz/doctype/student_applicant_fees/__init__.py diff --git a/csf_tz/csf_tz/doctype/student_applicant_fees/__init__.py b/csf_tz/csf_tz/doctype/student_applicant_fees/__init__.py deleted file mode 100644 index e69de29b..00000000 From 57d8d7e2b886e2367a575efce97a624d5e0fbb7d Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:16 +0300 Subject: [PATCH 21/36] refactor: move Program Enrollment hooks to edu_tz --- csf_tz/csftz_hooks/program_enrollment.py | 57 ------------------------ 1 file changed, 57 deletions(-) delete mode 100644 csf_tz/csftz_hooks/program_enrollment.py diff --git a/csf_tz/csftz_hooks/program_enrollment.py b/csf_tz/csftz_hooks/program_enrollment.py deleted file mode 100644 index fad98b18..00000000 --- a/csf_tz/csftz_hooks/program_enrollment.py +++ /dev/null @@ -1,57 +0,0 @@ -import frappe - -# nosemgrep: frappe-semgrep-rules.rules.frappe-monkey-patching-not-allowed -from education.education.doctype.program_enrollment.program_enrollment import ProgramEnrollment -from frappe import _ - -# from csf_tz import console - - -def create_course_enrollments(self): - student = frappe.get_doc("Student", self.student) - program = frappe.get_doc("Program", self.program) - course_list = [course.course for course in program.courses] - for course_name in course_list: - student.enroll_in_course(course_name=course_name, program_enrollment=self.name) - - -def create_course_enrollments_override(doc, method): - ProgramEnrollment.create_course_enrollments = create_course_enrollments - - -@frappe.whitelist() -def get_fee_schedule(program, academic_year, academic_term=None, student_category=None): - """Returns Fee Schedule. - - :param program: Program. - :param student_category: Student Category - :param academic_year - :param academic_term - """ - fs = frappe.get_list( - "Program Fee", - fields=["academic_term", "fee_structure", "due_date", "amount"], - filters={"parent": program, "student_category": student_category}, - parent_doctype="Program Enrollment", - order_by="idx", - ) - - fees_list = [] - for i in fs: - fs_academic_year = frappe.get_value("Fee Structure", i["fee_structure"], "academic_year") or "" - fs_academic_term = "False" - if academic_term: - fs_academic_term = frappe.get_value("Fee Structure", i["fee_structure"], "academic_term") or "" - if fs_academic_term != "False": - if fs_academic_term == academic_term and fs_academic_year == academic_year: - fees_list.append(i) - else: - if fs_academic_year == academic_year: - fees_list.append(i) - - return fees_list - - -def validate_submit_program_enrollment(doc, method): - if not doc.student_category: - frappe.throw(_("Please set Student Category")) From c90989510b616b1178943a9360005753e1d7a738 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:17 +0300 Subject: [PATCH 22/36] refactor: move Student Applicant fee creation to edu_tz --- csf_tz/csftz_hooks/student_applicant.py | 55 ------------------------- 1 file changed, 55 deletions(-) delete mode 100644 csf_tz/csftz_hooks/student_applicant.py diff --git a/csf_tz/csftz_hooks/student_applicant.py b/csf_tz/csftz_hooks/student_applicant.py deleted file mode 100644 index ee6e645e..00000000 --- a/csf_tz/csftz_hooks/student_applicant.py +++ /dev/null @@ -1,55 +0,0 @@ -import frappe -from frappe.utils import today - -# from frappe.utils import from frappe.utils import today, format_datetime, now, nowdate, getdate, get_url, get_host_name, format_datetime, now, nowdate, getdate, get_url, get_host_name - - -def make_student_applicant_fees(doc, method): - if doc.docstatus != 1: - return - if doc.application_status != "Awaiting Registration Fees" or doc.student_applicant_fee: - return - fee_structure = frappe.get_doc("Fee Structure", doc.fee_structure) - student_name = doc.first_name - if doc.middle_name: - student_name += " " + doc.middle_name - if doc.last_name: - student_name += " " + doc.last_name - - fee_doc = frappe.get_doc( - { - "doctype": "Student Applicant Fees", - "student": doc.name, - "student_name": student_name, - "fee_schedule": None, - "company": fee_structure.company, - "posting_date": today(), - "due_date": today(), - "program_enrollment": doc.program_enrollment, - "program": fee_structure.program, - "student_batch": None, - "student_email": doc.student_email_id, - "student_category": fee_structure.student_category, - "academic_term": fee_structure.academic_term, - "academic_year": fee_structure.academic_year, - "currency": frappe.get_value("Company", fee_structure.company, "default_currency"), - "fee_structure": doc.fee_structure, - "grand_total": fee_structure.total_amount, - "receivable_account": fee_structure.receivable_account, - "income_account": fee_structure.income_account, - "cost_center": fee_structure.cost_center, - } - ) - - fee_doc.flags.ignore_permissions = True - frappe.flags.ignore_account_permission = True - fee_doc.save() - callback_token = fee_doc.callback_token - doc.bank_reference = fee_doc.bank_reference or "None" - doc.student_applicant_fee = fee_doc.name or "None" - doc.db_update() - fee_doc.reload() - fee_doc.callback_token = callback_token - fee_doc.bank_reference = doc.bank_reference - fee_doc.db_update() - fee_doc.submit() From 1a7ec69e1a211fa2472db72a8a858432e01a676f Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:18 +0300 Subject: [PATCH 23/36] refactor: move Fees form script to edu_tz --- csf_tz/csf_tz/fees.js | 26 -------------------------- 1 file changed, 26 deletions(-) delete mode 100644 csf_tz/csf_tz/fees.js diff --git a/csf_tz/csf_tz/fees.js b/csf_tz/csf_tz/fees.js deleted file mode 100644 index 7b9435d5..00000000 --- a/csf_tz/csf_tz/fees.js +++ /dev/null @@ -1,26 +0,0 @@ -frappe.ui.form.on('Fees', { - refresh: function (frm) { - if (frm.doc.docstatus == 1 && frm.doc.outstanding_amount > 0) { - frm.add_custom_button(__("Invoice Submission"), function () { - frappe.call({ - method: 'csf_tz.bank_api.invoice_submission', - args: { - fees_name: frm.doc.name, - }, - callback: function (r) { - if (r.message) { - console.log(r.message); - } - } - }); - }); - }; - frm.set_query("sales_invoice_income_account", function () { - return { - filters: [ - ["Account", "company", "=", frm.doc.company] - ] - }; - }); - }, -}); From a364a6a116f4a369c37c45f4c2a738f3b1b973c4 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:19 +0300 Subject: [PATCH 24/36] refactor: move Program Enrollment form script to edu_tz --- csf_tz/csf_tz/program_enrollment.js | 34 ----------------------------- 1 file changed, 34 deletions(-) delete mode 100644 csf_tz/csf_tz/program_enrollment.js diff --git a/csf_tz/csf_tz/program_enrollment.js b/csf_tz/csf_tz/program_enrollment.js deleted file mode 100644 index caa0883d..00000000 --- a/csf_tz/csf_tz/program_enrollment.js +++ /dev/null @@ -1,34 +0,0 @@ -frappe.ui.form.on("Program Enrollment", { - program: function (frm) { - frm.set_value("fees", ""); - frm.events.get_courses(frm); - if (frm.doc.program) { - frappe.call({ - method: "csf_tz.csftz_hooks.program_enrollment.get_fee_schedule", - args: { - "program": frm.doc.program, - "student_category": frm.doc.student_category, - "academic_year": frm.doc.academic_year, - "academic_term": frm.doc.academic_term - }, - async: false, - callback: function (r) { - if (r.message) { - frm.set_value("fees", r.message); - frm.events.get_courses(frm); - } - } - }); - } - }, - - student_category: function () { - frappe.ui.form.trigger("program"); - }, - - validate: function (frm) { - if (( !frm.doc.fees || !frm.doc.fees.length) && frm.doc.student_category) { - frm.trigger("program"); - } - } -}); From 66077b97a9840d9f13f96469ef6b92a848e09bcb Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:20 +0300 Subject: [PATCH 25/36] refactor: move Program Enrollment Tool script to edu_tz --- csf_tz/csf_tz/program_enrollment_tool.js | 38 ------------------------ 1 file changed, 38 deletions(-) delete mode 100644 csf_tz/csf_tz/program_enrollment_tool.js diff --git a/csf_tz/csf_tz/program_enrollment_tool.js b/csf_tz/csf_tz/program_enrollment_tool.js deleted file mode 100644 index f7da49d7..00000000 --- a/csf_tz/csf_tz/program_enrollment_tool.js +++ /dev/null @@ -1,38 +0,0 @@ -frappe.ui.form.on("Program Enrollment Tool", { - refresh: function (frm) { - frm.toggle_display(['enroll_students']); - }, - academic_year: function (frm) { - frm.toggle_display("enroll_students", is_viewable); - }, - get_students: function (frm) { - if (frm.doc.students.length > 0) { - frm.add_custom_button(__("Enroll All Students"), function () { - if (frm.doc.students.length > 0) { - frappe.call({ - method: "csf_tz.custom_api.enroll_all_students", - args: { - "self": frm.doc - }, - callback: function (r) { - if (r.message === 'queued') { - frappe.show_alert({ - message: __("Students enrollment has been queued."), - indicator: 'orange' - }); - } else { - frappe.show_alert({ - message: __("{0} students enrolled.", [r.message]), - indicator: 'green' - }); - } - } - }); - } else { - frappe.msgprint("No students to enroll") - } - }) - } - }, - -}) From be1d23c4a2b9a1fe989090159341ef4f29981ec6 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:21 +0300 Subject: [PATCH 26/36] refactor: move Student Applicant form script to edu_tz --- csf_tz/csf_tz/student_applicant.js | 37 ------------------------------ 1 file changed, 37 deletions(-) delete mode 100644 csf_tz/csf_tz/student_applicant.js diff --git a/csf_tz/csf_tz/student_applicant.js b/csf_tz/csf_tz/student_applicant.js deleted file mode 100644 index d941087e..00000000 --- a/csf_tz/csf_tz/student_applicant.js +++ /dev/null @@ -1,37 +0,0 @@ -frappe.ui.form.on('Student Applicant', { - onload: function(frm) { - frm.trigger("setup_btns"); - }, - refresh: function(frm) { - frm.trigger("setup_btns"); - }, - setup_btns: function(frm) { - if (!frm.send_fee_details_to_bank) { - return; - } - if(frm.doc.docstatus == 1 && frm.doc.application_status != "Approved") { - frm.clear_custom_buttons(); - if(frm.doc.application_status == "Applied") { - frm.add_custom_button(__("Reject"), function() { - frm.set_value("application_status", "Rejected"); - frm.save_or_update(); - }, 'Student Applicant Actions'); - } - if(["Applied", "Rejected"].includes(frm.doc.application_status)) { - frm.add_custom_button(__("Awaiting Registration Fees"), function() { - frm.set_value("application_status", "Awaiting Registration Fees"); - frm.save_or_update(); - }, 'Student Applicant Actions'); - } - } - }, - setup: function(frm) { - frappe.db.get_value('Fee Structure', frm.doc.fee_structure, ["company"], function(value1) { - frappe.db.get_value('Company', value1.company, ["send_fee_details_to_bank"], function(value2) { - frm.send_fee_details_to_bank = value2.send_fee_details_to_bank || 0; - - }); - }); - }, - -}); From 03fda0cad0fec05708bbbbdf67d6332a98742363 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:22 +0300 Subject: [PATCH 27/36] refactor: drop fee and enrollment helpers moved to edu_tz --- csf_tz/custom_api.py | 82 -------------------------------------------- 1 file changed, 82 deletions(-) diff --git a/csf_tz/custom_api.py b/csf_tz/custom_api.py index 762542c3..9447ac0f 100644 --- a/csf_tz/custom_api.py +++ b/csf_tz/custom_api.py @@ -462,15 +462,6 @@ def delete_doc(doctype, docname): frappe.msgprint(_("{0} {1} is Deleted").format("Stock Entry", doc.name)) -def on_cancel_fees(doc, method): - from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries - - unlink_ref_doc_from_payment_entries(doc) - from csf_tz.bank_api import cancel_invoice - - cancel_invoice(doc, "before_cancel") - - def check_validate_delivery_note(doc=None, method=None, doc_name=None): if not doc and doc_name: doc = frappe.get_doc("Sales Invoice", doc_name) @@ -989,79 +980,6 @@ def make_withholding_tax_gl_entries_for_purchase(doc: Any, method: Any): ) -@frappe.whitelist() -def set_fee_abbr(doc: Any = None, method: Any = None): - doc.company = frappe.get_value("Fee Structure", doc.fee_structure, "company") - send_fee_details_to_bank = frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 - if not send_fee_details_to_bank: - return - doc.abbr = frappe.get_value("Company", doc.company, "abbr") - - -@frappe.whitelist() -def enroll_all_students(self): - """Enrolls students or applicants. - - :param self: Program Enrollment Tool - - This is created to allow enqueue of students creation. - The default enroll process fails when there are too many enrollments to do at a go - """ - import json - - self = json.loads(self) - self = frappe.get_doc(dict(self)) - - if self.get_students_from == "Student Applicant": - frappe.msgprint(_("Remove student applicants that are already created")) - - if len(self.students) > 30: - frappe.enqueue("csf_tz.custom_api.enroll_students", self=self) - return "queued" - else: - enroll_students(self=self) - return len(self.students) - - -@frappe.whitelist() -def enroll_students(self): - """Enrolls students or applicants. - - :param self: Program Enrollment Tool - - This is a copy of ERPNext function meant to allow loading from custom doctypes and frappe.enqueue - Used in csf_tz.custom_api.enroll_students - """ - from education.education.api import enroll_student - - total = len(self.students) - for i, stud in enumerate(self.students): - frappe.publish_realtime( - "program_enrollment_tool", - dict(progress=[i + 1, total]), - user=frappe.session.user, - ) - if stud.student: - prog_enrollment = frappe.new_doc("Program Enrollment") - prog_enrollment.student = stud.student - prog_enrollment.student_name = stud.student_name - prog_enrollment.program = self.new_program - prog_enrollment.academic_year = self.new_academic_year - prog_enrollment.academic_term = self.new_academic_term - prog_enrollment.student_batch_name = ( - stud.student_batch_name if stud.student_batch_name else self.new_student_batch - ) - prog_enrollment.save() - elif stud.student_applicant: - prog_enrollment = enroll_student(stud.student_applicant) - prog_enrollment.academic_year = self.academic_year - prog_enrollment.academic_term = self.academic_term - prog_enrollment.student_batch_name = ( - stud.student_batch_name if stud.student_batch_name else self.new_student_batch - ) - prog_enrollment.save() - - @frappe.whitelist() def get_tax_category(doc_type: Any, company: Any): fetch_default_tax_category = ( From 8728eecbf42e7c76308fca53d05b9f687ff0a3f9 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:31 +0300 Subject: [PATCH 28/36] refactor: reduce bank_api to NMB callback shims forwarding to edu_tz --- csf_tz/bank_api.py | 475 ++------------------------------------------- 1 file changed, 13 insertions(+), 462 deletions(-) diff --git a/csf_tz/bank_api.py b/csf_tz/bank_api.py index 5bc79f6b..ac91a8e9 100644 --- a/csf_tz/bank_api.py +++ b/csf_tz/bank_api.py @@ -1,477 +1,28 @@ -# Copyright (c) 2020, Youssef Restom and contributors -# For license information, please see license.txt +"""Deprecated NMB endpoints. -import binascii -import json -import os -from datetime import datetime -from time import sleep -from urllib.parse import quote, urlparse, urlunparse +The NMB fee integration lives in edu_tz.edu_tz.nmb.api. These entry points stay +because NMB stores the callback URL of invoices submitted before the move. +""" import frappe -import requests -from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry from frappe import _ -from frappe.utils import flt, get_host_name -from frappe.utils.background_jobs import enqueue -from frappe.utils.password import get_decrypted_password -from csf_tz.csf_tz.doctype.csf_api_response_log.csf_api_response_log import add_log +def get_callback_handler(name: str): + if "edu_tz" not in frappe.get_installed_apps(): + frappe.throw(_("NMB fee callbacks moved to the edu_tz app. Install edu_tz to process them.")) + from edu_tz.edu_tz.nmb import api -class ToObject: - def __init__(self, data): - self.__dict__ = json.loads(data) - - -def set_callback_token(doc, method): - send_fee_details_to_bank = frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 - if not send_fee_details_to_bank: - return - doc.callback_token = binascii.hexlify(os.urandom(14)).decode() - series = frappe.get_value("Company", doc.company, "nmb_series") or "" - if not series: - frappe.throw(_(f"Please set NMB User Series in Company {doc.company}")) - reference = str(series) + "F" + str(doc.name) - if not doc.abbr: - doc.abbr = frappe.get_value("Company", doc.company, "abbr") or "" - doc.bank_reference = reference.replace("-", "").replace("FEE" + doc.abbr, "") - if method == "invoice_submission": - doc.save() - frappe.db.commit() - - -def get_nmb_token(company): - url = frappe.get_value("Company", company, "nmb_url") - if not url: - frappe.throw(_(f"Please set NMB URL in Company {company}")) - url = url + "auth" - username = frappe.get_value("Company", company, "nmb_username") - if not username: - frappe.throw(_(f"Please set NMB User Name in Company {company}")) - password = get_decrypted_password("Company", company, "nmb_password") - if not password: - frappe.throw(_(f"Please set NMB Password in Company {company}")) - data = { - "username": username, - "password": password, - } - for i in range(3): - try: - r = requests.post(url, data=json.dumps(data), timeout=5) - r.raise_for_status() - frappe.logger().debug({"get_nmb_token webhook_success": r.text}) - if json.loads(r.text): - add_log( - request_type="NMB token", - request_url=url, - request_header="no header", - request_body=json.dumps(data), - response_data=json.loads(r.text), - ) - if json.loads(r.text)["status"] == 1: - return json.loads(r.text)["token"] - else: - frappe.throw(json.loads(r.text)) - except Exception as e: - frappe.logger().debug({"get_nmb_token webhook_error": e, "try": i + 1}) - sleep(3 * i + 1) - if i != 2: - continue - else: - raise e - - -def send_nmb(method, data, company): - url = frappe.get_value("Company", company, "nmb_url") - if not url: - frappe.throw(_(f"Please set NMB URL in Company {company}")) - data["token"] = get_nmb_token(company) - url = url + str(method) - for i in range(3): - try: - r = requests.post(url, data=json.dumps(data), timeout=5) - r.raise_for_status() - frappe.logger().debug({"send_nmb webhook_success": r.text}) - if json.loads(r.text): - add_log( - request_type="NMB " + method, - request_url=url, - request_header="no header", - request_body=json.dumps(data), - response_data=json.loads(r.text), - ) - if json.loads(r.text)["status"] == 1: - frappe.msgprint("Response from bank:

" + json.loads(r.text)["description"]) - return json.loads(r.text) - else: - print(json.loads(r.text)["description"]) - if json.loads(r.text)["description"] == "Duplicate Invoice Number": - return json.loads(r.text) - frappe.msgprint("Error detected at bank:

" + json.loads(r.text)["description"]) - frappe.throw(json.loads(r.text)) - except Exception as e: - frappe.logger().debug({"send_nmb webhook_error": e, "try": i + 1}) - sleep(3 * i + 1) - if i != 2: - continue - else: - raise e - - -@frappe.whitelist() -def invoice_submission(doc=None, method=None, fees_name=None): - send_fee_details_to_bank = frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 - - partial_payment = frappe.get_value("Edu Tz Settings", "Edu Tz Settings", "partial_payment") - - # Handle None case and convert to string for bank API - if partial_payment is None or not partial_payment: - partial_payment = "FALSE" - else: - partial_payment = "TRUE" - - if not send_fee_details_to_bank: - return - if not doc and fees_name: - doc = frappe.get_doc("Fees", fees_name) - if not doc.callback_token: - frappe.msgprint( - _("This fee is not set with a token to be sent to the Bank. Generating the token..."), - alert=True, - ) - set_callback_token(doc, "invoice_submission") - series = frappe.get_value("Company", doc.company, "nmb_series") or "" - if not series: - frappe.throw(_(f"Please set NMB User Series in Company {doc.company}")) - data = { - "reference": doc.bank_reference, - "student_name": doc.student_name, - "student_id": doc.student, - "amount": doc.grand_total, - "type": "Fees Invoice", - "code": 10, - "allow_partial": partial_payment, - "callback_url": "https://" - + get_host_name() - + "/api/method/csf_tz.bank_api.receive_callback?token=" - + doc.callback_token, - } - send_nmb("invoice_submission", data, doc.company) + return getattr(api, name) +# nosemgrep: guest-whitelisted-method -- NMB posts payment callbacks unauthenticated @frappe.whitelist(allow_guest=True) def receive_callback(*args, **kwargs): - r = frappe.request - url = url_fix(r.url.replace("+", " ")) - # http_method = r.method - body = r.get_data() - # headers = r.headers - message = {} - if body: - data = body.decode("utf-8") - msgs = ToObject(data) - atr_list = list(msgs.__dict__) - for atr in atr_list: - if getattr(msgs, atr): - message[atr] = getattr(msgs, atr) - else: - frappe.throw("This has no body!") - parsed_url = urlparse(url) - message["fees_token"] = parsed_url[4][6:] - message["doctype"] = "NMB Callback" - nmb_doc = frappe.get_doc(message) - - if nmb_doc.insert(ignore_permissions=True): - frappe.response["status"] = 1 - frappe.response["description"] = "success" - else: - frappe.response["description"] = "insert failed" - frappe.response["http_status_code"] = 409 - - enqueue( - method=make_payment_entry, - queue="short", - timeout=10000, - is_async=True, - kwargs=nmb_doc, - ) - - -def make_payment_entry(method="callback", **kwargs): - for _key, value in kwargs.items(): - nmb_doc = value - doc_info = get_fee_info(nmb_doc.reference) - - nmb_amount = flt(nmb_doc.amount) - frappe.flags.ignore_account_permission = True - if doc_info["doctype"] == "Fees": - if method == "callback": - frappe.set_user("Administrator") - fees_name = doc_info["name"] - bank_reference, receivable_account = frappe.get_value( - "Fees", fees_name, ["bank_reference", "receivable_account"] - ) - if bank_reference == nmb_doc.reference: - payment_entry = get_payment_entry( - "Fees", - fees_name, - party_amount=nmb_amount, - bank_amount=nmb_amount, - party_type="Student", - payment_type="Receive", - ) - payment_entry.update( - { - "payment_date": nmb_doc.timestamp, - "posting_date": nmb_doc.timestamp, - "reference_no": nmb_doc.reference, - "reference_date": nmb_doc.timestamp, - "remarks": "Payment Entry against {} {} via NMB Bank Payment {}".format( - "Fees", fees_name, nmb_doc.reference - ), - "paid_from": receivable_account, - "party_account": receivable_account, - } - ) - payment_entry.flags.ignore_permissions = True - # payment_entry.references = [] - # payment_entry.set_missing_values() - payment_entry.save() - payment_entry.submit() - return nmb_doc - - elif doc_info["doctype"] == "Student Applicant Fees": - doc = frappe.get_doc("Student Applicant Fees", doc_info["name"]) - if not doc.callback_token == nmb_doc.fees_token: - return - # Below remarked after introducing VFD in AV solutions - # jl_rows = [] - # debit_row = dict( - # account=accounts["bank"], - # debit_in_account_currency=nmb_amount, - # account_currency=accounts["currency"], - # cost_center=doc.cost_center, - # ) - # jl_rows.append(debit_row) - - # credit_row_1 = dict( - # account=accounts["income"], - # credit_in_account_currency=nmb_amount, - # account_currency=accounts["currency"], - # cost_center=doc.cost_center, - # ) - # jl_rows.append(credit_row_1) - - # user_remark = ( - # "Journal Entry against {0} {1} via NMB Bank Payment {2}".format( - # "Student Applicant Fees", doc_info["name"], nmb_doc.reference - # ) - # ) - # jv_doc = frappe.get_doc( - # dict( - # doctype="Journal Entry", - # posting_date=nmb_doc.timestamp, - # accounts=jl_rows, - # company=doc.company, - # multi_currency=0, - # user_remark=user_remark, - # ) - # ) - - # jv_doc.flags.ignore_permissions = True - # frappe.flags.ignore_account_permission = True - # jv_doc.save() - # jv_doc.submit() - # jv_url = frappe.utils.get_url_to_form(jv_doc.doctype, jv_doc.name) - # si_msgprint = "Journal Entry Created {1}".format( - # jv_url, jv_doc.name - # ) - # frappe.msgprint(_(si_msgprint)) - frappe.db.set_value("Student Applicant", doc.student, "application_status", "Approved") - return nmb_doc + return get_callback_handler("receive_callback")(*args, **kwargs) +# nosemgrep: guest-whitelisted-method -- NMB validates references unauthenticated @frappe.whitelist(allow_guest=True) def receive_validate_reference(*args, **kwargs): - r = frappe.request - # uri = url_fix(r.url.replace("+"," ")) - # http_method = r.method - body = r.get_data() - # headers = r.headers - message = {} - if body: - data = body.decode("utf-8") - msgs = ToObject(data) - atr_list = list(msgs.__dict__) - for atr in atr_list: - if getattr(msgs, atr): - message[atr] = getattr(msgs, atr) - else: - frappe.throw("This has no body!") - - doc_info = get_fee_info(message["reference"]) - if doc_info["name"]: - doc = frappe.get_doc(doc_info["doctype"], doc_info["name"]) - response = dict( - status=1, - reference=doc.bank_reference, - student_name=doc.student_name, - student_id=doc.student, - amount=doc.grand_total, - type="Fees Invoice", - code=10, - allow_partial="FALSE", - callback_url="https://" - + get_host_name() - + "/api/method/csf_tz.bank_api.receive_callback?token=" - + doc.callback_token, - token=message["token"], - ) - return response - else: - frappe.response["status"] = 0 - frappe.response["description"] = "Not Exist" - - -def cancel_invoice(doc, method): - send_fee_details_to_bank = frappe.get_value("Company", doc.company, "send_fee_details_to_bank") or 0 - if not send_fee_details_to_bank: - return - data = { - "reference": str(doc.bank_reference), - } - message = send_nmb("invoice_cancel", data, doc.company) - frappe.msgprint(str(message)) - - -def reconciliation(doc=None, method=None): - companys = frappe.get_all("Company") - for company in companys: - if not frappe.get_value("Company", company["name"], "nmb_username"): - continue - data = {"reconcile_date": datetime.today().strftime("%d-%m-%Y")} - frappe.msgprint(str(data)) - message = send_nmb("reconcilliation", data, company["name"]) - if message["status"] == 1 and len(message["transactions"]) > 0: - for i in message["transactions"]: - if ( - len( - frappe.get_all( - "NMB Callback", - filters=[ - ["NMB Callback", "reference", "=", i.reference], - ["NMB Callback", "receipt", "=", i.receipt], - ], - fields=["name"], - ) - ) - == 1 - ): - doc_info = get_fee_info(message["reference"]) - if doc_info["name"]: - message["fees_token"] = frappe.get_value( - doc_info["doctype"], doc_info["name"], "callback_token" - ) - message["doctype"] = "NMB Callback" - nmb_doc = frappe.get_doc(message) - enqueue( - method=make_payment_entry, - queue="short", - timeout=10000, - is_async=True, - kwargs=nmb_doc, - ) - - -def get_fee_info(bank_reference): - data = {"name": "", "doctype": ""} - doc_list = frappe.get_all( - "Fees", - filters=[ - ["Fees", "bank_reference", "=", bank_reference], - ["Fees", "docstatus", "=", 1], - ], - fields=["name", "company"], - ) - if len(doc_list): - data["name"] = doc_list[0]["name"] - data["doctype"] = "Fees" - data["company"] = doc_list[0]["company"] - return data - else: - doc_list = frappe.get_all( - "Student Applicant Fees", - filters=[ - ["Student Applicant Fees", "bank_reference", "=", bank_reference], - ["Student Applicant Fees", "docstatus", "=", 1], - ], - fields=["name", "company"], - ) - if len(doc_list): - data["name"] = doc_list[0]["name"] - data["doctype"] = "Student Applicant Fees" - data["company"] = doc_list[0]["company"] - return data - - -def get_fees_default_accounts(company): - data = {"bank": "", "income": "", "currency": ""} - data["currency"] = frappe.get_value("Company", company, "default_currency") or "" - data["bank"] = frappe.get_value("Company", company, "fee_bank_account") or "" - if not data["bank"]: - data["bank"] = frappe.get_value("Company", company, "default_bank_account") or "" - data["income"] = frappe.get_value("Company", company, "student_applicant_fees_revenue_account") or "" - if not data["income"]: - data["bank"] = frappe.get_value("Company", company, "default_income_account") or "" - if not data["bank"]: - frappe.throw(_(f"Please set Fee Bank Account in Company {company}")) - if not data["income"]: - frappe.throw(_(f"Please set Student Applicant Fees Revenue Account in Company {company}")) - return data - - -@frappe.whitelist() -def make_payment_entry_from_call(docname): - nmb_doc = frappe.get_doc("NMB Callback", docname) - make_payment_entry(method="frontend", kwargs=nmb_doc) - - -@frappe.whitelist() -def url_fix(url: str, charset: str = "utf-8") -> str: - """Fixes the URL by encoding the non-ASCII characters. - - Args: - url (str): The URL to fix. - charset (str, optional): The charset to use. Defaults to "utf-8". - - Examples: - >>> url_fix("http://example.com/äöüß") - 'http://example.com/%C3%A4%C3%B6%C3%BC%C3%9F' - - >>> url_fix("http://example.com/漢字") - 'http://example.com/%E6%BC%A2%E5%AD%97' - - >>> url_fix("http://example.com/|pipe") - 'http://example.com/%7Cpipe' - - >>> url_fix("http://example.com/page#fragment with space") - 'http://example.com/page%23fragment%20with%20space' - - >>> url_fix("http://example.com/{curly}) - 'http://example.com/%7Bcurly%7D' - - >>> url_fix("http://example.com/[square]) - 'http://example.com/%5Bsquare%5D' - - """ - s = url.replace("\\", "/") - - if s.startswith("file://") and s[7:8].isalpha() and s[8:10] in (":/", "|/"): - s = f"file:///{s[7:]}" - - url = urlparse(s) - path = quote(url.path, safe="/%+$!*'(),") - qs = quote(url.query, safe=":&%=+$!*'(),") - anchor = quote(url.fragment, safe=":&%=+$!*'(),") - return urlunparse((url.scheme, url.netloc, path, qs, "", anchor)) + return get_callback_handler("receive_validate_reference")(*args, **kwargs) From 99d1ce7df256e46ce454b033d417648acc34dd2c Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:40 +0300 Subject: [PATCH 29/36] refactor: drop education account filters from Company form --- csf_tz/csf_tz/company.js | 18 ------------------ 1 file changed, 18 deletions(-) diff --git a/csf_tz/csf_tz/company.js b/csf_tz/csf_tz/company.js index 3de366b9..52ffe586 100644 --- a/csf_tz/csf_tz/company.js +++ b/csf_tz/csf_tz/company.js @@ -17,24 +17,6 @@ frappe.ui.form.on("Company", { } }; }); - frm.set_query("fee_bank_account", function() { - return { - "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() { - return { - "filters": { - "company": frm.doc.name, - "account_type": "Income Account", - "account_currency": frm.doc.default_currency, - } - }; - }); }, From a6934c30fde0647982e7cc8007366aa541539b8d Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:41 +0300 Subject: [PATCH 30/36] fix: skip Student ledger join when education is not installed --- .../general_ledger_pro/general_ledger_pro.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/csf_tz/csf_tz/report/general_ledger_pro/general_ledger_pro.py b/csf_tz/csf_tz/report/general_ledger_pro/general_ledger_pro.py index 508434d0..9ead015c 100644 --- a/csf_tz/csf_tz/report/general_ledger_pro/general_ledger_pro.py +++ b/csf_tz/csf_tz/report/general_ledger_pro/general_ledger_pro.py @@ -205,7 +205,25 @@ def get_gl_entries(filters, accounting_dimensions): as_dict=1, ) - gl_entries_students = frappe.db.sql( + gl_entries_students = [] + if frappe.db.exists("DocType", "Student"): + gl_entries_students = get_student_gl_entries( + filters, dimension_fields, select_fields, distributed_cost_center_query, order_by_statement + ) + + gl_entries = (gl_entries_all_except_students or []) + (gl_entries_students or []) + + if filters.get("presentation_currency"): + return convert_to_presentation_currency(gl_entries, currency_map, filters.get("company")) + else: + return gl_entries + + +def get_student_gl_entries( + filters, dimension_fields, select_fields, distributed_cost_center_query, order_by_statement +): + """Student party names come from the education app, so this only runs when it is installed.""" + return frappe.db.sql( f""" select gle.name as gl_entry, posting_date, account, party_type, CONCAT(std.first_name, " ", IFNULL(std.middle_name, ''), " ", IFNULL(std.last_name, '')) as party, @@ -223,13 +241,6 @@ def get_gl_entries(filters, accounting_dimensions): as_dict=1, ) - gl_entries = (gl_entries_all_except_students or []) + (gl_entries_students or []) - - if filters.get("presentation_currency"): - return convert_to_presentation_currency(gl_entries, currency_map, filters.get("company")) - else: - return gl_entries - def get_conditions(filters): conditions = [] From d2764316bb8bbef59fccfeef504ccaad35491a22 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:49 +0300 Subject: [PATCH 31/36] refactor: hand Company education section to edu_tz --- .../custom_fields_for_removed_edu_fields_in_csf_tz.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/csf_tz/patches/custom_fields/custom_fields_for_removed_edu_fields_in_csf_tz.py b/csf_tz/patches/custom_fields/custom_fields_for_removed_edu_fields_in_csf_tz.py index 085fe1ce..1cfbc13a 100644 --- a/csf_tz/patches/custom_fields/custom_fields_for_removed_edu_fields_in_csf_tz.py +++ b/csf_tz/patches/custom_fields/custom_fields_for_removed_edu_fields_in_csf_tz.py @@ -140,12 +140,6 @@ def execute(): "insert_after": "auto_create_for_sales_withholding", "label": "Auto Submit For Sales Withholding", }, - { - "fieldname": "education_section", - "fieldtype": "Section Break", - "insert_after": "auto_submit_for_sales_withholding", - "label": "Education", - }, { "fieldname": "bypass_material_request_validation", "fieldtype": "Check", From b4cad578e2e4dc21cb2a1f2714633fbaa849b701 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:58 +0300 Subject: [PATCH 32/36] chore: drop education fields from legacy fixture json --- csf_tz/patches/fixtures/custom_field.json | 12800 +++++++++----------- 1 file changed, 5833 insertions(+), 6967 deletions(-) diff --git a/csf_tz/patches/fixtures/custom_field.json b/csf_tz/patches/fixtures/custom_field.json index 150ec022..520d053c 100644 --- a/csf_tz/patches/fixtures/custom_field.json +++ b/csf_tz/patches/fixtures/custom_field.json @@ -1,6968 +1,5834 @@ [ - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Fees", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "callback_token", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "healthcare_practitioner", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Callback Token", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-10 17:17:36.137062", - "module": null, - "module_def": null, - "name": "Fees-callback_token", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Vehicle Service", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "spare_name", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": null, - "is_system_generated": 0, - "is_virtual": 0, - "label": "Spare Name", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-03 21:04:45.705267", - "module": null, - "module_def": null, - "name": "Vehicle Service-spare_name", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Delivery Note", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "form_sales_invoice", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "patient_name", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Form Sales Invoice", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-28 22:15:11.257297", - "module": null, - "module_def": null, - "name": "Delivery Note-form_sales_invoice", - "no_copy": 0, - "non_negative": 0, - "options": "Sales Invoice", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Vehicle Service", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "quantity", - "fieldtype": "Float", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "spare_name", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Quantity", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-03 21:04:45.981769", - "module": null, - "module_def": null, - "name": "Vehicle Service-quantity", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "2", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Fee Structure", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "default_fee_category", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "student_category", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Default Fee Category", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-31 12:20:15.767419", - "module": null, - "module_def": null, - "name": "Fee Structure-default_fee_category", - "no_copy": 0, - "non_negative": 0, - "options": "Fee Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "is_ignored_in_pending_qty", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "item_code", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Is Ignored In Pending Qty", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-09-23 23:33:08.695837", - "module": null, - "module_def": null, - "name": "Sales Invoice Item-is_ignored_in_pending_qty", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 2, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 2, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice Payment", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "payment_reference", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "amount", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Payment Reference", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-03-24 10:13:38.606282", - "module": null, - "module_def": null, - "name": "Sales Invoice Payment-payment_reference", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "POS Profile", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_1", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "disabled", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-03-05 19:37:44.023203", - "module": null, - "module_def": null, - "name": "POS Profile-column_break_1", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Vehicle Service", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "invoice", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "type", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Invoice", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-03 21:04:46.285638", - "module": null, - "module_def": null, - "name": "Vehicle Service-invoice", - "no_copy": 0, - "non_negative": 0, - "options": "Purchase Invoice", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Program Fee", - "fetch_from": "fee_structure.default_fee_category", - "fetch_if_empty": 0, - "fieldname": "default_fee_category", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "due_date", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Default Fee Category", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-31 12:32:27.126255", - "module": null, - "module_def": null, - "name": "Program Fee-default_fee_category", - "no_copy": 0, - "non_negative": 0, - "options": "Fee Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Program", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "fees", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "courses", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Fees", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-31 09:39:52.330874", - "module": null, - "module_def": null, - "name": "Program-fees", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Fees", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "bank_reference", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "send_payment_request", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Bank Reference", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-10 17:13:35.213803", - "module": null, - "module_def": null, - "name": "Fees-bank_reference", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Student Applicant", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "student_applicant_fee", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "paid", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Student Applicant Fee", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-08 18:39:21.143298", - "module": null, - "module_def": null, - "name": "Student Applicant-student_applicant_fee", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "POS Profile", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "electronic_fiscal_device", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "is_not_vfd_invoice", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Electronic Fiscal Device", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-03-05 19:37:44.325865", - "module": null, - "module_def": null, - "name": "POS Profile-electronic_fiscal_device", - "no_copy": 0, - "non_negative": 0, - "options": "Electronic Fiscal Device", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Vehicle Fine Record", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "fully_paid", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "offence", - "is_system_generated": 0, - "is_virtual": 0, - "label": "FULLY PAID", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-12-10 10:50:48.832611", - "module": null, - "module_def": null, - "name": "Vehicle Fine Record-fully_paid", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "witholding_tax_rate_on_purchase", - "fieldtype": "Float", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "stock_uom", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Withholding Tax Rate on Purchase", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-05-05 12:33:29.224597", - "module": null, - "module_def": null, - "name": "Item-witholding_tax_rate_on_purchase", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Student Applicant", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "bank_reference", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "student_applicant_fee", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Bank Reference", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-08 18:39:21.460847", - "module": null, - "module_def": null, - "name": "Student Applicant-bank_reference", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "withholding_tax_rate_on_sales", - "fieldtype": "Float", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "witholding_tax_rate_on_purchase", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Withholding Tax Rate on Sales", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-06 01:17:58.305162", - "module": null, - "module_def": null, - "name": "Item-withholding_tax_rate_on_sales", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Fees", - "fetch_from": "company.abbr", - "fetch_if_empty": 0, - "fieldname": "abbr", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "company", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Abbr", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-22 13:06:42.155142", - "module": null, - "module_def": null, - "name": "Fees-abbr", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "1", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Print Settings", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "compact_item_print", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "with_letterhead", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Compact Item Print", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-04-29 12:32:28.701703", - "module": null, - "module_def": null, - "name": "Print Settings-compact_item_print", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Reconciliation Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "material_request", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "amount", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Material Request", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-24 22:55:04.777565", - "module": null, - "module_def": null, - "name": "Stock Reconciliation Item-material_request", - "no_copy": 0, - "non_negative": 0, - "options": "Material Request", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "0", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "allow_over_sell", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "customer_item_code", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Allow Over Sell", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-19 17:42:30.386345", - "module": null, - "module_def": null, - "name": "Sales Invoice Item-allow_over_sell", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Payment Entry Reference", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "section_break_9", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "exchange_rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:23:11.532502", - "module": null, - "module_def": null, - "name": "Payment Entry Reference-section_break_9", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Vehicle Log", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_11", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "odometer", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-20 16:46:46.909608", - "module": null, - "module_def": null, - "name": "Vehicle Log-column_break_11", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Order", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "posting_date", - "fieldtype": "Date", - "hidden": 1, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "transaction_date", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Posting Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-03-13 17:00:30.266085", - "module": null, - "module_def": null, - "name": "Sales Order-posting_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 1, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Payment Entry Reference", - "fetch_from": "reference_name.posting_date", - "fetch_if_empty": 0, - "fieldname": "posting_date", - "fieldtype": "Date", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "section_break_9", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Posting Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:23:23.225925", - "module": null, - "module_def": null, - "name": "Payment Entry Reference-posting_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "Today", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Purchase Order", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "posting_date", - "fieldtype": "Date", - "hidden": 1, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "transaction_date", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Posting Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-03-13 16:59:13.689530", - "module": null, - "module_def": null, - "name": "Purchase Order-posting_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Vehicle Log", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "trip_destination", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_preview": 0, - "in_standard_filter": 1, - "insert_after": "column_break_11", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Trip Destination", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-20 16:46:47.646836", - "module": null, - "module_def": null, - "name": "Vehicle Log-trip_destination", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Payment Entry Reference", - "fetch_from": "reference_name.from_date", - "fetch_if_empty": 0, - "fieldname": "start_date", - "fieldtype": "Date", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "posting_date", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Start Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:23:57.230372", - "module": null, - "module_def": null, - "name": "Payment Entry Reference-start_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Vehicle Log", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "destination_description", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "trip_destination", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Destination Description", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-20 16:46:48.115791", - "module": null, - "module_def": null, - "name": "Vehicle Log-destination_description", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Student Applicant", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "fee_structure", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "application_date", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Fee Structure", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-08 18:38:13.430256", - "module": null, - "module_def": null, - "name": "Student Applicant-fee_structure", - "no_copy": 0, - "non_negative": 0, - "options": "Fee Structure", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "BOM", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "warehouses", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "image", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Warehouses", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-01-21 06:42:52.549662", - "module": null, - "module_def": null, - "name": "BOM-warehouses", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Operation", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "image", - "fieldtype": "Attach Image", - "hidden": 1, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "description", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Image", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-10-17 02:40:39.330498", - "module": null, - "module_def": null, - "name": "Operation-image", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Payment Entry Reference", - "fetch_from": "reference_name.to_date", - "fetch_if_empty": 0, - "fieldname": "end_date", - "fieldtype": "Date", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "start_date", - "is_system_generated": 0, - "is_virtual": 0, - "label": "End Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:23:36.497233", - "module": null, - "module_def": null, - "name": "Payment Entry Reference-end_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Student Applicant", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "program_enrollment", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "fee_structure", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Program Enrollment", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-08 21:42:34.633789", - "module": null, - "module_def": null, - "name": "Student Applicant-program_enrollment", - "no_copy": 0, - "non_negative": 0, - "options": "Program Enrollment", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "BOM", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "source_warehouse", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "warehouses", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Source Warehouse", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-01-21 06:54:48.841522", - "module": null, - "module_def": null, - "name": "BOM-source_warehouse", - "no_copy": 0, - "non_negative": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "excisable_item", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "is_stock_item", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Excisable Item", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-03-06 07:27:57.882935", - "module": null, - "module_def": null, - "name": "Item-excisable_item", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Address", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "tax_category", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "fax", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Tax Category", - "length": 0, - "mandatory_depends_on": null, - "modified": "2018-12-28 22:29:21.828090", - "module": null, - "module_def": null, - "name": "Address-tax_category", - "no_copy": 0, - "non_negative": 0, - "options": "Tax Category", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "eval:doc.docstatus == 0", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Reconciliation", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "sort_items", - "fieldtype": "Button", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "items", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Sort Items", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-17 23:53:18.064909", - "module": null, - "module_def": null, - "name": "Stock Reconciliation-sort_items", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Employee", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "old_employee_id", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "image", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Old Employee ID", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-24 11:29:02.966955", - "module": null, - "module_def": null, - "name": "Employee-old_employee_id", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "BOM", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "fg_warehouse", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "source_warehouse", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Target Warehouse", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-01-21 06:44:08.776914", - "module": null, - "module_def": null, - "name": "BOM-fg_warehouse", - "no_copy": 0, - "non_negative": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "0", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Print Settings", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "print_taxes_with_zero_amount", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "allow_print_for_cancelled", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Print taxes with zero amount", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-04-29 12:32:28.864970", - "module": null, - "module_def": null, - "name": "Print Settings-print_taxes_with_zero_amount", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "BOM", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_15", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "fg_warehouse", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-01-21 06:55:01.821947", - "module": null, - "module_def": null, - "name": "BOM-column_break_15", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "BOM", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "wip_warehouse", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "column_break_15", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Work-in-Progress Warehouse", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-01-21 06:42:52.960942", - "module": null, - "module_def": null, - "name": "BOM-wip_warehouse", - "no_copy": 0, - "non_negative": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "BOM", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "scrap_warehouse", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "wip_warehouse", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Scrap Warehouse", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-01-21 06:44:09.157050", - "module": null, - "module_def": null, - "name": "BOM-scrap_warehouse", - "no_copy": 0, - "non_negative": 0, - "options": "Warehouse", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "section_break_12", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "company_description", - "is_system_generated": 0, - "is_virtual": 0, - "label": "", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:22:27.862997", - "module": null, - "module_def": null, - "name": "Company-section_break_12", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "0", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Address", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "is_your_company_address", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "linked_with", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Is Your Company Address", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-10-14 17:41:40.878179", - "module": null, - "module_def": null, - "name": "Address-is_your_company_address", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Student", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "bank", - "fieldtype": "Select", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "student_applicant", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Bank", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-21 02:47:58.927966", - "module": null, - "module_def": null, - "name": "Student-bank", - "no_copy": 0, - "non_negative": 0, - "options": "\n NMB Bank", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Account", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "item", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "include_in_gross", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Expense Item", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-16 00:19:13.785448", - "module": null, - "module_def": null, - "name": "Account-item", - "no_copy": 0, - "non_negative": 0, - "options": "Item", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "company_bank_details", - "fieldtype": "Text", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "section_break_12", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Company Bank Details", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:22:19.201911", - "module": null, - "module_def": null, - "name": "Company-company_bank_details", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "eval: !in_list(frappe.user_roles, \"Healthcare Receptionist\")", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "previous_invoice_number", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "is_return", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Previous Invoice Number", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-05 14:33:05.352542", - "module": null, - "module_def": null, - "name": "Sales Invoice-previous_invoice_number", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "vrn", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "website", - "is_system_generated": 0, - "is_virtual": 0, - "label": "VRN", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:22:02.995304", - "module": null, - "module_def": null, - "name": "Company-vrn", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Contact", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "is_billing_contact", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "is_primary_contact", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Is Billing Contact", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-02 11:00:03.432994", - "module": null, - "module_def": null, - "name": "Contact-is_billing_contact", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Supplier", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "vrn", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "tax_id", - "is_system_generated": 0, - "is_virtual": 0, - "label": "VRN", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-01-04 10:14:48.458823", - "module": null, - "module_def": null, - "name": "Supplier-vrn", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "tin", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "vrn", - "is_system_generated": 0, - "is_virtual": 0, - "label": "TIN", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:22:10.324100", - "module": null, - "module_def": null, - "name": "Company-tin", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "authotp", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "amended_from", - "is_system_generated": 0, - "is_virtual": 0, - "label": "AuthOTP", - "length": 0, - "mandatory_depends_on": null, - "modified": "2023-03-10 22:33:10.290382", - "module": null, - "module_def": null, - "name": "Sales Invoice-authotp", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "p_o_box", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "tin", - "is_system_generated": 0, - "is_virtual": 0, - "label": "P O Box", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-05-07 17:49:19.841962", - "module": null, - "module_def": null, - "name": "Company-p_o_box", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_sn02w", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "authotp_method", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2023-03-10 22:36:54.210165", - "module": null, - "module_def": null, - "name": "Sales Invoice-column_break_sn02w", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry Detail", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "item_weight_details", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "sample_quantity", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Item Weight Details", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-30 15:34:01.228484", - "module": null, - "module_def": null, - "name": "Stock Entry Detail-item_weight_details", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "city", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "p_o_box", - "is_system_generated": 0, - "is_virtual": 0, - "label": "City", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-05-07 17:49:20.430328", - "module": null, - "module_def": null, - "name": "Company-city", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry Detail", - "fetch_from": "item_code.weight_per_unit", - "fetch_if_empty": 0, - "fieldname": "weight_per_unit", - "fieldtype": "Float", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "item_weight_details", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Weight Per Unit", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-30 15:34:01.669365", - "module": null, - "module_def": null, - "name": "Stock Entry Detail-weight_per_unit", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "plot_number", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "city", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Plot Number", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-05-07 13:58:31.627080", - "module": null, - "module_def": null, - "name": "Company-plot_number", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry Detail", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "total_weight", - "fieldtype": "Float", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "weight_per_unit", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Total Weight", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-30 15:34:02.259714", - "module": null, - "module_def": null, - "name": "Stock Entry Detail-total_weight", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "block_number", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "plot_number", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Block Number", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-05-07 13:58:32.111317", - "module": null, - "module_def": null, - "name": "Company-block_number", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry Detail", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_32", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "total_weight", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-30 15:34:02.814827", - "module": null, - "module_def": null, - "name": "Stock Entry Detail-column_break_32", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "street", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "block_number", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Street", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-05-07 13:58:32.664481", - "module": null, - "module_def": null, - "name": "Company-street", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry Detail", - "fetch_from": "item_code.weight_uom", - "fetch_if_empty": 0, - "fieldname": "weight_uom", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "column_break_32", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Weight UOM", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-30 15:34:03.282211", - "module": null, - "module_def": null, - "name": "Stock Entry Detail-weight_uom", - "no_copy": 0, - "non_negative": 0, - "options": "UOM", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Purchase Invoice Item", - "fetch_from": "item_code.witholding_tax_rate_on_purchase", - "fetch_if_empty": 1, - "fieldname": "withholding_tax_rate", - "fieldtype": "Float", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "item_tax_template", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Withholding Tax Rate", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-17 01:03:26.025362", - "module": null, - "module_def": null, - "name": "Purchase Invoice Item-withholding_tax_rate", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Purchase Invoice Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "withholding_tax_entry", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "withholding_tax_rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Withholding Tax Entry", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-17 16:37:56.527525", - "module": null, - "module_def": null, - "name": "Purchase Invoice Item-withholding_tax_entry", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Employee", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "heslb_f4_index_number", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "national_identity", - "is_system_generated": 0, - "is_virtual": 0, - "label": "HESLB F4 Index Number", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-08-16 17:52:02.294599", - "module": null, - "module_def": null, - "name": "Employee-heslb_f4_index_number", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice Item", - "fetch_from": "item_code.withholding_tax_rate_on_sales", - "fetch_if_empty": 0, - "fieldname": "withholding_tax_rate", - "fieldtype": "Percent", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "item_tax_template", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Withholding Tax Rate", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 00:57:44.450609", - "module": null, - "module_def": null, - "name": "Sales Invoice Item-withholding_tax_rate", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Order", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "default_item_discount", - "fieldtype": "Percent", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "scan_barcode", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Default Item Discount", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-02-04 08:16:47.825862", - "module": null, - "module_def": null, - "name": "Sales Order-default_item_discount", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "withholding_tax_entry", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "withholding_tax_rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Withholding Tax Entry", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-06 01:34:59.329482", - "module": null, - "module_def": null, - "name": "Sales Invoice Item-withholding_tax_entry", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "eval: doc.stock_entry_type == \"Send to Warehouse\"", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "final_destination", - "fieldtype": "Select", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "target_address_display", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Final Destination", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-29 18:07:10.893479", - "module": null, - "module_def": null, - "name": "Stock Entry-final_destination", - "no_copy": 0, - "non_negative": 0, - "options": "", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "authotp_validated", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "column_break_sn02w", - "is_system_generated": 0, - "is_virtual": 0, - "label": "AuthOTP Validated", - "length": 0, - "mandatory_depends_on": null, - "modified": "2023-03-10 22:36:54.540607", - "module": null, - "module_def": null, - "name": "Sales Invoice-authotp_validated", - "no_copy": 1, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Material Request Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "stock_reconciliation", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "lead_time_date", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Stock Reconciliation", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-24 22:47:46.584890", - "module": null, - "module_def": null, - "name": "Material Request Item-stock_reconciliation", - "no_copy": 0, - "non_negative": 0, - "options": "Stock Reconciliation", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Fees", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "vehicle", - "is_system_generated": 1, - "is_virtual": 0, - "label": "From Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-18 13:10:14.387470", - "module": null, - "module_def": null, - "name": "Fees-from_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Fees", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "from_date", - "is_system_generated": 1, - "is_virtual": 0, - "label": "To Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-18 13:10:14.889080", - "module": null, - "module_def": null, - "name": "Fees-to_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "0", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "max_records_in_dialog", - "fieldtype": "Int", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "sales_monthly_history", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Max records in Dialog", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-05-07 08:55:04.364472", - "module": null, - "module_def": null, - "name": "Company-max_records_in_dialog", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "total_net_weight", - "fieldtype": "Float", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "section_break_19", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Total Net Weight", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-30 14:59:16.406735", - "module": null, - "module_def": null, - "name": "Stock Entry-total_net_weight", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Journal Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "from_date", - "fieldtype": "Date", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "auto_repeat", - "is_system_generated": 0, - "is_virtual": 0, - "label": "From Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:22:47.765536", - "module": null, - "module_def": null, - "name": "Journal Entry-from_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "allow_override_net_rate", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "net_rate", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Allow Override Net Rate", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-02 17:20:45.022013", - "module": null, - "module_def": null, - "name": "Sales Invoice Item-allow_override_net_rate", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 1, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Journal Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "to_date", - "fieldtype": "Date", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "from_date", - "is_system_generated": 0, - "is_virtual": 0, - "label": "To Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:22:38.720845", - "module": null, - "module_def": null, - "name": "Journal Entry-to_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": "Value Added Tax Registration Number (VAT RN = VRN)", - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Customer", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "vrn", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "tax_id", - "is_system_generated": 0, - "is_virtual": 0, - "label": "VRN", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:21:41.980276", - "module": null, - "module_def": null, - "name": "Customer-vrn", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "withholding_section", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "default_expense_claim_payable_account", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Withholding", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-05 22:35:42.216333", - "module": null, - "module_def": null, - "name": "Company-withholding_section", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "default_withholding_payable_account", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "withholding_section", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Default Withholding Payable Account", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-17 00:37:50.184521", - "module": null, - "module_def": null, - "name": "Company-default_withholding_payable_account", - "no_copy": 0, - "non_negative": 0, - "options": "Account", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "auto_create_for_purchase_withholding", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "default_withholding_payable_account", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Auto Create For Purchase Withholding", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-01 23:21:15.437803", - "module": null, - "module_def": null, - "name": "Company-auto_create_for_purchase_withholding", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "0", - "depends_on": "auto_create_for_purchase_withholding", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "auto_submit_for_purchase_withholding", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "auto_create_for_purchase_withholding", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Auto Submit For Purchase Withholding", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-05 22:48:17.854167", - "module": null, - "module_def": null, - "name": "Company-auto_submit_for_purchase_withholding", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "student_applicant_fees_revenue_account", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "fee_bank_account", - "is_system_generated": 1, - "is_virtual": 0, - "label": "Student Applicant Fees Revenue Account", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-01 01:36:24.342359", - "module": null, - "module_def": null, - "name": "Company-student_applicant_fees_revenue_account", - "no_copy": 0, - "non_negative": 0, - "options": "Account", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_55", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "auto_submit_for_purchase_withholding", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-05 22:48:18.181399", - "module": null, - "module_def": null, - "name": "Company-column_break_55", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "eval: !in_list(frappe.user_roles, \"Healthcare Receptionist\")", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "default_item_discount", - "fieldtype": "Percent", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "update_stock", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Default Item Discount", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-01-09 19:30:39.855293", - "module": null, - "module_def": null, - "name": "Sales Invoice-default_item_discount", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_60", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "student_applicant_fees_revenue_account", - "is_system_generated": 1, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-18 15:47:26.978439", - "module": null, - "module_def": null, - "name": "Company-column_break_60", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "default_withholding_receivable_account", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "column_break_55", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Default Withholding Receivable Account", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-05 22:48:18.477944", - "module": null, - "module_def": null, - "name": "Company-default_withholding_receivable_account", - "no_copy": 0, - "non_negative": 0, - "options": "Account", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": "company.default_item_tax_template", - "fetch_if_empty": 1, - "fieldname": "default_item_tax_template", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "default_item_discount", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Default Item Tax Template", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-02-08 22:50:03.490542", - "module": null, - "module_def": null, - "name": "Sales Invoice-default_item_tax_template", - "no_copy": 0, - "non_negative": 0, - "options": "Item Tax Template", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "auto_create_for_sales_withholding", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "default_withholding_receivable_account", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Auto Create For Sales Withholding", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-01 23:21:15.814351", - "module": null, - "module_def": null, - "name": "Company-auto_create_for_sales_withholding", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "section_break_80", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "default_item_tax_template", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-02-08 22:50:04.586234", - "module": null, - "module_def": null, - "name": "Sales Invoice-section_break_80", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "0", - "depends_on": "auto_create_for_sales_withholding", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "auto_submit_for_sales_withholding", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "auto_create_for_sales_withholding", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Auto Submit For Sales Withholding", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-05 22:48:18.789116", - "module": null, - "module_def": null, - "name": "Company-auto_submit_for_sales_withholding", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "education_section", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "auto_submit_for_sales_withholding", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Education", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-18 15:46:30.057666", - "module": null, - "module_def": null, - "name": "Company-education_section", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "0", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "send_fee_details_to_bank", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "education_section", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Send Fee details to Bank", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-18 15:46:30.517510", - "module": null, - "module_def": null, - "name": "Company-send_fee_details_to_bank", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "fee_bank_account", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "send_fee_details_to_bank", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Fee Bank Account", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-01 01:35:36.035810", - "module": null, - "module_def": null, - "name": "Company-fee_bank_account", - "no_copy": 0, - "non_negative": 0, - "options": "Account", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "transporter_info", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "letter_head", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Transporter Info", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:00.237708", - "module": null, - "module_def": null, - "name": "Stock Entry-transporter_info", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "transporter", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "transporter_info", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Transporter", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:00.755674", - "module": null, - "module_def": null, - "name": "Stock Entry-transporter", - "no_copy": 0, - "non_negative": 0, - "options": "Supplier", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "driver", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "transporter", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Driver", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:01.308481", - "module": null, - "module_def": null, - "name": "Stock Entry-driver", - "no_copy": 0, - "non_negative": 0, - "options": "Driver", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "send_fee_details_to_bank", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "nmb_series", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "column_break_60", - "is_system_generated": 0, - "is_virtual": 0, - "label": "NMB Series", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-10 16:10:54.189754", - "module": null, - "module_def": null, - "name": "Company-nmb_series", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "transport_receipt_no", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "driver", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Transport Receipt No", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:01.837481", - "module": null, - "module_def": null, - "name": "Stock Entry-transport_receipt_no", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "send_fee_details_to_bank", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "nmb_username", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "nmb_series", - "is_system_generated": 0, - "is_virtual": 0, - "label": "NMB User Name", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-28 03:54:27.672291", - "module": null, - "module_def": null, - "name": "Company-nmb_username", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "vehicle_no", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "transport_receipt_no", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Vehicle No", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:02.364744", - "module": null, - "module_def": null, - "name": "Stock Entry-vehicle_no", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "send_fee_details_to_bank", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "nmb_password", - "fieldtype": "Password", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "nmb_username", - "is_system_generated": 0, - "is_virtual": 0, - "label": "NMB Password", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-28 03:54:28.347794", - "module": null, - "module_def": null, - "name": "Company-nmb_password", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_69", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "vehicle_no", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:02.892782", - "module": null, - "module_def": null, - "name": "Stock Entry-column_break_69", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "send_fee_details_to_bank", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "nmb_url", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "nmb_password", - "is_system_generated": 0, - "is_virtual": 0, - "label": "NMb URL", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-10 16:11:58.119081", - "module": null, - "module_def": null, - "name": "Company-nmb_url", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "transporter_name", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "column_break_69", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Transporter Name", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:03.373538", - "module": null, - "module_def": null, - "name": "Stock Entry-transporter_name", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": "driver.full_name", - "fetch_if_empty": 0, - "fieldname": "driver_name", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "transporter_name", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Driver Name", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:03.900970", - "module": null, - "module_def": null, - "name": "Stock Entry-driver_name", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Stock Entry", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "transport_receipt_date", - "fieldtype": "Date", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "driver_name", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Transport Receipt Date", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-23 16:43:04.427622", - "module": null, - "module_def": null, - "name": "Stock Entry-transport_receipt_date", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "price_reduction", - "fieldtype": "Currency", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "base_net_total", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Total Price Reduction Amount", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-28 02:56:42.240626", - "module": null, - "module_def": null, - "name": "Sales Invoice-price_reduction", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Item", - "fetch_from": "", - "fetch_if_empty": 0, - "fieldname": "default_tax_template", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "item_tax_section_break", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Default Tax Template", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-03-10 12:04:59.499730", - "module": null, - "module_def": null, - "name": "Item-default_tax_template", - "no_copy": 0, - "non_negative": 0, - "options": "Item Tax Template", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice Item", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "delivery_status", - "fieldtype": "Select", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "sales_order", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Delivery Status", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-19 18:17:11.595366", - "module": null, - "module_def": null, - "name": "Sales Invoice Item-delivery_status", - "no_copy": 0, - "non_negative": 0, - "options": "\nNot Delivered\nPart Delivered\nDelivered\nOver Delivered", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "bypass_material_request_validation", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "stock_adjustment_account", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Bypass Material Request validation on Stock Entry", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-09-10 16:46:34.320822", - "module": null, - "module_def": null, - "name": "Company-bypass_material_request_validation", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "default_item_tax_template", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "default_in_transit_warehouse", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Default Item Tax Template", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-03-31 08:45:00.684605", - "module": null, - "module_def": null, - "name": "Company-default_item_tax_template", - "no_copy": 0, - "non_negative": 0, - "options": "Item Tax Template", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "1", - "depends_on": null, - "description": "For Sales Invoices", - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Company", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "enabled_auto_create_delivery_notes", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "column_break_32", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Enabled Auto Create Delivery Notes", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-07-17 22:30:49.037893", - "module": null, - "module_def": null, - "name": "Company-enabled_auto_create_delivery_notes", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Purchase Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "reference", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "language", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Reference", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-01-20 13:49:43.238770", - "module": null, - "module_def": null, - "name": "Purchase Invoice-reference", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": "eval: !in_list(frappe.user_roles, \"Healthcare Receptionist\")", - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "statutory_details", - "fieldtype": "Section Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "po_date", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Statutory Details", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:25:08.716714", - "module": null, - "module_def": null, - "name": "Sales Invoice-statutory_details", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "tra_control_number", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "statutory_details", - "is_system_generated": 0, - "is_virtual": 0, - "label": "TRA Control Number", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:24:51.344893", - "module": null, - "module_def": null, - "name": "Sales Invoice-tra_control_number", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "witholding_tax_certificate_number", - "fieldtype": "Data", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "tra_control_number", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Witholding Tax Certificate Number", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:23:45.693752", - "module": null, - "module_def": null, - "name": "Sales Invoice-witholding_tax_certificate_number", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "column_break_29", - "fieldtype": "Column Break", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "witholding_tax_certificate_number", - "is_system_generated": 0, - "is_virtual": 0, - "label": null, - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:24:28.702495", - "module": null, - "module_def": null, - "name": "Sales Invoice-column_break_29", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": "pos_profile.electronic_fiscal_device", - "fetch_if_empty": 1, - "fieldname": "electronic_fiscal_device", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "column_break_29", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Electronic Fiscal Device", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-03-05 16:05:01.332647", - "module": null, - "module_def": null, - "name": "Sales Invoice-electronic_fiscal_device", - "no_copy": 0, - "non_negative": 0, - "options": "Electronic Fiscal Device", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "efd_z_report", - "fieldtype": "Link", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "electronic_fiscal_device", - "is_system_generated": 0, - "is_virtual": 0, - "label": "EFD Z Report", - "length": 0, - "mandatory_depends_on": null, - "modified": "2019-12-20 19:24:07.925266", - "module": null, - "module_def": null, - "name": "Sales Invoice-efd_z_report", - "no_copy": 0, - "non_negative": 0, - "options": "EFD Z Report", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "excise_duty_applicable", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "col_break23", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Excise Duty Applicable", - "length": 0, - "mandatory_depends_on": null, - "modified": "2021-03-06 07:42:46.718597", - "module": null, - "module_def": null, - "name": "Sales Invoice-excise_duty_applicable", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": "1", - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": "", - "fetch_if_empty": 0, - "fieldname": "enabled_auto_create_delivery_notes", - "fieldtype": "Check", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 0, - "insert_after": "connections_tab", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Enabled Auto Create Delivery Notes", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-08-13 18:32:53.375956", - "module": null, - "module_def": null, - "name": "Sales Invoice-enabled_auto_create_delivery_notes", - "no_copy": 0, - "non_negative": 0, - "options": null, - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 0, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 0, - "unique": 0, - "width": null - }, - { - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": null, - "columns": 0, - "default": null, - "depends_on": null, - "description": null, - "docstatus": 0, - "doctype": "Custom Field", - "dt": "Sales Invoice", - "fetch_from": null, - "fetch_if_empty": 0, - "fieldname": "delivery_status", - "fieldtype": "Select", - "hidden": 0, - "hide_border": 0, - "hide_days": 0, - "hide_seconds": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_preview": 0, - "in_standard_filter": 1, - "insert_after": "demo_done", - "is_system_generated": 0, - "is_virtual": 0, - "label": "Delivery Status", - "length": 0, - "mandatory_depends_on": null, - "modified": "2020-06-19 18:16:07.249867", - "module": null, - "module_def": null, - "name": "Sales Invoice-delivery_status", - "no_copy": 0, - "non_negative": 0, - "options": "\nNot Delivered\nPart Delivered\nDelivered\nOver Delivered", - "permlevel": 0, - "precision": null, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": null, - "read_only": 1, - "read_only_depends_on": null, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "translatable": 1, - "unique": 0, - "width": null - } -] + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Vehicle Service", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "spare_name", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": null, + "is_system_generated": 0, + "is_virtual": 0, + "label": "Spare Name", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-09-03 21:04:45.705267", + "module": null, + "module_def": null, + "name": "Vehicle Service-spare_name", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Delivery Note", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "form_sales_invoice", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "patient_name", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Form Sales Invoice", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-28 22:15:11.257297", + "module": null, + "module_def": null, + "name": "Delivery Note-form_sales_invoice", + "no_copy": 0, + "non_negative": 0, + "options": "Sales Invoice", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Vehicle Service", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "quantity", + "fieldtype": "Float", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "spare_name", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Quantity", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-09-03 21:04:45.981769", + "module": null, + "module_def": null, + "name": "Vehicle Service-quantity", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": "2", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "is_ignored_in_pending_qty", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "item_code", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Is Ignored In Pending Qty", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-09-23 23:33:08.695837", + "module": null, + "module_def": null, + "name": "Sales Invoice Item-is_ignored_in_pending_qty", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 2, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 2, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice Payment", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "payment_reference", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "amount", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Payment Reference", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-03-24 10:13:38.606282", + "module": null, + "module_def": null, + "name": "Sales Invoice Payment-payment_reference", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_1", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "disabled", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-03-05 19:37:44.023203", + "module": null, + "module_def": null, + "name": "POS Profile-column_break_1", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Vehicle Service", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "invoice", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "type", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Invoice", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-09-03 21:04:46.285638", + "module": null, + "module_def": null, + "name": "Vehicle Service-invoice", + "no_copy": 0, + "non_negative": 0, + "options": "Purchase Invoice", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "POS Profile", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "electronic_fiscal_device", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "is_not_vfd_invoice", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Electronic Fiscal Device", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-03-05 19:37:44.325865", + "module": null, + "module_def": null, + "name": "POS Profile-electronic_fiscal_device", + "no_copy": 0, + "non_negative": 0, + "options": "Electronic Fiscal Device", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Vehicle Fine Record", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "fully_paid", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "offence", + "is_system_generated": 0, + "is_virtual": 0, + "label": "FULLY PAID", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-12-10 10:50:48.832611", + "module": null, + "module_def": null, + "name": "Vehicle Fine Record-fully_paid", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "witholding_tax_rate_on_purchase", + "fieldtype": "Float", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "stock_uom", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Withholding Tax Rate on Purchase", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-05-05 12:33:29.224597", + "module": null, + "module_def": null, + "name": "Item-witholding_tax_rate_on_purchase", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "withholding_tax_rate_on_sales", + "fieldtype": "Float", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "witholding_tax_rate_on_purchase", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Withholding Tax Rate on Sales", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-06 01:17:58.305162", + "module": null, + "module_def": null, + "name": "Item-withholding_tax_rate_on_sales", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "1", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Print Settings", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "compact_item_print", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "with_letterhead", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Compact Item Print", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-04-29 12:32:28.701703", + "module": null, + "module_def": null, + "name": "Print Settings-compact_item_print", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Reconciliation Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "material_request", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "amount", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Material Request", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-24 22:55:04.777565", + "module": null, + "module_def": null, + "name": "Stock Reconciliation Item-material_request", + "no_copy": 0, + "non_negative": 0, + "options": "Material Request", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "allow_over_sell", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "customer_item_code", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Allow Over Sell", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-19 17:42:30.386345", + "module": null, + "module_def": null, + "name": "Sales Invoice Item-allow_over_sell", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Payment Entry Reference", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "section_break_9", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "exchange_rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:23:11.532502", + "module": null, + "module_def": null, + "name": "Payment Entry Reference-section_break_9", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Vehicle Log", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_11", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "odometer", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-20 16:46:46.909608", + "module": null, + "module_def": null, + "name": "Vehicle Log-column_break_11", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Order", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "posting_date", + "fieldtype": "Date", + "hidden": 1, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "transaction_date", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Posting Date", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-03-13 17:00:30.266085", + "module": null, + "module_def": null, + "name": "Sales Order-posting_date", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 1, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Payment Entry Reference", + "fetch_from": "reference_name.posting_date", + "fetch_if_empty": 0, + "fieldname": "posting_date", + "fieldtype": "Date", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "section_break_9", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Posting Date", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:23:23.225925", + "module": null, + "module_def": null, + "name": "Payment Entry Reference-posting_date", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "Today", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Purchase Order", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "posting_date", + "fieldtype": "Date", + "hidden": 1, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "transaction_date", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Posting Date", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-03-13 16:59:13.689530", + "module": null, + "module_def": null, + "name": "Purchase Order-posting_date", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Vehicle Log", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "trip_destination", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 1, + "in_preview": 0, + "in_standard_filter": 1, + "insert_after": "column_break_11", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Trip Destination", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-20 16:46:47.646836", + "module": null, + "module_def": null, + "name": "Vehicle Log-trip_destination", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 1, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Payment Entry Reference", + "fetch_from": "reference_name.from_date", + "fetch_if_empty": 0, + "fieldname": "start_date", + "fieldtype": "Date", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "posting_date", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Start Date", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:23:57.230372", + "module": null, + "module_def": null, + "name": "Payment Entry Reference-start_date", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Vehicle Log", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "destination_description", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "trip_destination", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Destination Description", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-20 16:46:48.115791", + "module": null, + "module_def": null, + "name": "Vehicle Log-destination_description", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "BOM", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "warehouses", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "image", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Warehouses", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-01-21 06:42:52.549662", + "module": null, + "module_def": null, + "name": "BOM-warehouses", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Operation", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "image", + "fieldtype": "Attach Image", + "hidden": 1, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "description", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Image", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-10-17 02:40:39.330498", + "module": null, + "module_def": null, + "name": "Operation-image", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Payment Entry Reference", + "fetch_from": "reference_name.to_date", + "fetch_if_empty": 0, + "fieldname": "end_date", + "fieldtype": "Date", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "start_date", + "is_system_generated": 0, + "is_virtual": 0, + "label": "End Date", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:23:36.497233", + "module": null, + "module_def": null, + "name": "Payment Entry Reference-end_date", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "BOM", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "source_warehouse", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "warehouses", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Source Warehouse", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-01-21 06:54:48.841522", + "module": null, + "module_def": null, + "name": "BOM-source_warehouse", + "no_copy": 0, + "non_negative": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "excisable_item", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "is_stock_item", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Excisable Item", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-03-06 07:27:57.882935", + "module": null, + "module_def": null, + "name": "Item-excisable_item", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Address", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "tax_category", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "fax", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Tax Category", + "length": 0, + "mandatory_depends_on": null, + "modified": "2018-12-28 22:29:21.828090", + "module": null, + "module_def": null, + "name": "Address-tax_category", + "no_copy": 0, + "non_negative": 0, + "options": "Tax Category", + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": "eval:doc.docstatus == 0", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Reconciliation", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "sort_items", + "fieldtype": "Button", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "items", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Sort Items", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-07-17 23:53:18.064909", + "module": null, + "module_def": null, + "name": "Stock Reconciliation-sort_items", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Employee", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "old_employee_id", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "image", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Old Employee ID", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-09-24 11:29:02.966955", + "module": null, + "module_def": null, + "name": "Employee-old_employee_id", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "BOM", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "fg_warehouse", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "source_warehouse", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Target Warehouse", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-01-21 06:44:08.776914", + "module": null, + "module_def": null, + "name": "BOM-fg_warehouse", + "no_copy": 0, + "non_negative": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Print Settings", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "print_taxes_with_zero_amount", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "allow_print_for_cancelled", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Print taxes with zero amount", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-04-29 12:32:28.864970", + "module": null, + "module_def": null, + "name": "Print Settings-print_taxes_with_zero_amount", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "BOM", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_15", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "fg_warehouse", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-01-21 06:55:01.821947", + "module": null, + "module_def": null, + "name": "BOM-column_break_15", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "BOM", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "wip_warehouse", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "column_break_15", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Work-in-Progress Warehouse", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-01-21 06:42:52.960942", + "module": null, + "module_def": null, + "name": "BOM-wip_warehouse", + "no_copy": 0, + "non_negative": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "BOM", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "scrap_warehouse", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "wip_warehouse", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Scrap Warehouse", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-01-21 06:44:09.157050", + "module": null, + "module_def": null, + "name": "BOM-scrap_warehouse", + "no_copy": 0, + "non_negative": 0, + "options": "Warehouse", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "section_break_12", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "company_description", + "is_system_generated": 0, + "is_virtual": 0, + "label": "", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:22:27.862997", + "module": null, + "module_def": null, + "name": "Company-section_break_12", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Address", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "is_your_company_address", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "linked_with", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Is Your Company Address", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-10-14 17:41:40.878179", + "module": null, + "module_def": null, + "name": "Address-is_your_company_address", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": "", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Account", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "item", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "include_in_gross", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Expense Item", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-16 00:19:13.785448", + "module": null, + "module_def": null, + "name": "Account-item", + "no_copy": 0, + "non_negative": 0, + "options": "Item", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "company_bank_details", + "fieldtype": "Text", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "section_break_12", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Company Bank Details", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:22:19.201911", + "module": null, + "module_def": null, + "name": "Company-company_bank_details", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": "eval: !in_list(frappe.user_roles, \"Healthcare Receptionist\")", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "previous_invoice_number", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "is_return", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Previous Invoice Number", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-05 14:33:05.352542", + "module": null, + "module_def": null, + "name": "Sales Invoice-previous_invoice_number", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "vrn", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "website", + "is_system_generated": 0, + "is_virtual": 0, + "label": "VRN", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:22:02.995304", + "module": null, + "module_def": null, + "name": "Company-vrn", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Contact", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "is_billing_contact", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "is_primary_contact", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Is Billing Contact", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-02 11:00:03.432994", + "module": null, + "module_def": null, + "name": "Contact-is_billing_contact", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Supplier", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "vrn", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "tax_id", + "is_system_generated": 0, + "is_virtual": 0, + "label": "VRN", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-01-04 10:14:48.458823", + "module": null, + "module_def": null, + "name": "Supplier-vrn", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "tin", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "vrn", + "is_system_generated": 0, + "is_virtual": 0, + "label": "TIN", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:22:10.324100", + "module": null, + "module_def": null, + "name": "Company-tin", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "authotp", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "amended_from", + "is_system_generated": 0, + "is_virtual": 0, + "label": "AuthOTP", + "length": 0, + "mandatory_depends_on": null, + "modified": "2023-03-10 22:33:10.290382", + "module": null, + "module_def": null, + "name": "Sales Invoice-authotp", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "p_o_box", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "tin", + "is_system_generated": 0, + "is_virtual": 0, + "label": "P O Box", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-05-07 17:49:19.841962", + "module": null, + "module_def": null, + "name": "Company-p_o_box", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_sn02w", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "authotp_method", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2023-03-10 22:36:54.210165", + "module": null, + "module_def": null, + "name": "Sales Invoice-column_break_sn02w", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry Detail", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "item_weight_details", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "sample_quantity", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Item Weight Details", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-30 15:34:01.228484", + "module": null, + "module_def": null, + "name": "Stock Entry Detail-item_weight_details", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "city", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "p_o_box", + "is_system_generated": 0, + "is_virtual": 0, + "label": "City", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-05-07 17:49:20.430328", + "module": null, + "module_def": null, + "name": "Company-city", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry Detail", + "fetch_from": "item_code.weight_per_unit", + "fetch_if_empty": 0, + "fieldname": "weight_per_unit", + "fieldtype": "Float", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "item_weight_details", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Weight Per Unit", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-30 15:34:01.669365", + "module": null, + "module_def": null, + "name": "Stock Entry Detail-weight_per_unit", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "plot_number", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "city", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Plot Number", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-05-07 13:58:31.627080", + "module": null, + "module_def": null, + "name": "Company-plot_number", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry Detail", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "total_weight", + "fieldtype": "Float", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "weight_per_unit", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Total Weight", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-30 15:34:02.259714", + "module": null, + "module_def": null, + "name": "Stock Entry Detail-total_weight", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "block_number", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "plot_number", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Block Number", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-05-07 13:58:32.111317", + "module": null, + "module_def": null, + "name": "Company-block_number", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry Detail", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_32", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "total_weight", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-30 15:34:02.814827", + "module": null, + "module_def": null, + "name": "Stock Entry Detail-column_break_32", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "street", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "block_number", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Street", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-05-07 13:58:32.664481", + "module": null, + "module_def": null, + "name": "Company-street", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry Detail", + "fetch_from": "item_code.weight_uom", + "fetch_if_empty": 0, + "fieldname": "weight_uom", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "column_break_32", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Weight UOM", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-30 15:34:03.282211", + "module": null, + "module_def": null, + "name": "Stock Entry Detail-weight_uom", + "no_copy": 0, + "non_negative": 0, + "options": "UOM", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Purchase Invoice Item", + "fetch_from": "item_code.witholding_tax_rate_on_purchase", + "fetch_if_empty": 1, + "fieldname": "withholding_tax_rate", + "fieldtype": "Float", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "item_tax_template", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Withholding Tax Rate", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-07-17 01:03:26.025362", + "module": null, + "module_def": null, + "name": "Purchase Invoice Item-withholding_tax_rate", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Purchase Invoice Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "withholding_tax_entry", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "withholding_tax_rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Withholding Tax Entry", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-07-17 16:37:56.527525", + "module": null, + "module_def": null, + "name": "Purchase Invoice Item-withholding_tax_entry", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Employee", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "heslb_f4_index_number", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "national_identity", + "is_system_generated": 0, + "is_virtual": 0, + "label": "HESLB F4 Index Number", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-08-16 17:52:02.294599", + "module": null, + "module_def": null, + "name": "Employee-heslb_f4_index_number", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice Item", + "fetch_from": "item_code.withholding_tax_rate_on_sales", + "fetch_if_empty": 0, + "fieldname": "withholding_tax_rate", + "fieldtype": "Percent", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "item_tax_template", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Withholding Tax Rate", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 00:57:44.450609", + "module": null, + "module_def": null, + "name": "Sales Invoice Item-withholding_tax_rate", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Order", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_item_discount", + "fieldtype": "Percent", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "scan_barcode", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Item Discount", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-02-04 08:16:47.825862", + "module": null, + "module_def": null, + "name": "Sales Order-default_item_discount", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "withholding_tax_entry", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 1, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "withholding_tax_rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Withholding Tax Entry", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-06 01:34:59.329482", + "module": null, + "module_def": null, + "name": "Sales Invoice Item-withholding_tax_entry", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": "eval: doc.stock_entry_type == \"Send to Warehouse\"", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "final_destination", + "fieldtype": "Select", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "target_address_display", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Final Destination", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-29 18:07:10.893479", + "module": null, + "module_def": null, + "name": "Stock Entry-final_destination", + "no_copy": 0, + "non_negative": 0, + "options": "", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "authotp_validated", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "column_break_sn02w", + "is_system_generated": 0, + "is_virtual": 0, + "label": "AuthOTP Validated", + "length": 0, + "mandatory_depends_on": null, + "modified": "2023-03-10 22:36:54.540607", + "module": null, + "module_def": null, + "name": "Sales Invoice-authotp_validated", + "no_copy": 1, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Material Request Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "stock_reconciliation", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "lead_time_date", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Stock Reconciliation", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-24 22:47:46.584890", + "module": null, + "module_def": null, + "name": "Material Request Item-stock_reconciliation", + "no_copy": 0, + "non_negative": 0, + "options": "Stock Reconciliation", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "max_records_in_dialog", + "fieldtype": "Int", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "sales_monthly_history", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Max records in Dialog", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-05-07 08:55:04.364472", + "module": null, + "module_def": null, + "name": "Company-max_records_in_dialog", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "total_net_weight", + "fieldtype": "Float", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "section_break_19", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Total Net Weight", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-30 14:59:16.406735", + "module": null, + "module_def": null, + "name": "Stock Entry-total_net_weight", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Journal Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "from_date", + "fieldtype": "Date", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "auto_repeat", + "is_system_generated": 0, + "is_virtual": 0, + "label": "From Date", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:22:47.765536", + "module": null, + "module_def": null, + "name": "Journal Entry-from_date", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "allow_override_net_rate", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "net_rate", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Allow Override Net Rate", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-07-02 17:20:45.022013", + "module": null, + "module_def": null, + "name": "Sales Invoice Item-allow_override_net_rate", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 1, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Journal Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "to_date", + "fieldtype": "Date", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "from_date", + "is_system_generated": 0, + "is_virtual": 0, + "label": "To Date", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:22:38.720845", + "module": null, + "module_def": null, + "name": "Journal Entry-to_date", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 1, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": "Value Added Tax Registration Number (VAT RN = VRN)", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Customer", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "vrn", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "tax_id", + "is_system_generated": 0, + "is_virtual": 0, + "label": "VRN", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:21:41.980276", + "module": null, + "module_def": null, + "name": "Customer-vrn", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "withholding_section", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_expense_claim_payable_account", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Withholding", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-05 22:35:42.216333", + "module": null, + "module_def": null, + "name": "Company-withholding_section", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_withholding_payable_account", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "withholding_section", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Withholding Payable Account", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-07-17 00:37:50.184521", + "module": null, + "module_def": null, + "name": "Company-default_withholding_payable_account", + "no_copy": 0, + "non_negative": 0, + "options": "Account", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "auto_create_for_purchase_withholding", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_withholding_payable_account", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Auto Create For Purchase Withholding", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-09-01 23:21:15.437803", + "module": null, + "module_def": null, + "name": "Company-auto_create_for_purchase_withholding", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": "auto_create_for_purchase_withholding", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "auto_submit_for_purchase_withholding", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "auto_create_for_purchase_withholding", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Auto Submit For Purchase Withholding", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-05 22:48:17.854167", + "module": null, + "module_def": null, + "name": "Company-auto_submit_for_purchase_withholding", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_55", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "auto_submit_for_purchase_withholding", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-05 22:48:18.181399", + "module": null, + "module_def": null, + "name": "Company-column_break_55", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": "eval: !in_list(frappe.user_roles, \"Healthcare Receptionist\")", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_item_discount", + "fieldtype": "Percent", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "update_stock", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Item Discount", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-01-09 19:30:39.855293", + "module": null, + "module_def": null, + "name": "Sales Invoice-default_item_discount", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_60", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "student_applicant_fees_revenue_account", + "is_system_generated": 1, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-18 15:47:26.978439", + "module": null, + "module_def": null, + "name": "Company-column_break_60", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": "", + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_withholding_receivable_account", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "column_break_55", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Withholding Receivable Account", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-05 22:48:18.477944", + "module": null, + "module_def": null, + "name": "Company-default_withholding_receivable_account", + "no_copy": 0, + "non_negative": 0, + "options": "Account", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": "company.default_item_tax_template", + "fetch_if_empty": 1, + "fieldname": "default_item_tax_template", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_item_discount", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Item Tax Template", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-02-08 22:50:03.490542", + "module": null, + "module_def": null, + "name": "Sales Invoice-default_item_tax_template", + "no_copy": 0, + "non_negative": 0, + "options": "Item Tax Template", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "auto_create_for_sales_withholding", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_withholding_receivable_account", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Auto Create For Sales Withholding", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-09-01 23:21:15.814351", + "module": null, + "module_def": null, + "name": "Company-auto_create_for_sales_withholding", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "section_break_80", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_item_tax_template", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-02-08 22:50:04.586234", + "module": null, + "module_def": null, + "name": "Sales Invoice-section_break_80", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "0", + "depends_on": "auto_create_for_sales_withholding", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "auto_submit_for_sales_withholding", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "auto_create_for_sales_withholding", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Auto Submit For Sales Withholding", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-05 22:48:18.789116", + "module": null, + "module_def": null, + "name": "Company-auto_submit_for_sales_withholding", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "transporter_info", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "letter_head", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Transporter Info", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:00.237708", + "module": null, + "module_def": null, + "name": "Stock Entry-transporter_info", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "transporter", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "transporter_info", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Transporter", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:00.755674", + "module": null, + "module_def": null, + "name": "Stock Entry-transporter", + "no_copy": 0, + "non_negative": 0, + "options": "Supplier", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "driver", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "transporter", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Driver", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:01.308481", + "module": null, + "module_def": null, + "name": "Stock Entry-driver", + "no_copy": 0, + "non_negative": 0, + "options": "Driver", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "transport_receipt_no", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "driver", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Transport Receipt No", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:01.837481", + "module": null, + "module_def": null, + "name": "Stock Entry-transport_receipt_no", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "vehicle_no", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "transport_receipt_no", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Vehicle No", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:02.364744", + "module": null, + "module_def": null, + "name": "Stock Entry-vehicle_no", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_69", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "vehicle_no", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:02.892782", + "module": null, + "module_def": null, + "name": "Stock Entry-column_break_69", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "transporter_name", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "column_break_69", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Transporter Name", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:03.373538", + "module": null, + "module_def": null, + "name": "Stock Entry-transporter_name", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": "driver.full_name", + "fetch_if_empty": 0, + "fieldname": "driver_name", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "transporter_name", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Driver Name", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:03.900970", + "module": null, + "module_def": null, + "name": "Stock Entry-driver_name", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Stock Entry", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "transport_receipt_date", + "fieldtype": "Date", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "driver_name", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Transport Receipt Date", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-23 16:43:04.427622", + "module": null, + "module_def": null, + "name": "Stock Entry-transport_receipt_date", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "price_reduction", + "fieldtype": "Currency", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "base_net_total", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Total Price Reduction Amount", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-28 02:56:42.240626", + "module": null, + "module_def": null, + "name": "Sales Invoice-price_reduction", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Item", + "fetch_from": "", + "fetch_if_empty": 0, + "fieldname": "default_tax_template", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "item_tax_section_break", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Tax Template", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-03-10 12:04:59.499730", + "module": null, + "module_def": null, + "name": "Item-default_tax_template", + "no_copy": 0, + "non_negative": 0, + "options": "Item Tax Template", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice Item", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "delivery_status", + "fieldtype": "Select", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "sales_order", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Delivery Status", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-19 18:17:11.595366", + "module": null, + "module_def": null, + "name": "Sales Invoice Item-delivery_status", + "no_copy": 0, + "non_negative": 0, + "options": "\nNot Delivered\nPart Delivered\nDelivered\nOver Delivered", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "bypass_material_request_validation", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "stock_adjustment_account", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Bypass Material Request validation on Stock Entry", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-09-10 16:46:34.320822", + "module": null, + "module_def": null, + "name": "Company-bypass_material_request_validation", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "default_item_tax_template", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "default_in_transit_warehouse", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Default Item Tax Template", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-03-31 08:45:00.684605", + "module": null, + "module_def": null, + "name": "Company-default_item_tax_template", + "no_copy": 0, + "non_negative": 0, + "options": "Item Tax Template", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "1", + "depends_on": null, + "description": "For Sales Invoices", + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Company", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "enabled_auto_create_delivery_notes", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "column_break_32", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Enabled Auto Create Delivery Notes", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-07-17 22:30:49.037893", + "module": null, + "module_def": null, + "name": "Company-enabled_auto_create_delivery_notes", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Purchase Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "reference", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "language", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Reference", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-01-20 13:49:43.238770", + "module": null, + "module_def": null, + "name": "Purchase Invoice-reference", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": "eval: !in_list(frappe.user_roles, \"Healthcare Receptionist\")", + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "statutory_details", + "fieldtype": "Section Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "po_date", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Statutory Details", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:25:08.716714", + "module": null, + "module_def": null, + "name": "Sales Invoice-statutory_details", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "tra_control_number", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "statutory_details", + "is_system_generated": 0, + "is_virtual": 0, + "label": "TRA Control Number", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:24:51.344893", + "module": null, + "module_def": null, + "name": "Sales Invoice-tra_control_number", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "witholding_tax_certificate_number", + "fieldtype": "Data", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "tra_control_number", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Witholding Tax Certificate Number", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:23:45.693752", + "module": null, + "module_def": null, + "name": "Sales Invoice-witholding_tax_certificate_number", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "column_break_29", + "fieldtype": "Column Break", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "witholding_tax_certificate_number", + "is_system_generated": 0, + "is_virtual": 0, + "label": null, + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:24:28.702495", + "module": null, + "module_def": null, + "name": "Sales Invoice-column_break_29", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": "pos_profile.electronic_fiscal_device", + "fetch_if_empty": 1, + "fieldname": "electronic_fiscal_device", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "column_break_29", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Electronic Fiscal Device", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-03-05 16:05:01.332647", + "module": null, + "module_def": null, + "name": "Sales Invoice-electronic_fiscal_device", + "no_copy": 0, + "non_negative": 0, + "options": "Electronic Fiscal Device", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "efd_z_report", + "fieldtype": "Link", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "electronic_fiscal_device", + "is_system_generated": 0, + "is_virtual": 0, + "label": "EFD Z Report", + "length": 0, + "mandatory_depends_on": null, + "modified": "2019-12-20 19:24:07.925266", + "module": null, + "module_def": null, + "name": "Sales Invoice-efd_z_report", + "no_copy": 0, + "non_negative": 0, + "options": "EFD Z Report", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "excise_duty_applicable", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "col_break23", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Excise Duty Applicable", + "length": 0, + "mandatory_depends_on": null, + "modified": "2021-03-06 07:42:46.718597", + "module": null, + "module_def": null, + "name": "Sales Invoice-excise_duty_applicable", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 0, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": "1", + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": "", + "fetch_if_empty": 0, + "fieldname": "enabled_auto_create_delivery_notes", + "fieldtype": "Check", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 0, + "insert_after": "connections_tab", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Enabled Auto Create Delivery Notes", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-08-13 18:32:53.375956", + "module": null, + "module_def": null, + "name": "Sales Invoice-enabled_auto_create_delivery_notes", + "no_copy": 0, + "non_negative": 0, + "options": null, + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 0, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 0, + "unique": 0, + "width": null + }, + { + "allow_in_quick_entry": 0, + "allow_on_submit": 1, + "bold": 0, + "collapsible": 0, + "collapsible_depends_on": null, + "columns": 0, + "default": null, + "depends_on": null, + "description": null, + "docstatus": 0, + "doctype": "Custom Field", + "dt": "Sales Invoice", + "fetch_from": null, + "fetch_if_empty": 0, + "fieldname": "delivery_status", + "fieldtype": "Select", + "hidden": 0, + "hide_border": 0, + "hide_days": 0, + "hide_seconds": 0, + "ignore_user_permissions": 0, + "ignore_xss_filter": 0, + "in_global_search": 0, + "in_list_view": 0, + "in_preview": 0, + "in_standard_filter": 1, + "insert_after": "demo_done", + "is_system_generated": 0, + "is_virtual": 0, + "label": "Delivery Status", + "length": 0, + "mandatory_depends_on": null, + "modified": "2020-06-19 18:16:07.249867", + "module": null, + "module_def": null, + "name": "Sales Invoice-delivery_status", + "no_copy": 0, + "non_negative": 0, + "options": "\nNot Delivered\nPart Delivered\nDelivered\nOver Delivered", + "permlevel": 0, + "precision": null, + "print_hide": 0, + "print_hide_if_no_value": 0, + "print_width": null, + "read_only": 1, + "read_only_depends_on": null, + "report_hide": 0, + "reqd": 0, + "search_index": 0, + "translatable": 1, + "unique": 0, + "width": null + } +] \ No newline at end of file From 709190b42cdb3ad2fa09fbce40ac289aeee85220 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:17:59 +0300 Subject: [PATCH 33/36] chore: drop education names from legacy fixture list --- .../fixtures/old_fixtures_from_hooks.py | 26 ------------------- 1 file changed, 26 deletions(-) diff --git a/csf_tz/patches/fixtures/old_fixtures_from_hooks.py b/csf_tz/patches/fixtures/old_fixtures_from_hooks.py index 6828b7c7..69181c80 100644 --- a/csf_tz/patches/fixtures/old_fixtures_from_hooks.py +++ b/csf_tz/patches/fixtures/old_fixtures_from_hooks.py @@ -28,20 +28,12 @@ "Company-default_item_tax_template", "Company-default_withholding_payable_account", "Company-default_withholding_receivable_account", - "Company-education_section", "Company-enabled_auto_create_delivery_notes", - "Company-fee_bank_account", "Company-max_records_in_dialog", - "Company-nmb_password", - "Company-nmb_series", - "Company-nmb_url", - "Company-nmb_username", "Company-p_o_box", "Company-plot_number", "Company-section_break_12", - "Company-send_fee_details_to_bank", "Company-street", - "Company-student_applicant_fees_revenue_account", "Company-tin", "Company-vrn", "Company-withholding_section", @@ -49,12 +41,6 @@ "Customer-vrn", "Delivery Note-form_sales_invoice", "Employee-old_employee_id", - "Fee Structure-default_fee_category", - "Fees-abbr", - "Fees-bank_reference", - "Fees-callback_token", - "Fees-from_date", - "Fees-to_date", "Item-default_tax_template", "Item-excisable_item", "Item-withholding_tax_rate_on_sales", @@ -72,9 +58,6 @@ "POS Profile-electronic_fiscal_device", "Print Settings-compact_item_print", "Print Settings-print_taxes_with_zero_amount", - "Program Fee-default_fee_category", - "Program-fees", - "Program-program_fee", "Purchase Invoice Item-withholding_tax_entry", "Purchase Invoice Item-withholding_tax_rate", "Purchase Invoice-expense_record", @@ -121,11 +104,6 @@ "Stock Entry-vehicle_no", "Stock Reconciliation Item-material_request", "Stock Reconciliation-sort_items", - "Student Applicant-bank_reference", - "Student Applicant-fee_structure", - "Student Applicant-program_enrollment", - "Student Applicant-student_applicant_fee", - "Student-bank", "Supplier-vrn", "Vehicle Fine Record-fully_paid", "Vehicle Log-column_break_11", @@ -134,8 +112,6 @@ "Vehicle Service-invoice", "Vehicle Service-quantity", "Vehicle Service-spare_name", - "Fees-base_grand_total", - "Fees-advance_paid", "Employee-heslb_f4_index_number", "Sales Invoice Item-is_ignored_in_pending_qty", "Sales Invoice-authotp", @@ -210,8 +186,6 @@ "Sales Invoice-search_fields", "Scheduled Job Log-main-track_changes", "Stock Entry-from_warehouse-fetch_from", - "Student Applicant-application_status-options", - "Student Applicant-application_status-read_only", "Supplier-read_only_onload", "Supplier-tax_id-bold", "Supplier-tax_id-label", From ca5bb7c141a167cbd7889c1224441d3f3f904bc1 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:18:07 +0300 Subject: [PATCH 34/36] feat: drop moved education doctypes on sites without edu_tz --- csf_tz/patches/remove_education_doctypes.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 csf_tz/patches/remove_education_doctypes.py diff --git a/csf_tz/patches/remove_education_doctypes.py b/csf_tz/patches/remove_education_doctypes.py new file mode 100644 index 00000000..65f72818 --- /dev/null +++ b/csf_tz/patches/remove_education_doctypes.py @@ -0,0 +1,20 @@ +"""Drops the education DocTypes that moved to edu_tz from sites that do not run edu_tz.""" + +import frappe + +MOVED_DOCTYPES = ("NMB Callback", "Student Applicant Fees") + + +def execute(): + if "edu_tz" in frappe.get_installed_apps(): + return + + for doctype in MOVED_DOCTYPES: + if frappe.db.get_value("DocType", doctype, "module") != "CSF TZ": + continue + + if frappe.db.count(doctype): + frappe.logger().warning(f"{doctype} has records and moved to edu_tz; install edu_tz to keep it.") + continue + + frappe.delete_doc("DocType", doctype, force=True, ignore_permissions=True) From 853236a835cdbef117173535b6eb53c066c21e41 Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:18:16 +0300 Subject: [PATCH 35/36] chore: register remove_education_doctypes patch --- csf_tz/patches.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/csf_tz/patches.txt b/csf_tz/patches.txt index c7bb39db..9140ea9a 100755 --- a/csf_tz/patches.txt +++ b/csf_tz/patches.txt @@ -24,3 +24,4 @@ csf_tz.patches.migrate_vfd_providers_to_csf_tz execute:frappe.delete_doc_if_exists("Report", "Stock Ledger Mismatch") csf_tz.patches.remove_ot_component_custom_fields csf_tz.patches.remove_deleted_modules_metadata +csf_tz.patches.remove_education_doctypes From cb575d6bc4a7782cb00b711f08986a44a637d3fd Mon Sep 17 00:00:00 2001 From: av-dev2 Date: Fri, 21 Aug 2026 23:18:17 +0300 Subject: [PATCH 36/36] refactor: unregister education hooks and scripts moved to edu_tz --- csf_tz/hooks.py | 21 --------------------- csf_tz/tests/test_bank_api_shim.py | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+), 21 deletions(-) create mode 100644 csf_tz/tests/test_bank_api_shim.py diff --git a/csf_tz/hooks.py b/csf_tz/hooks.py index b91bc488..95d2a6e8 100755 --- a/csf_tz/hooks.py +++ b/csf_tz/hooks.py @@ -50,15 +50,11 @@ "Warehouse": "csf_tz/warehouse.js", "Company": "csf_tz/company.js", "Stock Reconciliation": "csf_tz/stock_reconciliation.js", - "Fees": "csf_tz/fees.js", - "Program Enrollment Tool": "csf_tz/program_enrollment_tool.js", "Purchase Invoice": "csf_tz/purchase_invoice.js", "Quotation": "csf_tz/quotation.js", "Purchase Receipt": "csf_tz/purchase_receipt.js", "Purchase Order": "csf_tz/purchase_order.js", - "Student Applicant": "csf_tz/student_applicant.js", "Bank Reconciliation": "csf_tz/bank_reconciliation.js", - "Program Enrollment": "csf_tz/program_enrollment.js", "Payroll Entry": [ "csf_tz/payroll_entry.js", "stanbic/payroll_entry.js", @@ -122,7 +118,6 @@ after_migrate = [ "csf_tz.utils.create_custom_fields.execute", - "csf_tz.utils.authority_notification_settings_fields.execute", "csf_tz.utils.create_property_setter.execute", "csf_tz.patches.custom_fields.vfd_providers_updated_custom_fields.execute", "csf_tz.patches.migrate_vfd_providers_to_csf_tz.execute", @@ -201,25 +196,10 @@ "Journal Entry": { "before_save": "csf_tz.csftz_hooks.budget.check_budget_for_journal_entry", }, - "Fees": { - "before_insert": "csf_tz.custom_api.set_fee_abbr", - "after_insert": "csf_tz.bank_api.set_callback_token", - "on_submit": "csf_tz.bank_api.invoice_submission", - "before_cancel": "csf_tz.custom_api.on_cancel_fees", - }, - "Program Enrollment": { - "onload": "csf_tz.csftz_hooks.program_enrollment.create_course_enrollments_override", - "refresh": "csf_tz.csftz_hooks.program_enrollment.create_course_enrollments_override", - "reload": "csf_tz.csftz_hooks.program_enrollment.create_course_enrollments_override", - "before_submit": "csf_tz.csftz_hooks.program_enrollment.validate_submit_program_enrollment", - }, "Stock Entry": { "validate": "csf_tz.custom_api.calculate_total_net_weight", "before_save": "csf_tz.csftz_hooks.stock.import_from_bom", }, - "Student Applicant": { - "on_update_after_submit": "csf_tz.csftz_hooks.student_applicant.make_student_applicant_fees", - }, "Payroll Entry": { "before_insert": "csf_tz.csftz_hooks.payroll.before_insert_payroll_entry", "before_update_after_submit": "csf_tz.csftz_hooks.payroll.before_update_after_submit", @@ -296,7 +276,6 @@ }, "daily": [ "csf_tz.custom_api.create_delivery_note_for_all_pending_sales_invoice", - "csf_tz.bank_api.reconciliation", "csf_tz.csftz_hooks.additional_salary.generate_additional_salary_records", "csf_tz.csftz_hooks.exchange_calculations.update_pending_transactions", "csf_tz.csf_tz.doctype.vehicle_sync_task.processor.seed_vehicle_sync_queue", diff --git a/csf_tz/tests/test_bank_api_shim.py b/csf_tz/tests/test_bank_api_shim.py new file mode 100644 index 00000000..5b27f92f --- /dev/null +++ b/csf_tz/tests/test_bank_api_shim.py @@ -0,0 +1,23 @@ +from unittest.mock import patch + +import frappe +from frappe.tests.utils import FrappeTestCase + +from csf_tz import bank_api + + +class TestBankApiShim(FrappeTestCase): + def test_throws_when_edu_tz_is_not_installed(self): + with patch("frappe.get_installed_apps", return_value=["frappe", "erpnext", "csf_tz"]): + with self.assertRaises(frappe.ValidationError): + bank_api.get_callback_handler("receive_callback") + + def test_forwards_to_edu_tz_when_installed(self): + if "edu_tz" not in frappe.get_installed_apps(): + self.skipTest("edu_tz is not installed on this site") + from edu_tz.edu_tz.nmb import api + + self.assertIs(bank_api.get_callback_handler("receive_callback"), api.receive_callback) + self.assertIs( + bank_api.get_callback_handler("receive_validate_reference"), api.receive_validate_reference + )